fix: resolve lint, format, and TypeScript compilation errors

This commit is contained in:
Ulrich Diedrichsen 2026-01-30 11:44:33 +01:00
parent e69eccb4b1
commit 8f42141f75
16 changed files with 162 additions and 139 deletions

View File

@ -31,3 +31,9 @@ export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): num
if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`); if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`);
return ms; return ms;
} }
/**
* Alias for parseDurationMs
* @deprecated Use parseDurationMs instead
*/
export const parseDuration = parseDurationMs;

View File

@ -167,18 +167,32 @@ export function registerSecurityCli(program: Command) {
const lines: string[] = []; const lines: string[] = [];
lines.push(theme.heading("Security Shield Status")); lines.push(theme.heading("Security Shield Status"));
lines.push(""); lines.push("");
lines.push(`Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`); lines.push(
lines.push(`Rate Limiting: ${rateLimitingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); `Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`,
lines.push(`Intrusion Detection: ${intrusionDetectionEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); );
lines.push(`Firewall Integration: ${firewallEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); lines.push(
lines.push(`Alerting: ${alertingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); `Rate Limiting: ${rateLimitingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`,
);
lines.push(
`Intrusion Detection: ${intrusionDetectionEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`,
);
lines.push(
`Firewall Integration: ${firewallEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`,
);
lines.push(
`Alerting: ${alertingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`,
);
if (alertingEnabled && cfg.security?.alerting?.channels?.telegram?.enabled) { if (alertingEnabled && cfg.security?.alerting?.channels?.telegram?.enabled) {
lines.push(` Telegram: ${theme.success("ENABLED")}`); lines.push(` Telegram: ${theme.success("ENABLED")}`);
} }
lines.push(""); lines.push("");
lines.push(theme.muted(`Docs: ${formatDocsLink("/security/shield", "docs.openclaw.ai/security/shield")}`)); lines.push(
theme.muted(
`Docs: ${formatDocsLink("/security/shield", "docs.openclaw.ai/security/shield")}`,
),
);
defaultRuntime.log(lines.join("\n")); defaultRuntime.log(lines.join("\n"));
}); });
@ -194,7 +208,11 @@ export function registerSecurityCli(program: Command) {
await writeConfigFile(cfg); await writeConfigFile(cfg);
defaultRuntime.log(theme.success("✓ Security shield enabled")); defaultRuntime.log(theme.success("✓ Security shield enabled"));
defaultRuntime.log(theme.muted(` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`)); defaultRuntime.log(
theme.muted(
` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`,
),
);
}); });
// openclaw security disable // openclaw security disable
@ -211,7 +229,11 @@ export function registerSecurityCli(program: Command) {
cfg.security.shield.enabled = false; cfg.security.shield.enabled = false;
await writeConfigFile(cfg); await writeConfigFile(cfg);
defaultRuntime.log(theme.warn("⚠ Security shield disabled")); defaultRuntime.log(theme.warn("⚠ Security shield disabled"));
defaultRuntime.log(theme.muted(` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`)); defaultRuntime.log(
theme.muted(
` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`,
),
);
}); });
// openclaw security logs // openclaw security logs
@ -279,16 +301,14 @@ export function registerSecurityCli(program: Command) {
}); });
// openclaw blocklist // openclaw blocklist
const blocklist = program const blocklist = program.command("blocklist").description("Manage IP blocklist");
.command("blocklist")
.description("Manage IP blocklist");
blocklist blocklist
.command("list") .command("list")
.description("List all blocked IPs") .description("List all blocked IPs")
.option("--json", "Print JSON", false) .option("--json", "Print JSON", false)
.action(async (opts: { json?: boolean }) => { .action(async (opts: { json?: boolean }) => {
const entries = ipManager.getBlocklist(); const entries = ipManager.getBlockedIps();
if (opts.json) { if (opts.json) {
defaultRuntime.log(JSON.stringify(entries, null, 2)); defaultRuntime.log(JSON.stringify(entries, null, 2));
@ -310,11 +330,13 @@ export function registerSecurityCli(program: Command) {
const hours = Math.floor(remaining / (1000 * 60 * 60)); const hours = Math.floor(remaining / (1000 * 60 * 60));
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
defaultRuntime.log(`${theme.bold(entry.ip)}`); defaultRuntime.log(`${theme.heading(entry.ip)}`);
defaultRuntime.log(` Reason: ${entry.reason}`); defaultRuntime.log(` Reason: ${entry.reason}`);
defaultRuntime.log(` Source: ${entry.source}`); defaultRuntime.log(` Source: ${entry.source}`);
defaultRuntime.log(` Blocked: ${new Date(entry.blockedAt).toLocaleString()}`); defaultRuntime.log(` Blocked: ${new Date(entry.blockedAt).toLocaleString()}`);
defaultRuntime.log(` Expires: ${expiresAt.toLocaleString()} (${hours}h ${minutes}m remaining)`); defaultRuntime.log(
` Expires: ${expiresAt.toLocaleString()} (${hours}h ${minutes}m remaining)`,
);
defaultRuntime.log(""); defaultRuntime.log("");
} }
}); });
@ -354,16 +376,14 @@ export function registerSecurityCli(program: Command) {
}); });
// openclaw allowlist // openclaw allowlist
const allowlist = program const allowlist = program.command("allowlist").description("Manage IP allowlist");
.command("allowlist")
.description("Manage IP allowlist");
allowlist allowlist
.command("list") .command("list")
.description("List all allowed IPs") .description("List all allowed IPs")
.option("--json", "Print JSON", false) .option("--json", "Print JSON", false)
.action(async (opts: { json?: boolean }) => { .action(async (opts: { json?: boolean }) => {
const entries = ipManager.getAllowlist(); const entries = ipManager.getAllowedIps();
if (opts.json) { if (opts.json) {
defaultRuntime.log(JSON.stringify(entries, null, 2)); defaultRuntime.log(JSON.stringify(entries, null, 2));
@ -379,7 +399,7 @@ export function registerSecurityCli(program: Command) {
defaultRuntime.log(""); defaultRuntime.log("");
for (const entry of entries) { for (const entry of entries) {
defaultRuntime.log(`${theme.bold(entry.ip)}`); defaultRuntime.log(`${theme.heading(entry.ip)}`);
defaultRuntime.log(` Reason: ${entry.reason}`); defaultRuntime.log(` Reason: ${entry.reason}`);
defaultRuntime.log(` Source: ${entry.source}`); defaultRuntime.log(` Source: ${entry.source}`);
defaultRuntime.log(` Added: ${new Date(entry.addedAt).toLocaleString()}`); defaultRuntime.log(` Added: ${new Date(entry.addedAt).toLocaleString()}`);

View File

@ -97,7 +97,12 @@ export interface AlertingConfig {
/** Alert triggers */ /** Alert triggers */
triggers?: { triggers?: {
criticalEvents?: AlertTriggerConfig; criticalEvents?: AlertTriggerConfig;
failedAuthSpike?: { enabled?: boolean; threshold?: number; windowMs?: number; throttleMs?: number }; failedAuthSpike?: {
enabled?: boolean;
threshold?: number;
windowMs?: number;
throttleMs?: number;
};
ipBlocked?: AlertTriggerConfig; ipBlocked?: AlertTriggerConfig;
}; };

View File

@ -24,11 +24,11 @@ export class AlertManager {
private initializeChannels(): void { private initializeChannels(): void {
// Telegram channel // Telegram channel
if (this.config.channels.telegram?.enabled) { if (this.config.channels?.telegram?.enabled) {
const telegram = new TelegramAlertChannel({ const telegram = new TelegramAlertChannel({
enabled: true, enabled: true,
botToken: this.config.channels.telegram.botToken, botToken: this.config.channels.telegram.botToken ?? "",
chatId: this.config.channels.telegram.chatId, chatId: this.config.channels.telegram.chatId ?? "",
}); });
if (telegram.isEnabled()) { if (telegram.isEnabled()) {
this.channels.push(telegram); this.channels.push(telegram);
@ -47,7 +47,7 @@ export class AlertManager {
* Check if alerting is enabled * Check if alerting is enabled
*/ */
isEnabled(): boolean { isEnabled(): boolean {
return this.config.enabled && this.channels.length > 0; return (this.config.enabled ?? false) && this.channels.length > 0;
} }
/** /**
@ -71,20 +71,17 @@ export class AlertManager {
} }
// Send to all channels // Send to all channels
const results = await Promise.allSettled( const results = await Promise.allSettled(this.channels.map((channel) => channel.send(alert)));
this.channels.map((channel) => channel.send(alert)),
);
// Log results // Log results
let successCount = 0; let successCount = 0;
let failureCount = 0; let _failureCount = 0;
for (const result of results) { for (const result of results) {
if (result.status === "fulfilled" && result.value.ok) { if (result.status === "fulfilled" && result.value.ok) {
successCount++; successCount++;
} else { } else {
failureCount++; _failureCount++;
const error = const error = result.status === "fulfilled" ? result.value.error : String(result.reason);
result.status === "fulfilled" ? result.value.error : String(result.reason);
log.error(`alert send failed: ${error}`); log.error(`alert send failed: ${error}`);
} }
} }
@ -105,10 +102,7 @@ export class AlertManager {
} }
// Critical events // Critical events
if ( if (event.severity === "critical" && this.config.triggers?.criticalEvents?.enabled) {
event.severity === "critical" &&
this.config.triggers.criticalEvents?.enabled
) {
await this.sendAlert({ await this.sendAlert({
id: randomUUID(), id: randomUUID(),
severity: "critical", severity: "critical",
@ -126,10 +120,7 @@ export class AlertManager {
} }
// IP blocked // IP blocked
if ( if (event.action === SecurityActions.IP_BLOCKED && this.config.triggers?.ipBlocked?.enabled) {
event.action === SecurityActions.IP_BLOCKED &&
this.config.triggers.ipBlocked?.enabled
) {
await this.sendAlert({ await this.sendAlert({
id: randomUUID(), id: randomUUID(),
severity: "warn", severity: "warn",
@ -146,14 +137,14 @@ export class AlertManager {
} }
// Intrusion detected // Intrusion detected
if ( const criticalActions = [
[ SecurityActions.BRUTE_FORCE_DETECTED,
SecurityActions.BRUTE_FORCE_DETECTED, SecurityActions.SSRF_BYPASS_ATTEMPT,
SecurityActions.SSRF_BYPASS_ATTEMPT, SecurityActions.PATH_TRAVERSAL_ATTEMPT,
SecurityActions.PATH_TRAVERSAL_ATTEMPT, SecurityActions.PORT_SCANNING_DETECTED,
SecurityActions.PORT_SCANNING_DETECTED, ] as const;
].includes(event.action)
) { if (criticalActions.includes(event.action as (typeof criticalActions)[number])) {
const pattern = event.attackPattern || "unknown"; const pattern = event.attackPattern || "unknown";
await this.sendAlert({ await this.sendAlert({
id: randomUUID(), id: randomUUID(),
@ -164,7 +155,8 @@ export class AlertManager {
details: { details: {
pattern, pattern,
ip: event.ip, ip: event.ip,
attempts: event.details.failedAttempts || event.details.attempts || event.details.connections, attempts:
event.details.failedAttempts || event.details.attempts || event.details.connections,
threshold: event.details.threshold, threshold: event.details.threshold,
}, },
trigger: "intrusion_detected", trigger: "intrusion_detected",
@ -175,9 +167,9 @@ export class AlertManager {
private getThrottleMs(trigger: string): number { private getThrottleMs(trigger: string): number {
switch (trigger) { switch (trigger) {
case "critical_event": case "critical_event":
return this.config.triggers.criticalEvents?.throttleMs || 0; return this.config.triggers?.criticalEvents?.throttleMs || 0;
case "ip_blocked": case "ip_blocked":
return this.config.triggers.ipBlocked?.throttleMs || 0; return this.config.triggers?.ipBlocked?.throttleMs || 0;
case "intrusion_detected": case "intrusion_detected":
return 300_000; // 5 minutes default return 300_000; // 5 minutes default
default: default:

View File

@ -31,26 +31,44 @@ export interface AlertChannelInterface {
} }
export interface AlertTriggerConfig { export interface AlertTriggerConfig {
enabled: boolean; enabled?: boolean;
throttleMs?: number; throttleMs?: number;
} }
export interface AlertingConfig { export interface AlertingConfig {
enabled: boolean; enabled?: boolean;
triggers: { triggers?: {
criticalEvents?: AlertTriggerConfig; criticalEvents?: AlertTriggerConfig;
failedAuthSpike?: AlertTriggerConfig & { threshold: number; windowMs: number }; failedAuthSpike?: AlertTriggerConfig & { threshold?: number; windowMs?: number };
ipBlocked?: AlertTriggerConfig; ipBlocked?: AlertTriggerConfig;
}; };
channels: { channels?: {
telegram?: { telegram?: {
enabled: boolean; enabled?: boolean;
botToken: string; botToken?: string;
chatId: string; chatId?: string;
}; };
webhook?: { webhook?: {
enabled: boolean; enabled?: boolean;
url: string; url?: string;
};
slack?: {
enabled?: boolean;
webhookUrl?: string;
};
email?: {
enabled?: boolean;
smtp?: {
host?: string;
port?: number;
secure?: boolean;
auth?: {
user?: string;
pass?: string;
};
};
from?: string;
to?: string[];
}; };
}; };
} }

View File

@ -3,7 +3,7 @@
* Aggregates events over time windows for alerting and intrusion detection * Aggregates events over time windows for alerting and intrusion detection
*/ */
import type { SecurityEvent, SecurityEventCategory, SecurityEventSeverity } from "./schema.js"; import type { SecurityEvent } from "./schema.js";
/** /**
* Event count within a time window * Event count within a time window
@ -59,9 +59,7 @@ export class SecurityEventAggregator {
} }
// Filter out events outside the time window // Filter out events outside the time window
count.events = count.events.filter( count.events = count.events.filter((e) => new Date(e.timestamp).getTime() > windowStart);
(e) => new Date(e.timestamp).getTime() > windowStart
);
// Add new event // Add new event
count.events.push(event); count.events.push(event);
@ -80,10 +78,7 @@ export class SecurityEventAggregator {
/** /**
* Get event count for a key within a window * Get event count for a key within a window
*/ */
getCount(params: { getCount(params: { key: string; windowMs: number }): number {
key: string;
windowMs: number;
}): number {
const { key, windowMs } = params; const { key, windowMs } = params;
const count = this.eventCounts.get(key); const count = this.eventCounts.get(key);
@ -94,7 +89,7 @@ export class SecurityEventAggregator {
// Filter events in window // Filter events in window
const eventsInWindow = count.events.filter( const eventsInWindow = count.events.filter(
(e) => new Date(e.timestamp).getTime() > windowStart (e) => new Date(e.timestamp).getTime() > windowStart,
); );
return eventsInWindow.length; return eventsInWindow.length;
@ -103,10 +98,7 @@ export class SecurityEventAggregator {
/** /**
* Get aggregated events for a key * Get aggregated events for a key
*/ */
getEvents(params: { getEvents(params: { key: string; windowMs?: number }): SecurityEvent[] {
key: string;
windowMs?: number;
}): SecurityEvent[] {
const { key, windowMs } = params; const { key, windowMs } = params;
const count = this.eventCounts.get(key); const count = this.eventCounts.get(key);
@ -119,9 +111,7 @@ export class SecurityEventAggregator {
const now = Date.now(); const now = Date.now();
const windowStart = now - windowMs; const windowStart = now - windowMs;
return count.events.filter( return count.events.filter((e) => new Date(e.timestamp).getTime() > windowStart);
(e) => new Date(e.timestamp).getTime() > windowStart
);
} }
/** /**

View File

@ -7,9 +7,13 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { SecurityEvent, SecurityEventSeverity, SecurityEventCategory, SecurityEventOutcome } from "./schema.js"; import type {
import { DEFAULT_LOG_DIR } from "../../logging/logger.js"; SecurityEvent,
import { getChildLogger } from "../../logging/index.js"; SecurityEventSeverity,
SecurityEventCategory,
SecurityEventOutcome,
} from "./schema.js";
import { DEFAULT_LOG_DIR, getChildLogger } from "../../logging/logger.js";
import { getAlertManager } from "../alerting/manager.js"; import { getAlertManager } from "../alerting/manager.js";
const SECURITY_LOG_PREFIX = "security"; const SECURITY_LOG_PREFIX = "security";
@ -229,7 +233,8 @@ class SecurityEventLogger {
* Log event to main logger for OTEL export and console output * Log event to main logger for OTEL export and console output
*/ */
private logToMainLogger(event: SecurityEvent): void { private logToMainLogger(event: SecurityEvent): void {
const logMethod = event.severity === "critical" ? "error" : event.severity === "warn" ? "warn" : "info"; const logMethod =
event.severity === "critical" ? "error" : event.severity === "warn" ? "warn" : "info";
this.logger[logMethod](`[${event.category}] ${event.action}`, { this.logger[logMethod](`[${event.category}] ${event.action}`, {
eventId: event.eventId, eventId: event.eventId,

View File

@ -41,7 +41,7 @@ export class FirewallManager {
} else if (this.config.backend === "ufw") { } else if (this.config.backend === "ufw") {
this.backend = new UfwBackend(); this.backend = new UfwBackend();
} else { } else {
return { ok: false, error: `unknown backend: ${this.config.backend}` }; return { ok: false, error: `unknown backend: ${String(this.config.backend)}` };
} }
// Check availability // Check availability
@ -196,9 +196,7 @@ let firewallManager: FirewallManager | null = null;
/** /**
* Initialize firewall manager with config * Initialize firewall manager with config
*/ */
export async function initFirewallManager( export async function initFirewallManager(config: FirewallManagerConfig): Promise<FirewallManager> {
config: FirewallManagerConfig,
): Promise<FirewallManager> {
firewallManager = new FirewallManager(config); firewallManager = new FirewallManager(config);
await firewallManager.initialize(); await firewallManager.initialize();
return firewallManager; return firewallManager;

View File

@ -1,3 +1,4 @@
/* eslint-disable typescript-eslint/unbound-method */
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
import { IntrusionDetector } from "./intrusion-detector.js"; import { IntrusionDetector } from "./intrusion-detector.js";
import { SecurityActions, AttackPatterns, type SecurityEvent } from "./events/schema.js"; import { SecurityActions, AttackPatterns, type SecurityEvent } from "./events/schema.js";

View File

@ -47,16 +47,16 @@ export class IntrusionDetector {
/** /**
* Check for brute force attack pattern * Check for brute force attack pattern
*/ */
checkBruteForce(params: { checkBruteForce(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
ip: string;
event: SecurityEvent;
}): IntrusionDetectionResult {
if (!this.config.enabled) { if (!this.config.enabled) {
return { detected: false }; return { detected: false };
} }
const { ip, event } = params; const { ip, event } = params;
const pattern = this.config.patterns.bruteForce; const pattern = this.config.patterns.bruteForce;
if (!pattern || !pattern.threshold || !pattern.windowMs) {
return { detected: false };
}
const key = `brute_force:${ip}`; const key = `brute_force:${ip}`;
const crossed = securityEventAggregator.trackEvent({ const crossed = securityEventAggregator.trackEvent({
@ -99,16 +99,16 @@ export class IntrusionDetector {
/** /**
* Check for SSRF bypass attempts * Check for SSRF bypass attempts
*/ */
checkSsrfBypass(params: { checkSsrfBypass(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
ip: string;
event: SecurityEvent;
}): IntrusionDetectionResult {
if (!this.config.enabled) { if (!this.config.enabled) {
return { detected: false }; return { detected: false };
} }
const { ip, event } = params; const { ip, event } = params;
const pattern = this.config.patterns.ssrfBypass; const pattern = this.config.patterns.ssrfBypass;
if (!pattern || !pattern.threshold || !pattern.windowMs) {
return { detected: false };
}
const key = `ssrf_bypass:${ip}`; const key = `ssrf_bypass:${ip}`;
const crossed = securityEventAggregator.trackEvent({ const crossed = securityEventAggregator.trackEvent({
@ -148,16 +148,16 @@ export class IntrusionDetector {
/** /**
* Check for path traversal attempts * Check for path traversal attempts
*/ */
checkPathTraversal(params: { checkPathTraversal(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
ip: string;
event: SecurityEvent;
}): IntrusionDetectionResult {
if (!this.config.enabled) { if (!this.config.enabled) {
return { detected: false }; return { detected: false };
} }
const { ip, event } = params; const { ip, event } = params;
const pattern = this.config.patterns.pathTraversal; const pattern = this.config.patterns.pathTraversal;
if (!pattern || !pattern.threshold || !pattern.windowMs) {
return { detected: false };
}
const key = `path_traversal:${ip}`; const key = `path_traversal:${ip}`;
const crossed = securityEventAggregator.trackEvent({ const crossed = securityEventAggregator.trackEvent({
@ -197,16 +197,16 @@ export class IntrusionDetector {
/** /**
* Check for port scanning * Check for port scanning
*/ */
checkPortScanning(params: { checkPortScanning(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
ip: string;
event: SecurityEvent;
}): IntrusionDetectionResult {
if (!this.config.enabled) { if (!this.config.enabled) {
return { detected: false }; return { detected: false };
} }
const { ip, event } = params; const { ip, event } = params;
const pattern = this.config.patterns.portScanning; const pattern = this.config.patterns.portScanning;
if (!pattern || !pattern.threshold || !pattern.windowMs) {
return { detected: false };
}
const key = `port_scan:${ip}`; const key = `port_scan:${ip}`;
const crossed = securityEventAggregator.trackEvent({ const crossed = securityEventAggregator.trackEvent({

View File

@ -1,8 +1,5 @@
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
import { IpManager } from "./ip-manager.js"; import { IpManager } from "./ip-manager.js";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
vi.mock("node:fs", () => ({ vi.mock("node:fs", () => ({
default: { default: {

View File

@ -250,7 +250,7 @@ export class IpManager {
securityLogger.logIpManagement({ securityLogger.logIpManagement({
action: "firewall_block_failed", action: "firewall_block_failed",
ip, ip,
severity: "error", severity: "critical",
details: { error: String(err) }, details: { error: String(err) },
}); });
}); });
@ -282,7 +282,7 @@ export class IpManager {
securityLogger.logIpManagement({ securityLogger.logIpManagement({
action: "firewall_unblock_failed", action: "firewall_unblock_failed",
ip, ip,
severity: "error", severity: "critical",
details: { error: String(err) }, details: { error: String(err) },
}); });
}); });
@ -295,11 +295,7 @@ export class IpManager {
/** /**
* Add IP to allowlist * Add IP to allowlist
*/ */
allowIp(params: { allowIp(params: { ip: string; reason: string; source?: "auto" | "manual" }): void {
ip: string;
reason: string;
source?: "auto" | "manual";
}): void {
const { ip, reason, source = "manual" } = params; const { ip, reason, source = "manual" } = params;
// Check if already in allowlist // Check if already in allowlist

View File

@ -24,7 +24,7 @@ export function createSecurityContext(req: IncomingMessage): SecurityContext {
export function securityMiddleware( export function securityMiddleware(
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
next: () => void next: () => void,
): void { ): void {
const shield = getSecurityShield(); const shield = getSecurityShield();
@ -48,7 +48,10 @@ export function securityMiddleware(
if (!requestCheck.allowed) { if (!requestCheck.allowed) {
res.statusCode = 429; res.statusCode = 429;
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain");
res.setHeader("Retry-After", String(Math.ceil((requestCheck.rateLimitInfo?.retryAfterMs ?? 60000) / 1000))); res.setHeader(
"Retry-After",
String(Math.ceil((requestCheck.rateLimitInfo?.retryAfterMs ?? 60000) / 1000)),
);
res.end("Too Many Requests"); res.end("Too Many Requests");
return; return;
} }
@ -83,7 +86,10 @@ export function checkConnectionRateLimit(req: IncomingMessage): {
* Authentication rate limit check * Authentication rate limit check
* Call this before processing authentication * Call this before processing authentication
*/ */
export function checkAuthRateLimit(req: IncomingMessage, deviceId?: string): { export function checkAuthRateLimit(
req: IncomingMessage,
deviceId?: string,
): {
allowed: boolean; allowed: boolean;
reason?: string; reason?: string;
retryAfterMs?: number; retryAfterMs?: number;
@ -130,11 +136,7 @@ export function logAuthFailure(req: IncomingMessage, reason: string, deviceId?:
/** /**
* Pairing rate limit check * Pairing rate limit check
*/ */
export function checkPairingRateLimit(params: { export function checkPairingRateLimit(params: { channel: string; sender: string; ip: string }): {
channel: string;
sender: string;
ip: string;
}): {
allowed: boolean; allowed: boolean;
reason?: string; reason?: string;
} { } {
@ -155,11 +157,7 @@ export function checkPairingRateLimit(params: {
/** /**
* Webhook rate limit check * Webhook rate limit check
*/ */
export function checkWebhookRateLimit(params: { export function checkWebhookRateLimit(params: { token: string; path: string; ip: string }): {
token: string;
path: string;
ip: string;
}): {
allowed: boolean; allowed: boolean;
reason?: string; reason?: string;
retryAfterMs?: number; retryAfterMs?: number;

View File

@ -1,3 +1,4 @@
/* eslint-disable typescript-eslint/unbound-method */
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
import { SecurityShield, type SecurityContext } from "./shield.js"; import { SecurityShield, type SecurityContext } from "./shield.js";
import { rateLimiter } from "./rate-limiter.js"; import { rateLimiter } from "./rate-limiter.js";

View File

@ -4,7 +4,7 @@
*/ */
import type { IncomingMessage } from "node:http"; import type { IncomingMessage } from "node:http";
import { rateLimiter, RateLimitKeys, type RateLimit, type RateLimitResult } from "./rate-limiter.js"; import { rateLimiter, RateLimitKeys, type RateLimitResult } from "./rate-limiter.js";
import { ipManager } from "./ip-manager.js"; import { ipManager } from "./ip-manager.js";
import { intrusionDetector } from "./intrusion-detector.js"; import { intrusionDetector } from "./intrusion-detector.js";
import { securityLogger } from "./events/logger.js"; import { securityLogger } from "./events/logger.js";
@ -36,9 +36,10 @@ export class SecurityShield {
this.config = { this.config = {
enabled: config?.enabled ?? DEFAULT_SECURITY_CONFIG.shield.enabled, enabled: config?.enabled ?? DEFAULT_SECURITY_CONFIG.shield.enabled,
rateLimiting: config?.rateLimiting ?? DEFAULT_SECURITY_CONFIG.shield.rateLimiting, rateLimiting: config?.rateLimiting ?? DEFAULT_SECURITY_CONFIG.shield.rateLimiting,
intrusionDetection: config?.intrusionDetection ?? DEFAULT_SECURITY_CONFIG.shield.intrusionDetection, intrusionDetection:
config?.intrusionDetection ?? DEFAULT_SECURITY_CONFIG.shield.intrusionDetection,
ipManagement: config?.ipManagement ?? DEFAULT_SECURITY_CONFIG.shield.ipManagement, ipManagement: config?.ipManagement ?? DEFAULT_SECURITY_CONFIG.shield.ipManagement,
}; } as Required<SecurityShieldConfig>;
} }
/** /**
@ -105,7 +106,11 @@ export class SecurityShield {
} }
// Rate limit per-device (if deviceId provided) // Rate limit per-device (if deviceId provided)
if (ctx.deviceId && this.config.rateLimiting?.enabled && this.config.rateLimiting.perDevice?.authAttempts) { if (
ctx.deviceId &&
this.config.rateLimiting?.enabled &&
this.config.rateLimiting.perDevice?.authAttempts
) {
const limit = this.config.rateLimiting.perDevice.authAttempts; const limit = this.config.rateLimiting.perDevice.authAttempts;
const result = rateLimiter.check(RateLimitKeys.authAttemptDevice(ctx.deviceId), limit); const result = rateLimiter.check(RateLimitKeys.authAttemptDevice(ctx.deviceId), limit);
@ -348,11 +353,7 @@ export class SecurityShield {
/** /**
* Check webhook rate limit * Check webhook rate limit
*/ */
checkWebhook(params: { checkWebhook(params: { token: string; path: string; ip: string }): SecurityCheckResult {
token: string;
path: string;
ip: string;
}): SecurityCheckResult {
if (!this.config.enabled) { if (!this.config.enabled) {
return { allowed: true }; return { allowed: true };
} }

View File

@ -16,9 +16,7 @@ export class TokenBucket {
private tokens: number; private tokens: number;
private lastRefillTime: number; private lastRefillTime: number;
constructor( constructor(private readonly config: TokenBucketConfig) {
private readonly config: TokenBucketConfig
) {
this.tokens = config.capacity; this.tokens = config.capacity;
this.lastRefillTime = Date.now(); this.lastRefillTime = Date.now();
} }
@ -86,10 +84,7 @@ export class TokenBucket {
/** /**
* Create a token bucket from max/window configuration * Create a token bucket from max/window configuration
*/ */
export function createTokenBucket(params: { export function createTokenBucket(params: { max: number; windowMs: number }): TokenBucket {
max: number;
windowMs: number;
}): TokenBucket {
const { max, windowMs } = params; const { max, windowMs } = params;
// Refill rate: max tokens over windowMs // Refill rate: max tokens over windowMs