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}`);
|
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;
|
||||||
|
|||||||
@ -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()}`);
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user