tirreno is designed to be customized for your specific security needs. No CLA or pull request is required for local modifications.
The two main customization points are:
tirreno includes pre-configured rule sets for common security scenarios. Presets provide a quick starting point—select one from the Rules page dropdown and click Apply.
| Preset | Use Case |
|---|---|
default | Empty rules (start from scratch) |
account_takeover | Detect compromised accounts via new devices, locations, password changes |
credential_stuffing | Detect automated login attempts and brute force attacks |
content_spam | Detect spam content and suspicious posting patterns |
account_registration | Protect registration from fake accounts and bots |
fraud_prevention | General fraud detection across multiple vectors |
insider_threat | Detect unusual employee behavior and data exfiltration |
bot_detection | Identify automated traffic and crawlers |
dormant_account | Monitor reactivation of long-inactive accounts |
multi_accounting | Detect users with multiple accounts |
promo_abuse | Detect promotional code and offer abuse |
api_protection | Protect APIs from abuse and scanning |
high_risk_regions | Flag traffic from high-fraud geographic regions |
Each preset assigns weights to specific rules. You can customize the weights after applying a preset.
Rule weights:
| Weight | Value | Effect on Risk Score |
|---|---|---|
| Positive | -20 | Decreases risk (trusted behavior) |
| None | 0 | Rule disabled |
| Medium | 10 | Moderate risk increase |
| High | 20 | Significant risk increase |
| Extreme | 70 | Major risk increase |
Rules are organized by namespace (core vs custom) and category (prefix letter).
Namespaces:
| Namespace | Directory | Description |
|---|---|---|
\Tirreno\Rules\Core | assets/rules/core/ | Built-in rules (109 rules) |
\Tirreno\Rules\Custom | assets/rules/custom/ | Your custom rules |
Rule categories by prefix:
| Prefix | Category | Example |
|---|---|---|
| A | Account takeover | A01–A08 |
| B | Behaviour | B01–B26 |
| C | Country | C01–C16 |
| D | Device | D01–D10 |
| E | E01–E30 | |
| I | IP | I01–I12 |
| P | Phone | P01–P04 |
| R | Reuse/Blacklist | R01–R03 |
| X | Custom/Extra | X01, X02, ... |
Custom rules must use the X prefix (e.g., X01.php, X02.php). Core rule prefixes (A–R) are reserved.
tirreno includes standard detection rules organized by category:
| Rule | Name | Description |
|---|---|---|
| A01 | Multiple login fail | User failed to login multiple times in a short term |
| A02 | Login failed on new device | User failed to login with new device |
| A03 | New device and new country | User logged in with new device from new location |
| A04 | New device and new subnet | User logged in with new device from new subnet |
| A05 | Password change on new device | User changed their password on new device |
| A06 | Password change in new country | User changed their password in new country |
| A07 | Password change in new subnet | User changed their password in new subnet |
| A08 | Browser language changed | User accessed the account with new browser language |
| Rule | Name | Description |
|---|---|---|
| B01 | Multiple countries | IP addresses are located in diverse countries |
| B02 | User has changed a password | The user has changed their password |
| B03 | User has changed an email | The user has changed their email |
| B04 | Multiple 5xx errors | User made multiple requests which evoked internal server error |
| B05 | Multiple 4xx errors | User made multiple requests which cannot be fulfilled |
| B06 | Potentially vulnerable URL | User made a request to suspicious URL |
| B07 | User's full name contains digits | Full name contains digits |
| B08 | Dormant account (30 days) | Account has been inactive for 30 days |
| B09 | Dormant account (90 days) | Account has been inactive for 90 days |
| B10 | Dormant account (1 year) | Account has been inactive for a year |
| B11 | New account (1 day) | Account has been created today |
| B12 | New account (1 week) | Account has been created this week |
| B13 | New account (1 month) | Account has been created this month |
| B14 | Aged account (>30 days) | Account has been created over 30 days ago |
| B15 | Aged account (>90 days) | Account has been created over 90 days ago |
| B16 | Aged account (>180 days) | Account has been created over 180 days ago |
| B17 | Single country | IP addresses are located in a single country |
| B18 | HEAD request | HTTP request HEAD method is often used by bots |
| B19 | Night time requests | User was active from midnight till 5 a.m. |
| B20 | Multiple countries in one session | User's country changed in less than 30 minutes |
| B21 | Multiple devices in one session | User's device changed in less than 30 minutes |
| B22 | Multiple IP addresses in one session | User's IP changed in less than 30 minutes |
| B23 | User's full name contains space or hyphen | Full name contains space or hyphen |
| B24 | Empty referer | User made a request without a referer |
| B25 | Unauthorized request | User made a successful request without authorization |
| B26 | Single event sessions | User had sessions with only one event |
| Rule | Name | Description |
|---|---|---|
| C01 | Nigeria IP address | IP address located in Nigeria |
| C02 | India IP address | IP address located in India |
| C03 | China IP address | IP address located in China |
| C04 | Brazil IP address | IP address located in Brazil |
| C05 | Pakistan IP address | IP address located in Pakistan |
| C06 | Indonesia IP address | IP address located in Indonesia |
| C07 | Venezuela IP address | IP address located in Venezuela |
| C08 | South Africa IP address | IP address located in South Africa |
| C09 | Philippines IP address | IP address located in Philippines |
| C10 | Romania IP address | IP address located in Romania |
| C11 | Russia IP address | IP address located in Russia |
| C12 | European IP address | IP address located in European Union |
| C13 | North America IP address | IP address located in Canada or USA |
| C14 | Australia IP address | IP address located in Australia |
| C15 | UAE IP address | IP address located in United Arab Emirates |
| C16 | Japan IP address | IP address located in Japan |
| Rule | Name | Description |
|---|---|---|
| D01 | Device is unknown | User has manipulated device information |
| D02 | Device is Linux | Linux OS, increased risk of crawler bot |
| D03 | Device is bot | User agent identified as a bot |
| D04 | Rare browser device | User operates device with uncommon browser |
| D05 | Rare OS device | User operates device with uncommon OS |
| D06 | Multiple devices per user | User accesses account using multiple devices |
| D07 | Several desktop devices | User accesses account using different OS desktop devices |
| D08 | Two or more phone devices | User accesses account using numerous phone devices |
| D09 | Old browser | User accesses account using an old browser version |
| D10 | Potentially vulnerable User-Agent | User made a request with suspicious User-Agent |
| Rule | Name | Description |
|---|---|---|
| E01 | Invalid email format | Invalid email format |
| E02 | New domain and no breaches | Email belongs to recently created domain with no breach history |
| E03 | Suspicious words in email | Email contains auto-generated mailbox patterns |
| E04 | Numeric email name | Email username consists entirely of numbers |
| E05 | Special characters in email | Email has unusually high number of special characters |
| E06 | Consecutive digits in email | Email includes at least two consecutive digits |
| E07 | Long email username | Email username exceeds average length |
| E08 | Long domain name | Email domain name is too long |
| E09 | Free email provider | Email belongs to free provider |
| E10 | The website is unavailable | Domain's website seems to be inactive |
| E11 | Disposable email | Disposable email addresses are temporary |
| E12 | Free email and no breaches | Email belongs to free provider with no breach history |
| E13 | New domain | Domain name was registered recently |
| E14 | No MX record | Email's domain has no MX record |
| E15 | No breaches for email | Email was not involved in any data breaches |
| E16 | Domain appears in spam lists | Email appears in spam lists |
| E17 | Free email and spam | Email appears in spam lists and is from free provider |
| E19 | Multiple emails changed | User has changed their email |
| E20 | Established domain (> 3 year old) | Email belongs to domain registered at least 3 years ago |
| E21 | No vowels in email | Email username does not contain any vowels |
| E22 | No consonants in email | Email username does not contain any consonants |
| E23 | Educational domain (.edu) | Email belongs to educational domain |
| E24 | Government domain (.gov) | Email belongs to government domain |
| E25 | Military domain (.mil) | Email belongs to military domain |
| E26 | iCloud mailbox | Email belongs to Apple domains (icloud.com, me.com, mac.com) |
| E27 | Email breaches | Email appears in data breaches |
| E28 | No digits in email | Email address does not include digits |
| E29 | Old breach (>3 years) | Earliest data breach appeared more than 3 years ago |
| E30 | Domain with average rank | Email domain has Tranco rank between 100,000 and 4,000,000 |
Note: E18 is reserved for future use.
| Rule | Name | Description |
|---|---|---|
| I01 | IP belongs to TOR | IP assigned to The Onion Router network |
| I02 | IP hosting domain | Higher risk of crawler bot |
| I03 | IP appears in spam list | User may have exhibited unwanted activity before |
| I04 | Shared IP | Multiple users detected on same IP address |
| I05 | IP belongs to commercial VPN | User tries to hide real location |
| I06 | IP belongs to datacenter | User is utilizing an ISP datacenter |
| I07 | IP belongs to Apple Relay | IP belongs to iCloud Private Relay |
| I08 | IP belongs to Starlink | IP belongs to SpaceX satellite network |
| I09 | Numerous IPs | User accesses account with numerous IP addresses |
| I10 | Only residential IPs | User uses only residential IP addresses |
| I11 | Single network | IP addresses belong to one network |
| I12 | IP belongs to LAN | IP address belongs to local access network |
| Rule | Name | Description |
|---|---|---|
| P01 | Invalid phone format | User provided incorrect phone number |
| P02 | Phone country mismatch | Phone number country is not among user's login countries |
| P03 | Shared phone number | User provided a phone number shared with another user |
| P04 | Valid phone | User provided correct phone number |
| Rule | Name | Description |
|---|---|---|
| R01 | IP in blacklist | This IP address appears in the blacklist |
| R02 | Email in blacklist | This email address appears in the blacklist |
| R03 | Phone in blacklist | This phone number appears in the blacklist |
Custom rules are placed in assets/rules/custom/ with filenames X01.php, X02.php, etc.
Each rule must:
Tirreno\Rules\Custom
\Tirreno\Assets\Rule
NAME, DESCRIPTION, ATTRIBUTES
defineCondition() method
See assets/rules/custom/X03.example.php for a complete example:
|
For rules that need custom data, create a Context class in assets/rules/custom/Context.php. See Context.example.php:
|
The rules engine uses ruler/ruler for condition evaluation. Available operators in defineCondition():
| Operator | Description | Example |
|---|---|---|
equalTo | Exact match | $this->rb['ea_total_country']->equalTo(1) |
notEqualTo | Not equal | $this->rb['eip_tor']->notEqualTo(true) |
greaterThan | Greater than | $this->rb['ea_total_ip']->greaterThan(9) |
greaterThanOrEqualTo | Greater or equal | $this->rb['ea_days_since_last_visit']->greaterThanOrEqualTo(30) |
lessThan | Less than | $this->rb['ea_days_since_account_creation']->lessThan(7) |
lessThanOrEqualTo | Less or equal | $this->rb['eup_device_count']->lessThanOrEqualTo(1) |
stringContains | Substring match | $this->rb['le_email']->stringContains('test') |
stringContainsInsensitive | Case-insensitive substring | $this->rb['le_domain_part']->stringContainsInsensitive('mail') |
startsWith | Prefix match | $this->rb['event_url_string']->startsWith('/api/') |
endsWith | Suffix match | $this->rb['le_email']->endsWith('.edu') |
sameAs | Variable comparison | $this->rb['lp_country_code']->sameAs($this->rb['eip_country_id']) |
Logical operators:
|
When writing custom rules, the following attributes are available in the defineCondition() method. Access them via $this->rb['attribute_name'].
From Event context:
| Attribute | Type | Description |
|---|---|---|
event_ip | array | IP IDs per event |
event_url_string | array | URLs per event |
event_empty_referer | array | Empty referer status per event |
event_device | array | Device IDs per event |
event_type | array | Event types |
event_http_code | array | HTTP response codes |
event_http_method | array | HTTP methods |
event_device_created | array | Device creation timestamps |
event_device_lastseen | array | Device last seen timestamps |
Derived event attributes:
| Attribute | Type | Description |
|---|---|---|
event_email_changed | bool | User changed email in recent events |
event_password_changed | bool | User changed password in recent events |
event_http_method_head | bool | HEAD request detected |
event_empty_referer | bool | Request had empty referer |
event_multiple_5xx_http | int | Count of 5xx server errors |
event_multiple_4xx_http | int | Count of 4xx client errors |
event_2xx_http | bool | Successful requests exist |
event_vulnerable_url | bool | URL matches suspicious patterns |
Raw account data from User context:
| Attribute | Type | Description |
|---|---|---|
ea_userid | string | User identifier |
ea_created | string | Account creation timestamp |
ea_lastseen | string | Last activity timestamp |
ea_total_visit | int | Total visits |
ea_total_country | int | Total countries |
ea_total_ip | int | Total IP addresses |
ea_total_device | int | Total devices |
ea_firstname | string | First name |
ea_lastname | string | Last name |
Derived account attributes:
| Attribute | Type | Description |
|---|---|---|
ea_days_since_account_creation | int | Days since account was created (-1 if unknown) |
ea_days_since_last_visit | int | Days since user's last activity (-1 if unknown) |
ea_fullname_has_numbers | bool | Full name contains digits |
ea_fullname_has_spaces_hyphens | bool | Full name contains spaces or hyphens |
From Ip context:
| Attribute | Type | Description |
|---|---|---|
eip_cidr_count | array | Count of IPs per CIDR |
eip_country_count | array | Count of IPs per country |
eip_country_id | array | Country IDs |
eip_data_center | bool | IP belongs to datacenter |
eip_tor | bool | IP belongs to TOR network |
eip_vpn | bool | IP belongs to commercial VPN |
eip_starlink | bool | IP belongs to Starlink |
eip_blocklist | bool | IP appears in spam/blocklist |
eip_has_fraud | bool | Fraud detected for IP |
eip_lan | bool | IP belongs to LAN |
eip_shared | int | Number of users sharing this IP |
eip_domains_count_len | int | Number of domains on IP |
eip_unique_cidrs | int | Number of unique network ranges |
eip_only_residential | bool | All IPs are residential (derived) |
From Device context:
| Attribute | Type | Description |
|---|---|---|
eup_device | array | Device types (desktop, smartphone, tablet, etc.) |
eup_browser_name | array | Browser names |
eup_browser_version | array | Browser versions |
eup_os_name | array | Operating system names |
eup_lang | array | Browser languages |
eup_ua | array | Raw user agent strings |
Derived device attributes:
| Attribute | Type | Description |
|---|---|---|
eup_device_count | int | Number of devices used |
eup_has_rare_browser | bool | User has uncommon browser |
eup_has_rare_os | bool | User has uncommon OS |
eup_vulnerable_ua | bool | User-Agent matches suspicious patterns |
From Session context:
| Attribute | Type | Description |
|---|---|---|
event_session_single_event | bool | Session had only one event |
event_session_multiple_country | bool | Country changed within 30 min |
event_session_multiple_ip | bool | IP changed within 30 min |
event_session_multiple_device | bool | Device changed within 30 min |
event_session_night_time | bool | Activity between midnight and 5 AM |
Last Email Attributes (le_):
| Attribute | Type | Description |
|---|---|---|
le_email | string | Email address |
le_local_part | string | Email username (before @) |
le_domain_part | string | Email domain (after @) |
le_blockemails | bool | Email is in blocklist |
le_data_breach | bool | Known data breaches |
le_checked | bool | Email has been verified |
le_fraud_detected | bool | Fraud detected for email |
le_alert_list | bool | Email on alert list |
Derived last email attributes:
| Attribute | Type | Description |
|---|---|---|
le_exists | bool | Email address exists |
le_is_invalid | bool | Email format is invalid |
le_has_suspicious_str | bool | Email contains suspicious patterns |
le_has_numeric_only_local_part | bool | Email username is all numbers |
le_email_has_consec_s_chars | bool | Email has consecutive special characters |
le_email_has_consec_nums | bool | Email has consecutive digits |
le_email_has_no_digits | bool | Email has no digits |
le_email_has_vowels | bool | Email username contains vowels |
le_email_has_consonants | bool | Email username contains consonants |
le_with_long_local_part_length | bool | Email username exceeds max length |
le_with_long_domain_length | bool | Email domain exceeds max length |
le_email_in_blockemails | bool | Email is in blocklist |
le_has_no_data_breaches | bool | No known data breaches |
le_appears_on_alert_list | bool | Email on alert list |
le_local_part_len | int | Length of email username |
Email Attributes (ee_):
| Attribute | Type | Description |
|---|---|---|
ee_email | array | All email addresses for user |
ee_earliest_breach | array | Earliest breach dates per email |
ee_days_since_first_breach | int | Days since earliest known breach (-1 if none) |
Last Domain Attributes (ld_):
| Attribute | Type | Description |
|---|---|---|
ld_disposable_domains | bool | Domain is disposable email provider |
ld_free_email_provider | bool | Domain is free email provider |
ld_blockdomains | bool | Domain is in blocklist |
ld_mx_record | bool | Domain has MX record |
ld_disabled | bool | Domain website is disabled |
ld_creation_date | string | Domain creation date |
ld_tranco_rank | int | Tranco ranking (-1 if not ranked) |
Derived last domain attributes:
| Attribute | Type | Description |
|---|---|---|
ld_is_disposable | bool | Domain is disposable email provider |
ld_days_since_domain_creation | int | Days since domain registration |
ld_domain_free_email_provider | bool | Domain is free email provider |
ld_from_blockdomains | bool | Domain is in blocklist |
ld_domain_without_mx_record | bool | Domain has no MX record |
ld_website_is_disabled | bool | Domain website is disabled |
From Phone context (ep_):
| Attribute | Type | Description |
|---|---|---|
ep_phone_number | array | Phone numbers |
ep_shared | array | Shared status per phone |
ep_type | array | Phone types |
Last phone from User context (lp_):
| Attribute | Type | Description |
|---|---|---|
lp_phone_number | string | Last phone number |
lp_country_code | string | Phone country code |
lp_invalid | bool | Phone number is invalid |
lp_fraud_detected | bool | Fraud detected for phone |
lp_alert_list | bool | Phone on alert list |
Derived phone attributes:
| Attribute | Type | Description |
|---|---|---|
lp_invalid_phone | bool | Phone number is invalid |
ep_shared_phone | bool | Phone is shared with other users |
tirreno maintains lists of suspicious patterns in assets/lists/:
| File | Purpose |
|---|---|
url.php | URL attack patterns (SQL injection, path traversal, etc.) |
user-agent.php | Suspicious user agent strings |
email.php | Suspicious email patterns |
file-extensions.php | File extension categories |
Each file returns a PHP array:
|
To add patterns:
assets/lists/
Example patterns by type:
| List | Example Patterns |
|---|---|
url.php | '.env', '../', '/wp-admin', 'phpmyadmin', '<script>' |
user-agent.php | Bot signatures, scanner identifiers, SQL injection attempts |
email.php | 'spam', 'test', 'dummy', '123', '000' |
________________________________________________________________________________