@nestjs-guardian/nest-ips
v1.1.2
Published
Application-level IDS/IPS module for NestJS
Maintainers
Readme
@nestjs-guardian/nest-ips
Application-level IDS/IPS for NestJS APIs.
What It Does
- Protects NestJS APIs with an IDS/IPS rule engine, not only a limiter.
- Detects attack patterns (401/404/429 spikes, scans, abusive routes, suspicious UA).
- Applies actions:
alert,rate-limit,ban,block. - Supports multi-worker/shared state via Redis and local mode via memory store.
- Sends security alerts to Slack and email with customizable templates.
- Can aggregate repeated
rateLimitalerts into periodic summary reports (alerts.rateLimitReport) to reduce alert spam. - Can enrich summary rows with VPN/proxy/hosting intelligence via built-in or custom
ipIntel.resolver.
Install
npm i @nestjs-guardian/nest-ipsQuick Start (Express/Nest)
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import {
IpsModule,
IpsGuard,
IpsInterceptor,
IpsNotFoundFilter,
createIpsMiddleware,
} from '@nestjs-guardian/nest-ips';
@Module({
imports: [IpsModule.forRoot({})],
providers: [
{ provide: APP_GUARD, useClass: IpsGuard },
{ provide: APP_INTERCEPTOR, useClass: IpsInterceptor },
{ provide: APP_FILTER, useClass: IpsNotFoundFilter },
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(createIpsMiddleware()).forRoutes('*');
}
}How Client IP Is Determined (Trust Proxy Model)
clientIp.mode: 'strict': trust forwarded headers only from trusted proxies.clientIp.mode: 'hops': resolve client IP by fixed proxy hop count.headersPrioritycontrols header parsing order.denyPrivateIpsFromHeaderscan reject private/local forwarded IPs.- See
Client IP Trust Modelbelow for detailed behavior and examples.
Rules Examples (401/404/429 + Path Rules)
IpsModule.forRoot({
profiles: {
default: {
behavior: { windowSec: 60, max401: 20, max404: 30, max429: 20 },
},
},
notFound: { windowSec: 60, max: 30 },
});[
{
"id": "block.admin.path",
"severity": "high",
"when": { "path": { "prefix": "/admin" } },
"match": [{ "field": "path", "contains": "/admin" }],
"action": "block",
"block": { "status": 403, "message": "Forbidden" }
}
]Storage (Memory vs Redis)
store.type: 'memory'(default): fastest local mode, state is per-process/per-worker.store.type: 'redis': shared counters/bans across workers and instances.store.type: 'auto': try Redis first, fallback to memory on connection failure.
store: {
type: 'auto',
redis: { url: process.env.REDIS_URL },
}Alerts (Slack/Email)
- Slack: requires
alerts.slack.webhookUrl. - Email: requires full
alerts.email.smtpconfig. - Optional:
alerts.rateLimitReportaggregates repeatedrateLimitevents (or all IPS alert events withscope: 'all') into periodic summary alerts. - If channel is configured but destination is missing, module throws startup error.
- Supports templates and field-based rendering.
Rate-Limit Summary Reports (alerts.rateLimitReport)
Use this feature to reduce alert spam from repeated IPS alerts.
What it does:
- Collects repeated IPS alert events in memory (scope is configurable).
- Groups identical events by
ruleId + ip + method + path + profile. - Sends one periodic summary alert (Slack/Email) instead of many repeated alerts.
- Can optionally suppress immediate alerts for included events.
What it does not change:
- Events outside selected
scopeare still sent normally.
Notes:
- If
alerts.rateLimitReportis omitted, this feature is disabled (default behavior remains unchanged). - If
alerts.rateLimitReportis provided andenabledis omitted, summary reporting is enabled by default. - Summary sending requires at least one configured alert channel (
alerts.slackand/oralerts.email). - Aggregation is per-process (each app instance/worker sends its own summary).
Mode selection via suppressImmediate
Use one config and switch behavior with suppressImmediate:
IpsModule.forRoot({
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
},
rateLimitReport: {
// enabled: true, // optional (default when object is present)
// scope: 'rateLimit', // default; use 'all' to aggregate behavior signals and other IPS alerts too
period: '30m',
suppressImmediate: true, // set false for hybrid mode
maxItems: 50,
maxGroups: 2000,
},
},
});How suppressImmediate works:
suppressImmediate: true-> summary-only mode (recommended for noisy production APIs)- included alerts (based on
scope) are collected and sent only in periodic reports (period) - immediate alerts for included events are suppressed
- included alerts (based on
suppressImmediate: false-> hybrid mode (immediate alerts + periodic summary)- immediate alerts are still sent
- the same events are also aggregated into periodic reports
Important:
- This flag affects only events included by
rateLimitReport.scope. - To aggregate all IPS alert-producing events (including behavior signals like
route-not-found), setscope: 'all'.
Aggregation scope (scope)
scope: 'rateLimit'(default)- aggregates only
rateLimitdecisions
- aggregates only
scope: 'all'- aggregates all alert-producing IPS events handled by runtime:
rateLimitdecisions- behavior signals (
route-not-found,spike.401,spike.404,spike.429,burst,stuffing) - rule/block/ban/admin-cidr/cheap-signature alerts
Example (scope: 'all' for 404 scans / route-not-found floods):
rateLimitReport: {
enabled: true,
scope: 'all',
period: '30m',
suppressImmediate: true,
}period format
- Supports seconds as number or numeric string:
30,'30' - Supports duration strings:
30m(30 minutes)1h/10h(hours)1d(day)
- Invalid values fallback to
30m
Memory control (maxGroups)
maxGroupslimits how many unique grouped rows are stored in memory during one report window.- When limit is reached, oldest groups are evicted first (FIFO) and new groups are accepted.
- Summary message includes eviction counters when this happens.
- Report rows are cleared after each summary flush (
period) and on shutdown flush, so data does not accumulate across windows. - Memory usage for this feature is bounded by
maxGroups(group count), not by total request volume. - Repeated events for the same group only increment
count(they do not create new rows).
Example behavior:
maxGroups: 2000- first 2000 unique groups are stored in the current window
- group #2001 arrives -> oldest stored group is removed, new group is added
- at next summary flush, all current rows are sent/cleared and a new window starts
IP intelligence in reports (ipIntel)
By default, ipIntel can work with a built-in resolver (no custom function needed).
It is used when:
ipIntel.enabled = trueipIntel.resolveris not provided- default provider key exists in env
Built-in resolver env:
IP_INTEL_TOKEN(built-in resolver key)
Built-in provider is IPinfo.
If you want another provider, use custom resolver.
If you want full control over fields and provider logic, use custom resolver:
IpsModule.forRoot({
alerts: {
slack: { webhookUrl: process.env.SLACK_WEBHOOK_URL! },
rateLimitReport: {
enabled: true,
scope: 'all',
period: '30m',
suppressImmediate: true,
ipIntel: {
enabled: true,
resolver: async (ip, context) => {
// Example only: plug in your provider/client here
// and map response to nest-ips format.
// `context?.signal` can be used to cancel outgoing request on timeout.
return {
provider: 'my-ip-intel',
isVpn: false,
isProxy: false,
isTor: false,
isHosting: false,
riskScore: 7,
countryCode: 'US',
countryName: 'United States',
region: 'Virginia',
city: 'Ashburn',
asn: 'AS14618',
org: 'Amazon.com, Inc.',
isp: 'Amazon',
connectionType: 'hosting',
};
},
},
},
},
});Notes:
ipIntelis optional and disabled unless configured.- Reliable VPN/proxy detection requires external IP intelligence data (provider or local DB).
- Resolver calls are cached in memory (
cacheTtlSec,maxCacheSize) and timeout-protected (timeoutMs). - If
ipIntel.enabled=trueandresolveris omitted, package tries built-in default resolver via env credentials. - Default values (can be omitted):
timeoutMs=1500,cacheTtlSec=3600,maxCacheSize=5000. - Built-in resolver safety: provider response size is limited to
512KB.
Resolver return shape (IpsIpIntelResult) supports:
providerisVpnisProxyisTorisHostingriskScorecountryCodecountryNameregioncityasnorgispconnectionType
Security Notes (Direct Access, Spoofing)
- If direct internet access to app is possible, do not trust forwarded headers from unknown sources.
- In strict mode, configure trusted proxy CIDRs or trust function correctly.
- Wrong proxy trust setup can lead to wrong client attribution and weaker bans.
- In multi-worker production, prefer Redis store for consistent behavior.
Configuration Reference
- Minimal/default config:
IpsModule.forRoot({}) - Full options and defaults: see sections below (
npm Usage Guide,Top-level options reference,Client IP Trust Model,Spikes And Actions,Alert Templates).
Funding
Support development:
- BuyMeACoffee: https://buymeacoffee.com/vladyslavkhyrsa
- GitHub Sponsors: https://github.com/sponsors/HirsaVladislav
npm Usage Guide
Risks and mandatory parameters (read first)
Security risks if misconfigured:
- If proxy trust is wrong, attacker IP attribution can be wrong.
- If app is accessible directly and headers are trusted incorrectly, IP spoofing risk increases.
- If Redis is not used in multi-worker deployment, each worker keeps separate memory state.
- If alert channel is configured without destination (
webhookUrlor SMTP), module throws at startup.
Mandatory parameters by scenario:
- Basic IPS: no mandatory fields in
forRoot, but required pipeline registration (module + providers + middleware). - Trusted proxy headers (
strict): configure trusted source (trustedProxyCidrsorisTrustedProxy). - Fixed hops mode: set
clientIp.hops. - Redis strict mode: set
store.redis.url(orREDIS_URL). - Slack alerts: set
alerts.slack.webhookUrl. - Email alerts: set full
alerts.email.smtpobject.
Required integration steps (must-have)
- Add
IpsModule.forRoot(...)inimports. - Register global providers:
APP_GUARD -> IpsGuardAPP_INTERCEPTOR -> IpsInterceptorAPP_FILTER -> IpsNotFoundFilter
- Apply middleware:
createIpsMiddleware()for routes.
Without these 3 steps, full IPS pipeline will not work.
Example 1: Minimal setup (safe defaults)
Required fields:
- none in
forRoot(all values have defaults).
Optional fields:
- everything in
IpsModuleOptions.
Defaults used:
mode: 'IPS'logging: trueclientIp.mode: 'strict'clientIp.headersPriority: ['cf-connecting-ip', 'true-client-ip', 'fastly-client-ip', 'x-forwarded-for', 'forwarded', 'x-real-ip']clientIp.hops: 1clientIp.denyPrivateIpsFromHeaders: truestore.type: 'memory'store.maxBytes: 500MBscoreThreshold: 100cheapSignatures.enabled: truenotFound.windowSec: 60notFound.max: 30
IpsModule.forRoot({});Example 2: Strict proxy trust (recommended for internet-facing apps)
Required fields for trusted headers:
clientIp.mode: 'strict'- at least one of:
clientIp.trustedProxyCidrsclientIp.isTrustedProxy
Optional fields:
clientIp.headersPriorityclientIp.denyPrivateIpsFromHeaders
Defaults:
headersPrioritydefault list (see above)denyPrivateIpsFromHeaders: true
IpsModule.forRoot({
clientIp: {
mode: 'strict',
trustedProxyCidrs: ['10.0.0.0/8'],
},
});Example 3: Hops mode (fixed proxy chain)
Required fields:
clientIp.mode: 'hops'clientIp.hops
Optional fields:
clientIp.headersPriorityclientIp.denyPrivateIpsFromHeaders
Defaults:
hops: 1(if omitted)denyPrivateIpsFromHeaders: true
IpsModule.forRoot({
clientIp: {
mode: 'hops',
hops: 2,
headersPriority: ['x-forwarded-for', 'forwarded'],
},
});Example 4: Redis store (shared state between workers)
Required fields:
- for
store.type: 'redis':store.redis.urlorREDIS_URLenv var.
Optional fields:
store.redis.keyPrefixstore.redis.connectTimeoutMsstore.redis.connectionRetriesstore.redis.retryDelayMs
Defaults:
store.type: 'memory'connectTimeoutMs: 5000connectionRetries: 10retryDelayMs: 300- in
automode, fallback to memory on Redis connection failure.
IpsModule.forRoot({
store: {
type: 'auto',
redis: {
url: process.env.REDIS_URL,
},
},
});Example 5: Alerts (Slack / Email)
Required fields:
- Slack channel:
alerts.slack.webhookUrl
- Email channel:
alerts.email.smtp.hostalerts.email.smtp.portalerts.email.smtp.useralerts.email.smtp.passalerts.email.smtp.fromalerts.email.smtp.to[]
Optional fields:
- templates/fields/throttle settings.
Defaults:
- Slack
throttleSec: 120 - Email
throttleSec: 300 - Email
secure: port === 465(inside transporter setup)
IpsModule.forRoot({
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
},
email: {
smtp: {
host: process.env.SMTP_HOST!,
port: 587,
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
from: '[email protected]',
to: ['[email protected]'],
},
},
},
});Example 6: Periodic rate-limit summary reports
Required fields:
- none (feature is disabled unless
alerts.rateLimitReportis configured andenabledis not set tofalse).
Optional fields:
alerts.rateLimitReport.enabledalerts.rateLimitReport.scopealerts.rateLimitReport.periodalerts.rateLimitReport.suppressImmediatealerts.rateLimitReport.maxItemsalerts.rateLimitReport.maxGroups
Defaults (when enabled):
enabled: true(whenalerts.rateLimitReportobject exists)scope: 'rateLimit'period: 1800seconds (30m)suppressImmediate: truemaxItems: 50maxGroups: 2000
IpsModule.forRoot({
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
},
rateLimitReport: {
enabled: true,
scope: 'rateLimit',
period: '30m',
suppressImmediate: true,
maxItems: 50,
maxGroups: 2000,
},
},
});Example 7: Spikes and profile behavior
Required fields:
- none (behavior has defaults per profile).
Optional fields:
profiles.<name>.behavior.{windowSec,max401,max404,max429,maxReq,maxUniqueUsernames}
Defaults (profile-dependent):
default.behavior:windowSec=60,max401=20,max404=30,max429=20,maxReq=300,maxUniqueUsernames=20public.behavior:60,30,40,30,400,40login.behavior:120,10,20,10,120,10admin.behavior:60,8,10,8,80,5
IpsModule.forRoot({
profiles: {
login: {
behavior: { windowSec: 120, max401: 8, max429: 8 },
},
},
});Top-level options reference
| Field | Required | Default |
|---|---|---|
| mode | no | 'IPS' |
| clientIp | no | strict defaults |
| logging | no | true |
| logger | no | internal logger |
| store | no | memory store |
| memoryCapBytes | no | 500 * 1024 * 1024 |
| rules | no | undefined |
| profiles | no | built-in defaults |
| alerts | no | disabled unless configured |
| privacy | no | standard include list |
| scoreThreshold | no | 100 |
| cheapSignatures | no | enabled + built-in patterns |
| notFound | no | { windowSec: 60, max: 30 } |
Nest Integration
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import {
IpsModule,
IpsGuard,
IpsInterceptor,
IpsNotFoundFilter,
createIpsMiddleware,
} from '@nestjs-guardian/nest-ips';
@Module({
imports: [
IpsModule.forRoot({
mode: 'IPS',
logging: true,
// logger: yourCustomLogger, // optional, should implement LoggerPort
clientIp: {
mode: 'strict',
trustedProxyCidrs: ['10.0.0.0/8'],
headersPriority: [
'cf-connecting-ip',
'true-client-ip',
'fastly-client-ip',
'x-forwarded-for',
'forwarded',
],
denyPrivateIpsFromHeaders: true,
},
store: {
type: 'memory',
maxBytes: 500 * 1024 * 1024,
},
profiles: {
default: {
rateLimit: { key: 'ip', windowSec: 60, max: 120 },
banTtlSec: 600,
},
login: {
rateLimit: { key: 'ip+id', windowSec: 120, max: 8 },
banTtlSec: 900,
behavior: { max401: 10, max429: 10, windowSec: 120 },
},
admin: {
rateLimit: { key: 'ip', windowSec: 60, max: 30 },
allowCidrs: ['10.0.0.0/8'],
banTtlSec: 1800,
},
public: {
rateLimit: { key: 'ip', windowSec: 60, max: 300 },
banTtlSec: 300,
},
},
rules: {
loadFrom: 'node_modules/@nestjs-guardian/nest-ips/rules/baseline.json',
},
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
throttleSec: 120,
template: '*{{actionUpper}}* ({{mode}})\nIP: {{ip}}\nPath: {{path}}\nMessage: {{message}}',
// payloadTemplate: { env: 'prod', service: 'your-service', title: '{{ruleId}}', error: '{{message}}' },
// payloadIncludeText: false,
},
email: {
smtp: {
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT || 587),
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
from: '[email protected]',
to: ['[email protected]'],
},
throttleSec: 300,
subjectTemplate: '[IPS][{{mode}}] {{actionUpper}} {{ip}}',
textTemplate:
'action={{action}} mode={{mode}}\nip={{ip}} method={{method}} path={{path}}\nrule={{ruleId}} severity={{severity}}\nmessage={{message}}\ncounts={{countsJson}}\nts={{tsIso}}',
},
// Optional periodic summary for repeated rate-limit alerts (429 spam reduction)
// rateLimitReport: {
// enabled: true, // optional if rateLimitReport object is present
// scope: 'all', // optional; aggregate all IPS alerts (not only rateLimit)
// period: '30m',
// suppressImmediate: true,
// maxItems: 50,
// maxGroups: 2000,
// },
},
}),
],
providers: [
{ provide: APP_GUARD, useClass: IpsGuard },
{ provide: APP_INTERCEPTOR, useClass: IpsInterceptor },
{ provide: APP_FILTER, useClass: IpsNotFoundFilter },
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(createIpsMiddleware()).forRoutes('*');
}
}Decorators
import { Controller, Get, Post } from '@nestjs/common';
import { IpsBypass, IpsProfile } from '@nestjs-guardian/nest-ips';
@Controller()
export class AppController {
@Get('/health')
@IpsBypass()
health() {
return 'ok';
}
@Post('/auth/login')
@IpsProfile('login')
login() {}
@Get('/admin/panel')
@IpsProfile('admin')
panel() {}
}@IpsBypass()
What problem it solves:
- Excludes a route/class from IPS guard/interceptor observation (for example
/health, probes, internal callbacks).
Example:
@IpsBypass()
@Get('/health')
health() {
return { ok: true };
}What it is for:
- Skips guard-level IPS checks and interceptor tracking for the route.
- For full route exclusion (including middleware), see
Excluding Routes From IPS Observationbelow.
@IpsProfile('default' | 'public' | 'login' | 'admin')
What problem it solves:
- Assigns a route/class to a specific IPS profile with its own limits and behavior thresholds.
Example:
@IpsProfile('login')
@Post('/auth/login')
login() {}What it is for:
- Use stricter anti-bruteforce/stuffing settings for auth routes.
- Use stronger controls for sensitive routes (for example
admin).
@IpsTags(...tags)
What problem it solves:
- Adds route/class tags into IPS request context for custom logic and future rule extensions.
Example:
@IpsTags('payments', 'public-api')
@Get('/payments/status')
status() {}What it is for:
- Group detections by feature/domain.
- Mark routes with domain-specific metadata (for custom integrations or future tag-based rules).
Current built-in behavior:
- Tags are attached to IPS request context by
IpsGuard. - Built-in rules/alerts/logs do not yet use tags directly.
Excluding Routes From IPS Observation (Example: /health)
For health-check routes (ALB/ELB, uptime probes), use both:
- middleware
exclude(...)to skipcreateIpsMiddleware() @IpsBypass()to skip IPS guard/interceptor checks
Why both are required:
exclude(...)disables only middleware checks (global rate-limit, cheap signatures, early block).@IpsBypass()disables IPS checks on handler/class level (guard/interceptor path).- Using only one of them can still leave the route counted or blocked at another IPS stage.
1) Exclude from IPS middleware
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { createIpsMiddleware } from '@nestjs-guardian/nest-ips';
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(createIpsMiddleware())
.exclude({ path: 'health', method: RequestMethod.GET })
.forRoutes('*');
}
}2) Add @IpsBypass() to the route
import { Controller, Get } from '@nestjs/common';
import { IpsBypass } from '@nestjs-guardian/nest-ips';
@Controller()
export class AppController {
@IpsBypass()
@Get('/health')
health() {
return { ok: true };
}
}Notes
- Check the exact path in
exclude(...)(including global prefix, e.g./api/health). - Keep
/healthlightweight (no heavy DB/external calls). - Protect public
/healthat ALB/WAF/security-group level rather than app-level IPS limits.
Client IP Trust Model
Client IP extraction is configured via clientIp.
strict mode:
- Reads client IP from headers only when request comes from trusted proxy.
- Trust can be defined by:
trustedProxyCidrsisTrustedProxy(remoteIp)
- If request is not from trusted proxy, headers are ignored.
clientIp: {
mode: 'strict',
trustedProxyCidrs: ['10.0.0.0/8', '192.168.0.0/16'],
headersPriority: ['cf-connecting-ip', 'x-forwarded-for', 'forwarded'],
denyPrivateIpsFromHeaders: true,
}Spikes And Actions
This package is a rule engine + behavior engine (not only a simple limiter).
Action semantics:
rate-limit: soft control, returns429 Too Many Requests.ban: hard control, blocks IP for TTL (403in IPS mode).spike: anomaly detector in a time window, can escalate toban/alert.slowdown: not implemented yet (planned), would delay response instead of blocking.
Where spikes are configured:
profiles.<name>.behaviorwindowSecmax401max404max429maxReqmaxUniqueUsernames
notFound(route-not-foundspike detector).
profiles: {
default: {
rateLimit: { key: 'ip', windowSec: 60, max: 120 },
behavior: {
windowSec: 60,
max401: 20,
max404: 30,
max429: 20,
maxReq: 300,
maxUniqueUsernames: 20,
},
banTtlSec: 600,
},
},
notFound: {
windowSec: 60,
max: 30,
}Canonical spike scenarios:
spike.401(ip, windowSec, max)-> configured bybehavior.max401.spike.404(ip, windowSec, max)-> configured bybehavior.max404.spike.429(ip, windowSec, max)-> configured bybehavior.max429.spike.path("/admin", ...)-> use rulematch.path+action.spike.ua(pattern, ...)-> use rulematch.ua.regex.spike.method("TRACE", ...)-> use rulewhen.methods.
Rules examples:
[
{
"id": "spike.path.admin",
"severity": "high",
"when": { "path": { "prefix": "/admin" } },
"match": [{ "field": "path", "contains": "/admin" }],
"action": "alert"
},
{
"id": "spike.ua.scanner",
"severity": "medium",
"match": [{ "field": "ua", "regex": "(sqlmap|nikto|nmap|gobuster)" }],
"action": "alert"
},
{
"id": "spike.method.trace",
"severity": "high",
"when": { "methods": ["TRACE"] },
"match": [{ "field": "path", "regex": ".*" }],
"action": "block",
"block": { "status": 403, "message": "Forbidden" }
}
]hops mode:
- Uses proxy hop count (
hops) to select client IP from forwarding chain. - Useful when app is always behind fixed number of proxies.
clientIp: {
mode: 'hops',
hops: 2,
headersPriority: ['x-forwarded-for', 'forwarded'],
denyPrivateIpsFromHeaders: true,
}Public API
Exports:
IpsModuleIpsGuard,IpsInterceptor,IpsNotFoundFilter,createIpsMiddleware()@IpsProfile(),@IpsBypass(),@IpsTags()- Types:
IpsModuleOptions,IpsClientIpOptions,IpsResolvedClientIpOptions,Rule,AlertEvent,AlertTemplateField,AlertIncludeField,Store,Alerter,LoggerPort
Alert Templates
You can customize Slack and email content via templates:
alerts.slack.templatealerts.slack.fieldsalerts.slack.payloadTemplatealerts.slack.payloadIncludeTextalerts.email.subjectTemplatealerts.email.textTemplatealerts.email.fields
Enable behavior:
- If
enabledis explicitly set, that value is used. - If
enabledis omitted and channel config exists, channel is treated as enabled. - If
alerts.slackis enabled/configured withoutwebhookUrl, module throws at startup. - If
alerts.emailis enabled/configured without fullsmtpconfig, module throws at startup.
Template placeholders:
{{ts}},{{tsIso}}{{mode}},{{action}},{{actionUpper}}{{ip}},{{method}},{{path}},{{ua}}{{profile}},{{ruleId}},{{severity}}{{counts}},{{countsJson}}{{message}}
If template is not provided, channel uses fields list (empty fields are skipped).
Slack formats
1) Plain text template
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
template: '*{{actionUpper}}* ({{mode}})\nIP: {{ip}}\nPath: {{path}}\nMessage: {{message}}',
},
}2) Auto text from selected fields
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
fields: ['actionUpper', 'mode', 'ip', 'path', 'ruleId', 'severity', 'message'],
},
}3) JSON payload template (for Slack Workflow fields like env/service/title/error)
alerts: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
payloadTemplate: {
env: 'prod',
service: 'your-service',
title: '[{{mode}}] {{actionUpper}} {{ruleId}}',
error: '{{message}}',
ip: '{{ip}}',
path: '{{path}}',
},
payloadIncludeText: false, // true by default
},
}Memory Cap
MemoryStore enforces a hard cap (maxBytes) using:
- TTL eviction
- LRU eviction
- pressure mode (TTL shortening + set trimming)
- high-priority ban keys (evicted last)
This cap is for IPS stored data only. Total Node.js process memory can be higher.
Redis Store
Use built-in Redis store (based on node-redis) to share counters/bans across workers:
IpsModule.forRoot({
store: {
type: 'auto', // 'redis' to require Redis, 'auto' to fallback to memory
redis: {
url: process.env.REDIS_URL,
keyPrefix: 'ips:',
connectTimeoutMs: 5000,
connectionRetries: 10, // default: 10
retryDelayMs: 300, // default: 300ms
},
},
});store.type = 'redis' requires redis.url (or REDIS_URL) and fails startup if unavailable.
Planned Tasks
Next tasks:
- [ ] Prometheus metrics for limits, bans, and rule hits.
- [ ] Canary mode for new rules (log-only before enforce).
- [ ] Slowdown action (response delay without blocking).
- [ ] Security event stream output (structured JSON + optional webhook).
- [ ] Cluster/worker stress tests for shared Redis state.
- [ ] Redis outage tests for
autofallback and strictredismode.
