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}`);
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[] = [];
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()}`);

View File

@ -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;
};

View File

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

View File

@ -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[];
};
};
}

View File

@ -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);
}
/**

View File

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

View File

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

View File

@ -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";

View File

@ -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({

View File

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

View File

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

View File

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

View File

@ -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";

View File

@ -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 };
}

View File

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