npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nestjs-guardian/nest-ips

v1.1.2

Published

Application-level IDS/IPS module for NestJS

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 rateLimit alerts 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-ips

Quick 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.
  • headersPriority controls header parsing order.
  • denyPrivateIpsFromHeaders can reject private/local forwarded IPs.
  • See Client IP Trust Model below 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.smtp config.
  • Optional: alerts.rateLimitReport aggregates repeated rateLimit events (or all IPS alert events with scope: '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 scope are still sent normally.

Notes:

  • If alerts.rateLimitReport is omitted, this feature is disabled (default behavior remains unchanged).
  • If alerts.rateLimitReport is provided and enabled is omitted, summary reporting is enabled by default.
  • Summary sending requires at least one configured alert channel (alerts.slack and/or alerts.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
  • 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), set scope: 'all'.

Aggregation scope (scope)

  • scope: 'rateLimit' (default)
    • aggregates only rateLimit decisions
  • scope: 'all'
    • aggregates all alert-producing IPS events handled by runtime:
    • rateLimit decisions
    • 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)

  • maxGroups limits 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 = true
  • ipIntel.resolver is 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:

  • ipIntel is 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=true and resolver is 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:

  • provider
  • isVpn
  • isProxy
  • isTor
  • isHosting
  • riskScore
  • countryCode
  • countryName
  • region
  • city
  • asn
  • org
  • isp
  • connectionType

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 (webhookUrl or 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 (trustedProxyCidrs or isTrustedProxy).
  • Fixed hops mode: set clientIp.hops.
  • Redis strict mode: set store.redis.url (or REDIS_URL).
  • Slack alerts: set alerts.slack.webhookUrl.
  • Email alerts: set full alerts.email.smtp object.

Required integration steps (must-have)

  1. Add IpsModule.forRoot(...) in imports.
  2. Register global providers:
    • APP_GUARD -> IpsGuard
    • APP_INTERCEPTOR -> IpsInterceptor
    • APP_FILTER -> IpsNotFoundFilter
  3. 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: true
  • clientIp.mode: 'strict'
  • clientIp.headersPriority: ['cf-connecting-ip', 'true-client-ip', 'fastly-client-ip', 'x-forwarded-for', 'forwarded', 'x-real-ip']
  • clientIp.hops: 1
  • clientIp.denyPrivateIpsFromHeaders: true
  • store.type: 'memory'
  • store.maxBytes: 500MB
  • scoreThreshold: 100
  • cheapSignatures.enabled: true
  • notFound.windowSec: 60
  • notFound.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.trustedProxyCidrs
    • clientIp.isTrustedProxy

Optional fields:

  • clientIp.headersPriority
  • clientIp.denyPrivateIpsFromHeaders

Defaults:

  • headersPriority default 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.headersPriority
  • clientIp.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.url or REDIS_URL env var.

Optional fields:

  • store.redis.keyPrefix
  • store.redis.connectTimeoutMs
  • store.redis.connectionRetries
  • store.redis.retryDelayMs

Defaults:

  • store.type: 'memory'
  • connectTimeoutMs: 5000
  • connectionRetries: 10
  • retryDelayMs: 300
  • in auto mode, 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.host
    • alerts.email.smtp.port
    • alerts.email.smtp.user
    • alerts.email.smtp.pass
    • alerts.email.smtp.from
    • alerts.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.rateLimitReport is configured and enabled is not set to false).

Optional fields:

  • alerts.rateLimitReport.enabled
  • alerts.rateLimitReport.scope
  • alerts.rateLimitReport.period
  • alerts.rateLimitReport.suppressImmediate
  • alerts.rateLimitReport.maxItems
  • alerts.rateLimitReport.maxGroups

Defaults (when enabled):

  • enabled: true (when alerts.rateLimitReport object exists)
  • scope: 'rateLimit'
  • period: 1800 seconds (30m)
  • suppressImmediate: true
  • maxItems: 50
  • maxGroups: 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=20
  • public.behavior: 60,30,40,30,400,40
  • login.behavior: 120,10,20,10,120,10
  • admin.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 Observation below.

@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:

  1. middleware exclude(...) to skip createIpsMiddleware()
  2. @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 /health lightweight (no heavy DB/external calls).
  • Protect public /health at 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:
    • trustedProxyCidrs
    • isTrustedProxy(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, returns 429 Too Many Requests.
  • ban: hard control, blocks IP for TTL (403 in IPS mode).
  • spike: anomaly detector in a time window, can escalate to ban/alert.
  • slowdown: not implemented yet (planned), would delay response instead of blocking.

Where spikes are configured:

  • profiles.<name>.behavior
    • windowSec
    • max401
    • max404
    • max429
    • maxReq
    • maxUniqueUsernames
  • notFound (route-not-found spike 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 by behavior.max401.
  • spike.404(ip, windowSec, max) -> configured by behavior.max404.
  • spike.429(ip, windowSec, max) -> configured by behavior.max429.
  • spike.path("/admin", ...) -> use rule match.path + action.
  • spike.ua(pattern, ...) -> use rule match.ua.regex.
  • spike.method("TRACE", ...) -> use rule when.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:

  • IpsModule
  • IpsGuard, 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.template
  • alerts.slack.fields
  • alerts.slack.payloadTemplate
  • alerts.slack.payloadIncludeText
  • alerts.email.subjectTemplate
  • alerts.email.textTemplate
  • alerts.email.fields

Enable behavior:

  • If enabled is explicitly set, that value is used.
  • If enabled is omitted and channel config exists, channel is treated as enabled.
  • If alerts.slack is enabled/configured without webhookUrl, module throws at startup.
  • If alerts.email is enabled/configured without full smtp config, 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 auto fallback and strict redis mode.