import { describe, expect, it } from 'vitest' import { isLanIPv4, isLanAddress, parsePrinterPorts, checkPrintEnvelope, checkPrintAddress, DEFAULT_PRINTER_PORTS, MAX_PRINT_BYTES, } from '../src/print-guard' import { createRateLimiter } from '../src/rate-limit' import { isSessionExpired, DEFAULT_SESSION_TTL } from '../src/session-policy' /** * Pure guards behind the P1 edge hardening (red-team H1, M2, M4): the LAN/port/byte print * validator, the sliding-window rate limiter, and the session-TTL check. Clock-injectable, so * they test without timers or sockets. The HTTP-surface behaviour lives in edge.test.ts. */ // ---- H1: /api/print SSRF allowlist ---------------------------------------------------------- describe('print-guard: LAN address classification (H1)', () => { it('accepts loopback + RFC-1918 private ranges', () => { for (const ip of ['127.0.0.1', '127.255.255.254', '10.0.0.5', '172.16.0.1', '172.31.255.255', '192.168.1.100']) { expect(isLanIPv4(ip)).toBe(true) } }) it('rejects public IPs, link-local, and the 172.16/12 edges outside the block', () => { for (const ip of ['8.8.8.8', '1.1.1.1', '169.254.1.1', '172.15.255.255', '172.32.0.1', '0.0.0.0', '255.255.255.255', 'not-an-ip', '']) { expect(isLanIPv4(ip)).toBe(false) } }) it('treats IPv6 loopback ::1 as LAN, other IPv6 as off-LAN', () => { expect(isLanAddress('::1')).toBe(true) expect(isLanAddress('2001:4860:4860::8888')).toBe(false) }) }) describe('print-guard: port allowlist + payload cap (H1)', () => { it('defaults to {9100,9101,9102} and parses SIMS_PRINTER_PORTS', () => { expect([...parsePrinterPorts(undefined)].sort()).toEqual([...DEFAULT_PRINTER_PORTS]) expect([...parsePrinterPorts('')].sort()).toEqual([...DEFAULT_PRINTER_PORTS]) expect([...parsePrinterPorts('abc,,-1,70000')].sort()).toEqual([...DEFAULT_PRINTER_PORTS]) // all invalid → default expect([...parsePrinterPorts('9100, 9200')].sort()).toEqual([9100, 9200]) }) it('checkPrintEnvelope enforces byte cap and port allowlist', () => { const ports = new Set(DEFAULT_PRINTER_PORTS) expect(checkPrintEnvelope(100, 9100, ports)).toEqual({ ok: true }) expect(checkPrintEnvelope(MAX_PRINT_BYTES + 1, 9100, ports)).toMatchObject({ ok: false, code: 'E-2201' }) expect(checkPrintEnvelope(100, 80, ports)).toMatchObject({ ok: false, code: 'E-2202' }) }) it('checkPrintAddress rejects a public IP, accepts a private one', () => { expect(checkPrintAddress('127.0.0.1')).toEqual({ ok: true }) expect(checkPrintAddress('8.8.8.8')).toMatchObject({ ok: false, code: 'E-2203' }) }) }) // ---- Rate limiter (M2/M4) ------------------------------------------------------------------- describe('rate limiter: sliding window (M2/M4)', () => { it('allows up to max within the window then rejects, and slides', () => { const rl = createRateLimiter({ windowMs: 1000, max: 2 }) expect(rl.hit('ip', 0)).toBe(true) expect(rl.hit('ip', 100)).toBe(true) expect(rl.hit('ip', 200)).toBe(false) // 3rd within window blocked expect(rl.remaining('ip', 200)).toBe(0) expect(rl.hit('ip', 1101)).toBe(true) // first attempt (t=0) aged out → allowed again }) it('keys are independent', () => { const rl = createRateLimiter({ windowMs: 1000, max: 1 }) expect(rl.hit('a', 0)).toBe(true) expect(rl.hit('b', 0)).toBe(true) expect(rl.hit('a', 0)).toBe(false) }) }) // ---- Session TTL (M4) ----------------------------------------------------------------------- describe('session TTL check (M4)', () => { const ttl = { absoluteMs: 1000, idleMs: 100 } it('is live within both bounds', () => { expect(isSessionExpired({ createdMs: 0, lastSeenMs: 0 }, 50, ttl)).toBe(false) }) it('expires past the idle timeout even with recent creation', () => { expect(isSessionExpired({ createdMs: 0, lastSeenMs: 0 }, 150, ttl)).toBe(true) }) it('expires past the absolute lifetime even when recently seen', () => { expect(isSessionExpired({ createdMs: 0, lastSeenMs: 990 }, 1001, ttl)).toBe(true) }) it('ships sane defaults (12h absolute / 30m idle)', () => { expect(DEFAULT_SESSION_TTL.absoluteMs).toBe(12 * 60 * 60 * 1000) expect(DEFAULT_SESSION_TTL.idleMs).toBe(30 * 60 * 1000) }) })