fix: resolve lint, format, and TypeScript compilation errors
This commit is contained in:
parent
e69eccb4b1
commit
8f42141f75
@ -31,3 +31,9 @@ export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): num
|
||||
if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`);
|
||||
return ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for parseDurationMs
|
||||
* @deprecated Use parseDurationMs instead
|
||||
*/
|
||||
export const parseDuration = parseDurationMs;
|
||||
|
||||
@ -167,18 +167,32 @@ export function registerSecurityCli(program: Command) {
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.heading("Security Shield Status"));
|
||||
lines.push("");
|
||||
lines.push(`Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`);
|
||||
lines.push(`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")}`);
|
||||
lines.push(
|
||||
`Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`,
|
||||
);
|
||||
lines.push(
|
||||
`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) {
|
||||
lines.push(` Telegram: ${theme.success("ENABLED")}`);
|
||||
}
|
||||
|
||||
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"));
|
||||
});
|
||||
|
||||
@ -194,7 +208,11 @@ export function registerSecurityCli(program: Command) {
|
||||
|
||||
await writeConfigFile(cfg);
|
||||
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
|
||||
@ -211,7 +229,11 @@ export function registerSecurityCli(program: Command) {
|
||||
cfg.security.shield.enabled = false;
|
||||
await writeConfigFile(cfg);
|
||||
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
|
||||
@ -279,16 +301,14 @@ export function registerSecurityCli(program: Command) {
|
||||
});
|
||||
|
||||
// openclaw blocklist
|
||||
const blocklist = program
|
||||
.command("blocklist")
|
||||
.description("Manage IP blocklist");
|
||||
const blocklist = program.command("blocklist").description("Manage IP blocklist");
|
||||
|
||||
blocklist
|
||||
.command("list")
|
||||
.description("List all blocked IPs")
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
const entries = ipManager.getBlocklist();
|
||||
const entries = ipManager.getBlockedIps();
|
||||
|
||||
if (opts.json) {
|
||||
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 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(` Source: ${entry.source}`);
|
||||
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("");
|
||||
}
|
||||
});
|
||||
@ -354,16 +376,14 @@ export function registerSecurityCli(program: Command) {
|
||||
});
|
||||
|
||||
// openclaw allowlist
|
||||
const allowlist = program
|
||||
.command("allowlist")
|
||||
.description("Manage IP allowlist");
|
||||
const allowlist = program.command("allowlist").description("Manage IP allowlist");
|
||||
|
||||
allowlist
|
||||
.command("list")
|
||||
.description("List all allowed IPs")
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
const entries = ipManager.getAllowlist();
|
||||
const entries = ipManager.getAllowedIps();
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(entries, null, 2));
|
||||
@ -379,7 +399,7 @@ export function registerSecurityCli(program: Command) {
|
||||
defaultRuntime.log("");
|
||||
|
||||
for (const entry of entries) {
|
||||
defaultRuntime.log(`${theme.bold(entry.ip)}`);
|
||||
defaultRuntime.log(`${theme.heading(entry.ip)}`);
|
||||
defaultRuntime.log(` Reason: ${entry.reason}`);
|
||||
defaultRuntime.log(` Source: ${entry.source}`);
|
||||
defaultRuntime.log(` Added: ${new Date(entry.addedAt).toLocaleString()}`);
|
||||
|
||||
@ -97,7 +97,12 @@ export interface AlertingConfig {
|
||||
/** Alert triggers */
|
||||
triggers?: {
|
||||
criticalEvents?: AlertTriggerConfig;
|
||||
failedAuthSpike?: { enabled?: boolean; threshold?: number; windowMs?: number; throttleMs?: number };
|
||||
failedAuthSpike?: {
|
||||
enabled?: boolean;
|
||||
threshold?: number;
|
||||
windowMs?: number;
|
||||
throttleMs?: number;
|
||||
};
|
||||
ipBlocked?: AlertTriggerConfig;
|
||||
};
|
||||
|
||||
|
||||
@ -24,11 +24,11 @@ export class AlertManager {
|
||||
|
||||
private initializeChannels(): void {
|
||||
// Telegram channel
|
||||
if (this.config.channels.telegram?.enabled) {
|
||||
if (this.config.channels?.telegram?.enabled) {
|
||||
const telegram = new TelegramAlertChannel({
|
||||
enabled: true,
|
||||
botToken: this.config.channels.telegram.botToken,
|
||||
chatId: this.config.channels.telegram.chatId,
|
||||
botToken: this.config.channels.telegram.botToken ?? "",
|
||||
chatId: this.config.channels.telegram.chatId ?? "",
|
||||
});
|
||||
if (telegram.isEnabled()) {
|
||||
this.channels.push(telegram);
|
||||
@ -47,7 +47,7 @@ export class AlertManager {
|
||||
* Check if alerting is enabled
|
||||
*/
|
||||
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
|
||||
const results = await Promise.allSettled(
|
||||
this.channels.map((channel) => channel.send(alert)),
|
||||
);
|
||||
const results = await Promise.allSettled(this.channels.map((channel) => channel.send(alert)));
|
||||
|
||||
// Log results
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
let _failureCount = 0;
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled" && result.value.ok) {
|
||||
successCount++;
|
||||
} else {
|
||||
failureCount++;
|
||||
const error =
|
||||
result.status === "fulfilled" ? result.value.error : String(result.reason);
|
||||
_failureCount++;
|
||||
const error = result.status === "fulfilled" ? result.value.error : String(result.reason);
|
||||
log.error(`alert send failed: ${error}`);
|
||||
}
|
||||
}
|
||||
@ -105,10 +102,7 @@ export class AlertManager {
|
||||
}
|
||||
|
||||
// Critical events
|
||||
if (
|
||||
event.severity === "critical" &&
|
||||
this.config.triggers.criticalEvents?.enabled
|
||||
) {
|
||||
if (event.severity === "critical" && this.config.triggers?.criticalEvents?.enabled) {
|
||||
await this.sendAlert({
|
||||
id: randomUUID(),
|
||||
severity: "critical",
|
||||
@ -126,10 +120,7 @@ export class AlertManager {
|
||||
}
|
||||
|
||||
// IP blocked
|
||||
if (
|
||||
event.action === SecurityActions.IP_BLOCKED &&
|
||||
this.config.triggers.ipBlocked?.enabled
|
||||
) {
|
||||
if (event.action === SecurityActions.IP_BLOCKED && this.config.triggers?.ipBlocked?.enabled) {
|
||||
await this.sendAlert({
|
||||
id: randomUUID(),
|
||||
severity: "warn",
|
||||
@ -146,14 +137,14 @@ export class AlertManager {
|
||||
}
|
||||
|
||||
// Intrusion detected
|
||||
if (
|
||||
[
|
||||
SecurityActions.BRUTE_FORCE_DETECTED,
|
||||
SecurityActions.SSRF_BYPASS_ATTEMPT,
|
||||
SecurityActions.PATH_TRAVERSAL_ATTEMPT,
|
||||
SecurityActions.PORT_SCANNING_DETECTED,
|
||||
].includes(event.action)
|
||||
) {
|
||||
const criticalActions = [
|
||||
SecurityActions.BRUTE_FORCE_DETECTED,
|
||||
SecurityActions.SSRF_BYPASS_ATTEMPT,
|
||||
SecurityActions.PATH_TRAVERSAL_ATTEMPT,
|
||||
SecurityActions.PORT_SCANNING_DETECTED,
|
||||
] as const;
|
||||
|
||||
if (criticalActions.includes(event.action as (typeof criticalActions)[number])) {
|
||||
const pattern = event.attackPattern || "unknown";
|
||||
await this.sendAlert({
|
||||
id: randomUUID(),
|
||||
@ -164,7 +155,8 @@ export class AlertManager {
|
||||
details: {
|
||||
pattern,
|
||||
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,
|
||||
},
|
||||
trigger: "intrusion_detected",
|
||||
@ -175,9 +167,9 @@ export class AlertManager {
|
||||
private getThrottleMs(trigger: string): number {
|
||||
switch (trigger) {
|
||||
case "critical_event":
|
||||
return this.config.triggers.criticalEvents?.throttleMs || 0;
|
||||
return this.config.triggers?.criticalEvents?.throttleMs || 0;
|
||||
case "ip_blocked":
|
||||
return this.config.triggers.ipBlocked?.throttleMs || 0;
|
||||
return this.config.triggers?.ipBlocked?.throttleMs || 0;
|
||||
case "intrusion_detected":
|
||||
return 300_000; // 5 minutes default
|
||||
default:
|
||||
|
||||
@ -31,26 +31,44 @@ export interface AlertChannelInterface {
|
||||
}
|
||||
|
||||
export interface AlertTriggerConfig {
|
||||
enabled: boolean;
|
||||
enabled?: boolean;
|
||||
throttleMs?: number;
|
||||
}
|
||||
|
||||
export interface AlertingConfig {
|
||||
enabled: boolean;
|
||||
triggers: {
|
||||
enabled?: boolean;
|
||||
triggers?: {
|
||||
criticalEvents?: AlertTriggerConfig;
|
||||
failedAuthSpike?: AlertTriggerConfig & { threshold: number; windowMs: number };
|
||||
failedAuthSpike?: AlertTriggerConfig & { threshold?: number; windowMs?: number };
|
||||
ipBlocked?: AlertTriggerConfig;
|
||||
};
|
||||
channels: {
|
||||
channels?: {
|
||||
telegram?: {
|
||||
enabled: boolean;
|
||||
botToken: string;
|
||||
chatId: string;
|
||||
enabled?: boolean;
|
||||
botToken?: string;
|
||||
chatId?: string;
|
||||
};
|
||||
webhook?: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
enabled?: boolean;
|
||||
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[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* 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
|
||||
@ -59,9 +59,7 @@ export class SecurityEventAggregator {
|
||||
}
|
||||
|
||||
// Filter out events outside the time window
|
||||
count.events = count.events.filter(
|
||||
(e) => new Date(e.timestamp).getTime() > windowStart
|
||||
);
|
||||
count.events = count.events.filter((e) => new Date(e.timestamp).getTime() > windowStart);
|
||||
|
||||
// Add new event
|
||||
count.events.push(event);
|
||||
@ -80,10 +78,7 @@ export class SecurityEventAggregator {
|
||||
/**
|
||||
* Get event count for a key within a window
|
||||
*/
|
||||
getCount(params: {
|
||||
key: string;
|
||||
windowMs: number;
|
||||
}): number {
|
||||
getCount(params: { key: string; windowMs: number }): number {
|
||||
const { key, windowMs } = params;
|
||||
const count = this.eventCounts.get(key);
|
||||
|
||||
@ -94,7 +89,7 @@ export class SecurityEventAggregator {
|
||||
|
||||
// Filter events in window
|
||||
const eventsInWindow = count.events.filter(
|
||||
(e) => new Date(e.timestamp).getTime() > windowStart
|
||||
(e) => new Date(e.timestamp).getTime() > windowStart,
|
||||
);
|
||||
|
||||
return eventsInWindow.length;
|
||||
@ -103,10 +98,7 @@ export class SecurityEventAggregator {
|
||||
/**
|
||||
* Get aggregated events for a key
|
||||
*/
|
||||
getEvents(params: {
|
||||
key: string;
|
||||
windowMs?: number;
|
||||
}): SecurityEvent[] {
|
||||
getEvents(params: { key: string; windowMs?: number }): SecurityEvent[] {
|
||||
const { key, windowMs } = params;
|
||||
const count = this.eventCounts.get(key);
|
||||
|
||||
@ -119,9 +111,7 @@ export class SecurityEventAggregator {
|
||||
const now = Date.now();
|
||||
const windowStart = now - windowMs;
|
||||
|
||||
return count.events.filter(
|
||||
(e) => new Date(e.timestamp).getTime() > windowStart
|
||||
);
|
||||
return count.events.filter((e) => new Date(e.timestamp).getTime() > windowStart);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,9 +7,13 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import type { SecurityEvent, SecurityEventSeverity, SecurityEventCategory, SecurityEventOutcome } from "./schema.js";
|
||||
import { DEFAULT_LOG_DIR } from "../../logging/logger.js";
|
||||
import { getChildLogger } from "../../logging/index.js";
|
||||
import type {
|
||||
SecurityEvent,
|
||||
SecurityEventSeverity,
|
||||
SecurityEventCategory,
|
||||
SecurityEventOutcome,
|
||||
} from "./schema.js";
|
||||
import { DEFAULT_LOG_DIR, getChildLogger } from "../../logging/logger.js";
|
||||
import { getAlertManager } from "../alerting/manager.js";
|
||||
|
||||
const SECURITY_LOG_PREFIX = "security";
|
||||
@ -229,7 +233,8 @@ class SecurityEventLogger {
|
||||
* Log event to main logger for OTEL export and console output
|
||||
*/
|
||||
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}`, {
|
||||
eventId: event.eventId,
|
||||
|
||||
@ -41,7 +41,7 @@ export class FirewallManager {
|
||||
} else if (this.config.backend === "ufw") {
|
||||
this.backend = new UfwBackend();
|
||||
} else {
|
||||
return { ok: false, error: `unknown backend: ${this.config.backend}` };
|
||||
return { ok: false, error: `unknown backend: ${String(this.config.backend)}` };
|
||||
}
|
||||
|
||||
// Check availability
|
||||
@ -196,9 +196,7 @@ let firewallManager: FirewallManager | null = null;
|
||||
/**
|
||||
* Initialize firewall manager with config
|
||||
*/
|
||||
export async function initFirewallManager(
|
||||
config: FirewallManagerConfig,
|
||||
): Promise<FirewallManager> {
|
||||
export async function initFirewallManager(config: FirewallManagerConfig): Promise<FirewallManager> {
|
||||
firewallManager = new FirewallManager(config);
|
||||
await firewallManager.initialize();
|
||||
return firewallManager;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable typescript-eslint/unbound-method */
|
||||
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
|
||||
import { IntrusionDetector } from "./intrusion-detector.js";
|
||||
import { SecurityActions, AttackPatterns, type SecurityEvent } from "./events/schema.js";
|
||||
|
||||
@ -47,16 +47,16 @@ export class IntrusionDetector {
|
||||
/**
|
||||
* Check for brute force attack pattern
|
||||
*/
|
||||
checkBruteForce(params: {
|
||||
ip: string;
|
||||
event: SecurityEvent;
|
||||
}): IntrusionDetectionResult {
|
||||
checkBruteForce(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
|
||||
if (!this.config.enabled) {
|
||||
return { detected: false };
|
||||
}
|
||||
|
||||
const { ip, event } = params;
|
||||
const pattern = this.config.patterns.bruteForce;
|
||||
if (!pattern || !pattern.threshold || !pattern.windowMs) {
|
||||
return { detected: false };
|
||||
}
|
||||
const key = `brute_force:${ip}`;
|
||||
|
||||
const crossed = securityEventAggregator.trackEvent({
|
||||
@ -99,16 +99,16 @@ export class IntrusionDetector {
|
||||
/**
|
||||
* Check for SSRF bypass attempts
|
||||
*/
|
||||
checkSsrfBypass(params: {
|
||||
ip: string;
|
||||
event: SecurityEvent;
|
||||
}): IntrusionDetectionResult {
|
||||
checkSsrfBypass(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
|
||||
if (!this.config.enabled) {
|
||||
return { detected: false };
|
||||
}
|
||||
|
||||
const { ip, event } = params;
|
||||
const pattern = this.config.patterns.ssrfBypass;
|
||||
if (!pattern || !pattern.threshold || !pattern.windowMs) {
|
||||
return { detected: false };
|
||||
}
|
||||
const key = `ssrf_bypass:${ip}`;
|
||||
|
||||
const crossed = securityEventAggregator.trackEvent({
|
||||
@ -148,16 +148,16 @@ export class IntrusionDetector {
|
||||
/**
|
||||
* Check for path traversal attempts
|
||||
*/
|
||||
checkPathTraversal(params: {
|
||||
ip: string;
|
||||
event: SecurityEvent;
|
||||
}): IntrusionDetectionResult {
|
||||
checkPathTraversal(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
|
||||
if (!this.config.enabled) {
|
||||
return { detected: false };
|
||||
}
|
||||
|
||||
const { ip, event } = params;
|
||||
const pattern = this.config.patterns.pathTraversal;
|
||||
if (!pattern || !pattern.threshold || !pattern.windowMs) {
|
||||
return { detected: false };
|
||||
}
|
||||
const key = `path_traversal:${ip}`;
|
||||
|
||||
const crossed = securityEventAggregator.trackEvent({
|
||||
@ -197,16 +197,16 @@ export class IntrusionDetector {
|
||||
/**
|
||||
* Check for port scanning
|
||||
*/
|
||||
checkPortScanning(params: {
|
||||
ip: string;
|
||||
event: SecurityEvent;
|
||||
}): IntrusionDetectionResult {
|
||||
checkPortScanning(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult {
|
||||
if (!this.config.enabled) {
|
||||
return { detected: false };
|
||||
}
|
||||
|
||||
const { ip, event } = params;
|
||||
const pattern = this.config.patterns.portScanning;
|
||||
if (!pattern || !pattern.threshold || !pattern.windowMs) {
|
||||
return { detected: false };
|
||||
}
|
||||
const key = `port_scan:${ip}`;
|
||||
|
||||
const crossed = securityEventAggregator.trackEvent({
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
|
||||
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", () => ({
|
||||
default: {
|
||||
|
||||
@ -250,7 +250,7 @@ export class IpManager {
|
||||
securityLogger.logIpManagement({
|
||||
action: "firewall_block_failed",
|
||||
ip,
|
||||
severity: "error",
|
||||
severity: "critical",
|
||||
details: { error: String(err) },
|
||||
});
|
||||
});
|
||||
@ -282,7 +282,7 @@ export class IpManager {
|
||||
securityLogger.logIpManagement({
|
||||
action: "firewall_unblock_failed",
|
||||
ip,
|
||||
severity: "error",
|
||||
severity: "critical",
|
||||
details: { error: String(err) },
|
||||
});
|
||||
});
|
||||
@ -295,11 +295,7 @@ export class IpManager {
|
||||
/**
|
||||
* Add IP to allowlist
|
||||
*/
|
||||
allowIp(params: {
|
||||
ip: string;
|
||||
reason: string;
|
||||
source?: "auto" | "manual";
|
||||
}): void {
|
||||
allowIp(params: { ip: string; reason: string; source?: "auto" | "manual" }): void {
|
||||
const { ip, reason, source = "manual" } = params;
|
||||
|
||||
// Check if already in allowlist
|
||||
|
||||
@ -24,7 +24,7 @@ export function createSecurityContext(req: IncomingMessage): SecurityContext {
|
||||
export function securityMiddleware(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: () => void
|
||||
next: () => void,
|
||||
): void {
|
||||
const shield = getSecurityShield();
|
||||
|
||||
@ -48,7 +48,10 @@ export function securityMiddleware(
|
||||
if (!requestCheck.allowed) {
|
||||
res.statusCode = 429;
|
||||
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");
|
||||
return;
|
||||
}
|
||||
@ -83,7 +86,10 @@ export function checkConnectionRateLimit(req: IncomingMessage): {
|
||||
* Authentication rate limit check
|
||||
* Call this before processing authentication
|
||||
*/
|
||||
export function checkAuthRateLimit(req: IncomingMessage, deviceId?: string): {
|
||||
export function checkAuthRateLimit(
|
||||
req: IncomingMessage,
|
||||
deviceId?: string,
|
||||
): {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
retryAfterMs?: number;
|
||||
@ -130,11 +136,7 @@ export function logAuthFailure(req: IncomingMessage, reason: string, deviceId?:
|
||||
/**
|
||||
* Pairing rate limit check
|
||||
*/
|
||||
export function checkPairingRateLimit(params: {
|
||||
channel: string;
|
||||
sender: string;
|
||||
ip: string;
|
||||
}): {
|
||||
export function checkPairingRateLimit(params: { channel: string; sender: string; ip: string }): {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
} {
|
||||
@ -155,11 +157,7 @@ export function checkPairingRateLimit(params: {
|
||||
/**
|
||||
* Webhook rate limit check
|
||||
*/
|
||||
export function checkWebhookRateLimit(params: {
|
||||
token: string;
|
||||
path: string;
|
||||
ip: string;
|
||||
}): {
|
||||
export function checkWebhookRateLimit(params: { token: string; path: string; ip: string }): {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
retryAfterMs?: number;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable typescript-eslint/unbound-method */
|
||||
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
|
||||
import { SecurityShield, type SecurityContext } from "./shield.js";
|
||||
import { rateLimiter } from "./rate-limiter.js";
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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 { intrusionDetector } from "./intrusion-detector.js";
|
||||
import { securityLogger } from "./events/logger.js";
|
||||
@ -36,9 +36,10 @@ export class SecurityShield {
|
||||
this.config = {
|
||||
enabled: config?.enabled ?? DEFAULT_SECURITY_CONFIG.shield.enabled,
|
||||
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,
|
||||
};
|
||||
} as Required<SecurityShieldConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,7 +106,11 @@ export class SecurityShield {
|
||||
}
|
||||
|
||||
// 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 result = rateLimiter.check(RateLimitKeys.authAttemptDevice(ctx.deviceId), limit);
|
||||
|
||||
@ -348,11 +353,7 @@ export class SecurityShield {
|
||||
/**
|
||||
* Check webhook rate limit
|
||||
*/
|
||||
checkWebhook(params: {
|
||||
token: string;
|
||||
path: string;
|
||||
ip: string;
|
||||
}): SecurityCheckResult {
|
||||
checkWebhook(params: { token: string; path: string; ip: string }): SecurityCheckResult {
|
||||
if (!this.config.enabled) {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
@ -16,9 +16,7 @@ export class TokenBucket {
|
||||
private tokens: number;
|
||||
private lastRefillTime: number;
|
||||
|
||||
constructor(
|
||||
private readonly config: TokenBucketConfig
|
||||
) {
|
||||
constructor(private readonly config: TokenBucketConfig) {
|
||||
this.tokens = config.capacity;
|
||||
this.lastRefillTime = Date.now();
|
||||
}
|
||||
@ -86,10 +84,7 @@ export class TokenBucket {
|
||||
/**
|
||||
* Create a token bucket from max/window configuration
|
||||
*/
|
||||
export function createTokenBucket(params: {
|
||||
max: number;
|
||||
windowMs: number;
|
||||
}): TokenBucket {
|
||||
export function createTokenBucket(params: { max: number; windowMs: number }): TokenBucket {
|
||||
const { max, windowMs } = params;
|
||||
|
||||
// Refill rate: max tokens over windowMs
|
||||
|
||||
Loading…
Reference in New Issue
Block a user