securitywatch 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sandeep Sharma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ import type { SecurityConfig, ThreatInfo, Store } from "../types.js";
2
+ export declare class DetectionEngine {
3
+ private config;
4
+ private thresholds;
5
+ private scorer;
6
+ private detectBruteForce;
7
+ private detectRateLimit;
8
+ private detectSuspicious;
9
+ constructor(config: SecurityConfig, store: Store);
10
+ analyze(params: {
11
+ ip: string;
12
+ path: string;
13
+ method: string;
14
+ body?: string;
15
+ query?: string;
16
+ headers?: string;
17
+ }): ThreatInfo;
18
+ recordResponse(ip: string, path: string, statusCode: number): void;
19
+ private decide;
20
+ private getRouteSensitivity;
21
+ isWhitelisted(ip: string): boolean;
22
+ destroy(): void;
23
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DetectionEngine = void 0;
4
+ const index_js_1 = require("./rules/index.js");
5
+ const scorer_js_1 = require("./scorer.js");
6
+ const DEFAULT_THRESHOLDS = { warn: 5, throttle: 10, block: 15 };
7
+ const SENSITIVITY_MULTIPLIER = {
8
+ low: 0.5, medium: 1, high: 1.5, critical: 2,
9
+ };
10
+ class DetectionEngine {
11
+ config;
12
+ thresholds;
13
+ scorer;
14
+ detectBruteForce;
15
+ detectRateLimit;
16
+ detectSuspicious;
17
+ constructor(config, store) {
18
+ this.config = config;
19
+ this.thresholds = { ...DEFAULT_THRESHOLDS, ...config.thresholds };
20
+ this.scorer = config.ipReputation !== false ? new scorer_js_1.IPScorer(store) : null;
21
+ const bfConfig = typeof config.bruteForce === "object" ? config.bruteForce : undefined;
22
+ this.detectBruteForce = (0, index_js_1.createBruteForceDetector)(store, bfConfig);
23
+ const rlConfig = typeof config.rateLimit === "object" ? config.rateLimit : undefined;
24
+ this.detectRateLimit = (0, index_js_1.createRateLimiter)(store, rlConfig);
25
+ this.detectSuspicious = (0, index_js_1.createSuspiciousBehaviorDetector)(store);
26
+ }
27
+ analyze(params) {
28
+ const { ip, path, method, body, query, headers } = params;
29
+ const results = [];
30
+ const payloadInputs = [body, query].filter(Boolean);
31
+ const combinedPayload = payloadInputs.join(" ");
32
+ const fullInput = headers ? `${combinedPayload} ${headers}` : combinedPayload;
33
+ if (this.config.sqlInjection !== false)
34
+ results.push((0, index_js_1.detectSQLInjection)(fullInput));
35
+ if (this.config.xss !== false)
36
+ results.push((0, index_js_1.detectXSS)(fullInput));
37
+ if (this.config.bruteForce !== false)
38
+ results.push(this.detectBruteForce(ip, path));
39
+ if (this.config.rateLimit !== false)
40
+ results.push(this.detectRateLimit(ip, path));
41
+ if (this.config.suspiciousBehavior !== false)
42
+ results.push(this.detectSuspicious(ip, path, method));
43
+ if (this.config.payloadAnomaly !== false)
44
+ results.push((0, index_js_1.detectPayloadAnomaly)(combinedPayload));
45
+ let totalScore = results
46
+ .filter((r) => r.triggered)
47
+ .reduce((sum, r) => sum + r.score, 0);
48
+ const sensitivity = this.getRouteSensitivity(path);
49
+ totalScore = Math.round(totalScore * SENSITIVITY_MULTIPLIER[sensitivity]);
50
+ if (this.scorer) {
51
+ this.scorer.addScore(ip, totalScore > 0 ? totalScore : -0.5);
52
+ const ipScore = this.scorer.getScore(ip);
53
+ if (ipScore > 20 && totalScore > 0)
54
+ totalScore += 5;
55
+ }
56
+ return {
57
+ action: this.decide(totalScore),
58
+ totalScore,
59
+ ip, path, method,
60
+ results: results.filter((r) => r.triggered),
61
+ timestamp: new Date(),
62
+ };
63
+ }
64
+ recordResponse(ip, path, statusCode) {
65
+ if (this.config.bruteForce === false)
66
+ return;
67
+ this.detectBruteForce(ip, path, statusCode);
68
+ }
69
+ decide(score) {
70
+ if (score >= this.thresholds.block)
71
+ return "block";
72
+ if (score >= this.thresholds.throttle)
73
+ return "throttle";
74
+ if (score >= this.thresholds.warn)
75
+ return "warn";
76
+ return "allow";
77
+ }
78
+ getRouteSensitivity(path) {
79
+ const map = this.config.routeSensitivity ?? {};
80
+ for (const [route, sensitivity] of Object.entries(map)) {
81
+ if (path.startsWith(route))
82
+ return sensitivity;
83
+ }
84
+ return "medium";
85
+ }
86
+ isWhitelisted(ip) {
87
+ return this.config.whitelist?.includes(ip) ?? false;
88
+ }
89
+ destroy() {
90
+ this.scorer?.destroy();
91
+ }
92
+ }
93
+ exports.DetectionEngine = DetectionEngine;
@@ -0,0 +1,2 @@
1
+ import type { DetectionResult, BruteForceConfig, Store } from "../../types.js";
2
+ export declare function createBruteForceDetector(store: Store, config?: Partial<BruteForceConfig>): (ip: string, path: string, statusCode?: number) => DetectionResult;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBruteForceDetector = createBruteForceDetector;
4
+ const DEFAULTS = {
5
+ maxAttempts: 5,
6
+ windowMs: 5 * 60 * 1000,
7
+ blockDurationMs: 15 * 60 * 1000,
8
+ authRoutes: ["/login", "/signin", "/auth", "/api/auth", "/api/login"],
9
+ };
10
+ function createBruteForceDetector(store, config) {
11
+ const opts = { ...DEFAULTS, ...config };
12
+ return function detectBruteForce(ip, path, statusCode) {
13
+ const isAuthRoute = opts.authRoutes.some((route) => path.toLowerCase().startsWith(route));
14
+ if (!isAuthRoute) {
15
+ return { triggered: false, score: 0, rule: "brute-force", reason: "" };
16
+ }
17
+ const blockKey = `bf:block:${ip}`;
18
+ const attemptKey = `bf:attempts:${ip}`;
19
+ if (store.get(blockKey)) {
20
+ return {
21
+ triggered: true,
22
+ score: 10,
23
+ rule: "brute-force",
24
+ reason: "IP is temporarily blocked due to repeated failed login attempts",
25
+ };
26
+ }
27
+ // Clear counter on successful auth
28
+ if (statusCode !== undefined && statusCode >= 200 && statusCode < 300) {
29
+ store.delete(attemptKey);
30
+ return { triggered: false, score: 0, rule: "brute-force", reason: "" };
31
+ }
32
+ if (statusCode !== undefined && statusCode >= 400 && statusCode < 500) {
33
+ const attempts = store.increment(attemptKey, opts.windowMs);
34
+ if (attempts >= opts.maxAttempts * 4) {
35
+ store.set(blockKey, true, 24 * 60 * 60 * 1000);
36
+ return {
37
+ triggered: true,
38
+ score: 10,
39
+ rule: "brute-force",
40
+ reason: `${attempts} failed login attempts — blocked for 24 hours`,
41
+ };
42
+ }
43
+ if (attempts >= opts.maxAttempts * 2) {
44
+ store.set(blockKey, true, opts.blockDurationMs);
45
+ return {
46
+ triggered: true,
47
+ score: 8,
48
+ rule: "brute-force",
49
+ reason: `${attempts} failed login attempts — temporarily blocked`,
50
+ };
51
+ }
52
+ if (attempts >= opts.maxAttempts) {
53
+ return {
54
+ triggered: true,
55
+ score: 7,
56
+ rule: "brute-force",
57
+ reason: `${attempts} failed login attempts in ${opts.windowMs / 60000} minutes`,
58
+ };
59
+ }
60
+ }
61
+ // Pre-request: check existing attempt count
62
+ if (statusCode === undefined) {
63
+ const currentAttempts = store.get(attemptKey) ?? 0;
64
+ if (currentAttempts >= opts.maxAttempts) {
65
+ return {
66
+ triggered: true,
67
+ score: 5,
68
+ rule: "brute-force",
69
+ reason: `${currentAttempts} prior failed login attempts from this IP`,
70
+ };
71
+ }
72
+ }
73
+ return { triggered: false, score: 0, rule: "brute-force", reason: "" };
74
+ };
75
+ }
@@ -0,0 +1,6 @@
1
+ export { detectSQLInjection } from "./sql-injection.js";
2
+ export { detectXSS } from "./xss.js";
3
+ export { createBruteForceDetector } from "./brute-force.js";
4
+ export { createRateLimiter } from "./rate-limit.js";
5
+ export { createSuspiciousBehaviorDetector } from "./suspicious-behavior.js";
6
+ export { detectPayloadAnomaly } from "./payload-anomaly.js";
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectPayloadAnomaly = exports.createSuspiciousBehaviorDetector = exports.createRateLimiter = exports.createBruteForceDetector = exports.detectXSS = exports.detectSQLInjection = void 0;
4
+ var sql_injection_js_1 = require("./sql-injection.js");
5
+ Object.defineProperty(exports, "detectSQLInjection", { enumerable: true, get: function () { return sql_injection_js_1.detectSQLInjection; } });
6
+ var xss_js_1 = require("./xss.js");
7
+ Object.defineProperty(exports, "detectXSS", { enumerable: true, get: function () { return xss_js_1.detectXSS; } });
8
+ var brute_force_js_1 = require("./brute-force.js");
9
+ Object.defineProperty(exports, "createBruteForceDetector", { enumerable: true, get: function () { return brute_force_js_1.createBruteForceDetector; } });
10
+ var rate_limit_js_1 = require("./rate-limit.js");
11
+ Object.defineProperty(exports, "createRateLimiter", { enumerable: true, get: function () { return rate_limit_js_1.createRateLimiter; } });
12
+ var suspicious_behavior_js_1 = require("./suspicious-behavior.js");
13
+ Object.defineProperty(exports, "createSuspiciousBehaviorDetector", { enumerable: true, get: function () { return suspicious_behavior_js_1.createSuspiciousBehaviorDetector; } });
14
+ var payload_anomaly_js_1 = require("./payload-anomaly.js");
15
+ Object.defineProperty(exports, "detectPayloadAnomaly", { enumerable: true, get: function () { return payload_anomaly_js_1.detectPayloadAnomaly; } });
@@ -0,0 +1,2 @@
1
+ import type { DetectionResult } from "../../types.js";
2
+ export declare function detectPayloadAnomaly(input: string): DetectionResult;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectPayloadAnomaly = detectPayloadAnomaly;
4
+ const MAX_PAYLOAD_LENGTH = 10_000;
5
+ const SPECIAL_CHAR_THRESHOLD = 0.3;
6
+ const MAX_NESTING_DEPTH = 50;
7
+ function specialCharRatio(input) {
8
+ if (input.length === 0)
9
+ return 0;
10
+ const specialChars = input.replace(/[a-zA-Z0-9\s.,;:!?@#$%&*()\-_=+\[\]{}'"\/\\]/g, "");
11
+ return specialChars.length / input.length;
12
+ }
13
+ function calculateNestingDepth(input) {
14
+ let maxDepth = 0;
15
+ let currentDepth = 0;
16
+ for (const char of input) {
17
+ if (char === "{" || char === "[") {
18
+ currentDepth++;
19
+ if (currentDepth > maxDepth)
20
+ maxDepth = currentDepth;
21
+ }
22
+ else if (char === "}" || char === "]") {
23
+ currentDepth = Math.max(0, currentDepth - 1);
24
+ }
25
+ }
26
+ return maxDepth;
27
+ }
28
+ function detectPayloadAnomaly(input) {
29
+ let score = 0;
30
+ const matched = [];
31
+ if (input.length > MAX_PAYLOAD_LENGTH) {
32
+ score += 3;
33
+ matched.push(`oversized payload (${input.length} chars)`);
34
+ }
35
+ const ratio = specialCharRatio(input);
36
+ if (ratio > SPECIAL_CHAR_THRESHOLD) {
37
+ score += 4;
38
+ matched.push(`high special-char density (${(ratio * 100).toFixed(0)}%)`);
39
+ }
40
+ if (input.includes("\0") || input.includes("%00")) {
41
+ score += 5;
42
+ matched.push("null byte injection");
43
+ }
44
+ const depth = calculateNestingDepth(input);
45
+ if (depth > MAX_NESTING_DEPTH) {
46
+ score += 3;
47
+ matched.push(`deeply nested structure (depth ${depth})`);
48
+ }
49
+ return {
50
+ triggered: score > 0,
51
+ score,
52
+ rule: "payload-anomaly",
53
+ reason: matched.length ? `Payload anomaly: ${matched.join(", ")}` : "",
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { DetectionResult, RateLimitConfig, Store } from "../../types.js";
2
+ export declare function createRateLimiter(store: Store, config?: Partial<RateLimitConfig>): (ip: string, path: string) => DetectionResult;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRateLimiter = createRateLimiter;
4
+ const DEFAULTS = {
5
+ windowMs: 60 * 1000,
6
+ maxRequests: 100,
7
+ routes: {},
8
+ };
9
+ function normalizePath(path) {
10
+ return path
11
+ .split("?")[0]
12
+ .replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "/:id")
13
+ .replace(/\/\d+/g, "/:n");
14
+ }
15
+ function createRateLimiter(store, config) {
16
+ const opts = { ...DEFAULTS, ...config };
17
+ return function detectRateLimit(ip, path) {
18
+ const normalized = normalizePath(path);
19
+ let maxRequests = opts.maxRequests;
20
+ for (const [route, limit] of Object.entries(opts.routes ?? {})) {
21
+ if (normalized.startsWith(route)) {
22
+ maxRequests = limit;
23
+ break;
24
+ }
25
+ }
26
+ const key = `rl:${ip}:${normalized}`;
27
+ const globalKey = `rl:global:${ip}`;
28
+ const routeCount = store.increment(key, opts.windowMs);
29
+ const globalCount = store.increment(globalKey, opts.windowMs);
30
+ if (globalCount > opts.maxRequests * 3) {
31
+ return {
32
+ triggered: true,
33
+ score: 6,
34
+ rule: "rate-limit",
35
+ reason: `Traffic spike: ${globalCount} requests in ${opts.windowMs / 1000}s (${opts.maxRequests * 3} threshold)`,
36
+ };
37
+ }
38
+ if (routeCount > maxRequests) {
39
+ return {
40
+ triggered: true,
41
+ score: 4,
42
+ rule: "rate-limit",
43
+ reason: `Rate limit exceeded: ${routeCount}/${maxRequests} on ${normalized}`,
44
+ };
45
+ }
46
+ return { triggered: false, score: 0, rule: "rate-limit", reason: "" };
47
+ };
48
+ }
@@ -0,0 +1,2 @@
1
+ import type { DetectionResult } from "../../types.js";
2
+ export declare function detectSQLInjection(input: string): DetectionResult;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectSQLInjection = detectSQLInjection;
4
+ const MAX_INPUT_LENGTH = 20_000;
5
+ const TAUTOLOGY = /('|")\s{0,5}OR\s{1,5}(1\s*=\s*1|\w{1,20}\s*=\s*\w{1,20})/i;
6
+ const UNION_SELECT = /UNION\s{1,10}(ALL\s{1,10})?SELECT\b/i;
7
+ const STACKED_QUERY = /;\s{0,5}(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE|EXEC)\b/i;
8
+ const COMMENT_WITH_SQL = /(--|\/\*).{0,200}?(SELECT|DROP|INSERT|DELETE|UPDATE|UNION)\b/i;
9
+ const ENCODED_INJECTION = /(CHAR|CHR|0x)\s{0,3}\(/i;
10
+ const TIME_BASED = /(SLEEP|BENCHMARK|WAITFOR\s{1,5}DELAY|PG_SLEEP)\s{0,3}\(/i;
11
+ const NOSQL_OPERATOR = /\$\s{0,3}(gt|gte|lt|lte|ne|in|nin|regex|where|exists|or|and)\b/i;
12
+ const COMMAND_EXECUTION = /\b(xp_cmdshell|cmd\.exe|EXEC(UTE)?)\b/i;
13
+ const SCHEMA_MANIPULATION = /\b(DROP\s{1,5}(TABLE|DATABASE|INDEX|VIEW)|ALTER\s{1,5}TABLE|GRANT\s{1,5}ALL|FLUSH\s{1,5}PRIVILEGES)\b/i;
14
+ const MASS_EXPORT = /\b(INTO\s{1,5}(OUT|DUMP)FILE|mysqldump|pg_dump)\b/i;
15
+ const rules = [
16
+ { pattern: TAUTOLOGY, score: 5, label: "tautology attack" },
17
+ { pattern: UNION_SELECT, score: 5, label: "UNION SELECT" },
18
+ { pattern: STACKED_QUERY, score: 6, label: "stacked query" },
19
+ { pattern: COMMENT_WITH_SQL, score: 4, label: "comment bypass with SQL keyword" },
20
+ { pattern: ENCODED_INJECTION, score: 4, label: "encoded injection" },
21
+ { pattern: TIME_BASED, score: 5, label: "time-based blind injection" },
22
+ { pattern: NOSQL_OPERATOR, score: 4, label: "NoSQL operator injection" },
23
+ { pattern: COMMAND_EXECUTION, score: 6, label: "OS command execution via SQL" },
24
+ { pattern: SCHEMA_MANIPULATION, score: 6, label: "schema manipulation" },
25
+ { pattern: MASS_EXPORT, score: 5, label: "mass data export" },
26
+ ];
27
+ function detectSQLInjection(input) {
28
+ const safe = input.length > MAX_INPUT_LENGTH ? input.slice(0, MAX_INPUT_LENGTH) : input;
29
+ let score = 0;
30
+ const matched = [];
31
+ for (const rule of rules) {
32
+ if (rule.pattern.test(safe)) {
33
+ score += rule.score;
34
+ matched.push(rule.label);
35
+ }
36
+ }
37
+ return {
38
+ triggered: score > 0,
39
+ score,
40
+ rule: "sql-injection",
41
+ reason: matched.length ? `SQL injection: ${matched.join(", ")}` : "",
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { DetectionResult, Store } from "../../types.js";
2
+ export declare function createSuspiciousBehaviorDetector(store: Store): (ip: string, path: string, method: string) => DetectionResult;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSuspiciousBehaviorDetector = createSuspiciousBehaviorDetector;
4
+ const SENSITIVE_PATHS = [
5
+ "/admin", "/.env", "/config", "/wp-admin", "/wp-login",
6
+ "/phpmyadmin", "/.git", "/.htaccess", "/server-status",
7
+ "/debug", "/actuator", "/graphql", "/api/v1/admin",
8
+ ];
9
+ const SUSPICIOUS_EXTENSIONS = /\.(sql|bak|backup|old|orig|conf|log|ini|env)$/i;
10
+ const MAX_TRACKED_ROUTES = 100;
11
+ const ENDPOINT_SCAN_THRESHOLD = 20;
12
+ function createSuspiciousBehaviorDetector(store) {
13
+ return function detectSuspiciousBehavior(ip, path, method) {
14
+ let score = 0;
15
+ const matched = [];
16
+ const normalizedPath = path.toLowerCase();
17
+ for (const sensitive of SENSITIVE_PATHS) {
18
+ if (normalizedPath.startsWith(sensitive)) {
19
+ score += 5;
20
+ matched.push(`probing ${sensitive}`);
21
+ break;
22
+ }
23
+ }
24
+ if (SUSPICIOUS_EXTENSIONS.test(path)) {
25
+ score += 4;
26
+ matched.push("suspicious file extension");
27
+ }
28
+ if (path.includes("..") || path.includes("%2e%2e") ||
29
+ path.includes("%2e.") || path.includes(".%2e") ||
30
+ path.includes("%252e%252e") || path.includes("..%5c") ||
31
+ path.includes("..%c0%af") || path.includes("..;/")) {
32
+ score += 6;
33
+ matched.push("directory traversal attempt");
34
+ }
35
+ const routeSetKey = `sb:routes:${ip}`;
36
+ const routes = store.get(routeSetKey) ?? [];
37
+ if (!routes.includes(path) && routes.length < MAX_TRACKED_ROUTES) {
38
+ routes.push(path);
39
+ store.set(routeSetKey, routes, 60_000);
40
+ }
41
+ if (routes.length > ENDPOINT_SCAN_THRESHOLD) {
42
+ score += 5;
43
+ matched.push(`endpoint scanning (${routes.length} unique routes in 1 min)`);
44
+ }
45
+ const unusualMethod = (method === "DELETE" || method === "PUT" || method === "PATCH") &&
46
+ (normalizedPath === "/" || normalizedPath.startsWith("/login") || normalizedPath.startsWith("/signup"));
47
+ if (unusualMethod) {
48
+ score += 3;
49
+ matched.push(`unusual ${method} on ${path}`);
50
+ }
51
+ return {
52
+ triggered: score > 0,
53
+ score,
54
+ rule: "suspicious-behavior",
55
+ reason: matched.length ? `Suspicious: ${matched.join(", ")}` : "",
56
+ };
57
+ };
58
+ }
@@ -0,0 +1,2 @@
1
+ import type { DetectionResult } from "../../types.js";
2
+ export declare function detectXSS(input: string): DetectionResult;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectXSS = detectXSS;
4
+ const MAX_INPUT_LENGTH = 20_000;
5
+ const SCRIPT_TAG = /<script[\s>]/i;
6
+ const EVENT_HANDLER = /\bon(error|load|click|mouseover|mouseenter|focus|focusin|blur|submit|change|input|keydown|keyup|pointerover|animationend|transitionend|beforeunload)\s{0,3}=/i;
7
+ const JS_PROTOCOL = /javascript\s{0,3}:/i;
8
+ const DANGEROUS_TAGS = /<(iframe|object|embed|form|base|meta|link|svg|math)[\s>]/i;
9
+ const DATA_URI = /data\s{0,3}:\s{0,3}(text\/html|application\/xhtml)/i;
10
+ const EVAL_PATTERN = /\b(eval|Function|setTimeout|setInterval)\s{0,3}\(/i;
11
+ const TEMPLATE_INJECTION = /\$\{.{0,100}?\}/;
12
+ const rules = [
13
+ { pattern: SCRIPT_TAG, score: 6, label: "script tag" },
14
+ { pattern: EVENT_HANDLER, score: 4, label: "event handler injection" },
15
+ { pattern: JS_PROTOCOL, score: 5, label: "javascript: protocol" },
16
+ { pattern: DANGEROUS_TAGS, score: 4, label: "dangerous HTML tag" },
17
+ { pattern: DATA_URI, score: 4, label: "data URI with HTML" },
18
+ { pattern: EVAL_PATTERN, score: 3, label: "eval/Function pattern" },
19
+ { pattern: TEMPLATE_INJECTION, score: 3, label: "template literal injection" },
20
+ ];
21
+ function detectXSS(input) {
22
+ const safe = input.length > MAX_INPUT_LENGTH ? input.slice(0, MAX_INPUT_LENGTH) : input;
23
+ let score = 0;
24
+ const matched = [];
25
+ for (const rule of rules) {
26
+ if (rule.pattern.test(safe)) {
27
+ score += rule.score;
28
+ matched.push(rule.label);
29
+ }
30
+ }
31
+ return {
32
+ triggered: score > 0,
33
+ score,
34
+ rule: "xss",
35
+ reason: matched.length ? `XSS: ${matched.join(", ")}` : "",
36
+ };
37
+ }
@@ -0,0 +1,11 @@
1
+ import type { Store } from "../types.js";
2
+ export declare class IPScorer {
3
+ private store;
4
+ private trackedIPs;
5
+ private decayTimer;
6
+ constructor(store: Store);
7
+ addScore(ip: string, points: number): number;
8
+ getScore(ip: string): number;
9
+ private decayAll;
10
+ destroy(): void;
11
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IPScorer = void 0;
4
+ const DECAY_INTERVAL_MS = 60_000;
5
+ const SCORE_TTL_MS = 24 * 60 * 60 * 1000;
6
+ const MAX_TRACKED_IPS = 10_000;
7
+ class IPScorer {
8
+ store;
9
+ trackedIPs = new Set();
10
+ decayTimer;
11
+ constructor(store) {
12
+ this.store = store;
13
+ this.decayTimer = setInterval(() => this.decayAll(), DECAY_INTERVAL_MS);
14
+ if (this.decayTimer.unref)
15
+ this.decayTimer.unref();
16
+ }
17
+ addScore(ip, points) {
18
+ const key = `ip:score:${ip}`;
19
+ const current = this.store.get(key) ?? 0;
20
+ const newScore = Math.max(0, current + points);
21
+ this.store.set(key, newScore, SCORE_TTL_MS);
22
+ if (newScore > 0 && this.trackedIPs.size < MAX_TRACKED_IPS) {
23
+ this.trackedIPs.add(ip);
24
+ }
25
+ else if (newScore === 0) {
26
+ this.trackedIPs.delete(ip);
27
+ }
28
+ return newScore;
29
+ }
30
+ getScore(ip) {
31
+ return this.store.get(`ip:score:${ip}`) ?? 0;
32
+ }
33
+ decayAll() {
34
+ for (const ip of this.trackedIPs) {
35
+ const key = `ip:score:${ip}`;
36
+ const score = this.store.get(key) ?? 0;
37
+ if (score > 0) {
38
+ this.store.set(key, score - 1, SCORE_TTL_MS);
39
+ }
40
+ else {
41
+ this.trackedIPs.delete(ip);
42
+ }
43
+ }
44
+ }
45
+ destroy() {
46
+ clearInterval(this.decayTimer);
47
+ this.trackedIPs.clear();
48
+ }
49
+ }
50
+ exports.IPScorer = IPScorer;
@@ -0,0 +1,11 @@
1
+ export { securityWatch } from "./middleware/express.js";
2
+ export type { SecurityConfig, ThreatInfo, DetectionResult, SecurityAction, Sensitivity, Thresholds, RateLimitConfig, BruteForceConfig, AlertConfig, Store, SecurityWatchMiddleware, } from "./types.js";
3
+ export { DetectionEngine } from "./core/engine.js";
4
+ export { IPScorer } from "./core/scorer.js";
5
+ export { MemoryStore } from "./store/memory.js";
6
+ export { detectSQLInjection } from "./core/rules/sql-injection.js";
7
+ export { detectXSS } from "./core/rules/xss.js";
8
+ export { detectPayloadAnomaly } from "./core/rules/payload-anomaly.js";
9
+ export { createBruteForceDetector } from "./core/rules/brute-force.js";
10
+ export { createRateLimiter } from "./core/rules/rate-limit.js";
11
+ export { createSuspiciousBehaviorDetector } from "./core/rules/suspicious-behavior.js";
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSuspiciousBehaviorDetector = exports.createRateLimiter = exports.createBruteForceDetector = exports.detectPayloadAnomaly = exports.detectXSS = exports.detectSQLInjection = exports.MemoryStore = exports.IPScorer = exports.DetectionEngine = exports.securityWatch = void 0;
4
+ var express_js_1 = require("./middleware/express.js");
5
+ Object.defineProperty(exports, "securityWatch", { enumerable: true, get: function () { return express_js_1.securityWatch; } });
6
+ // Core (for advanced usage)
7
+ var engine_js_1 = require("./core/engine.js");
8
+ Object.defineProperty(exports, "DetectionEngine", { enumerable: true, get: function () { return engine_js_1.DetectionEngine; } });
9
+ var scorer_js_1 = require("./core/scorer.js");
10
+ Object.defineProperty(exports, "IPScorer", { enumerable: true, get: function () { return scorer_js_1.IPScorer; } });
11
+ var memory_js_1 = require("./store/memory.js");
12
+ Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return memory_js_1.MemoryStore; } });
13
+ // Individual rules (for custom pipelines)
14
+ var sql_injection_js_1 = require("./core/rules/sql-injection.js");
15
+ Object.defineProperty(exports, "detectSQLInjection", { enumerable: true, get: function () { return sql_injection_js_1.detectSQLInjection; } });
16
+ var xss_js_1 = require("./core/rules/xss.js");
17
+ Object.defineProperty(exports, "detectXSS", { enumerable: true, get: function () { return xss_js_1.detectXSS; } });
18
+ var payload_anomaly_js_1 = require("./core/rules/payload-anomaly.js");
19
+ Object.defineProperty(exports, "detectPayloadAnomaly", { enumerable: true, get: function () { return payload_anomaly_js_1.detectPayloadAnomaly; } });
20
+ var brute_force_js_1 = require("./core/rules/brute-force.js");
21
+ Object.defineProperty(exports, "createBruteForceDetector", { enumerable: true, get: function () { return brute_force_js_1.createBruteForceDetector; } });
22
+ var rate_limit_js_1 = require("./core/rules/rate-limit.js");
23
+ Object.defineProperty(exports, "createRateLimiter", { enumerable: true, get: function () { return rate_limit_js_1.createRateLimiter; } });
24
+ var suspicious_behavior_js_1 = require("./core/rules/suspicious-behavior.js");
25
+ Object.defineProperty(exports, "createSuspiciousBehaviorDetector", { enumerable: true, get: function () { return suspicious_behavior_js_1.createSuspiciousBehaviorDetector; } });
@@ -0,0 +1,2 @@
1
+ import type { SecurityConfig, SecurityWatchMiddleware } from "../types.js";
2
+ export declare function securityWatch(config?: SecurityConfig): SecurityWatchMiddleware;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.securityWatch = securityWatch;
4
+ const engine_js_1 = require("../core/engine.js");
5
+ const memory_js_1 = require("../store/memory.js");
6
+ const alert_js_1 = require("../services/alert.js");
7
+ const logger_js_1 = require("../services/logger.js");
8
+ function getClientIP(req, trustProxy) {
9
+ if (trustProxy) {
10
+ const forwarded = req.headers["x-forwarded-for"];
11
+ if (typeof forwarded === "string")
12
+ return forwarded.split(",")[0].trim();
13
+ }
14
+ return req.socket.remoteAddress || "unknown";
15
+ }
16
+ function extractInput(req) {
17
+ const parts = [];
18
+ if (req.query) {
19
+ for (const value of Object.values(req.query)) {
20
+ if (typeof value === "string")
21
+ parts.push(value);
22
+ }
23
+ }
24
+ if (req.body) {
25
+ if (typeof req.body === "string") {
26
+ parts.push(req.body);
27
+ }
28
+ else if (typeof req.body === "object") {
29
+ try {
30
+ parts.push(JSON.stringify(req.body));
31
+ }
32
+ catch {
33
+ for (const val of Object.values(req.body)) {
34
+ if (typeof val === "string")
35
+ parts.push(val);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ if (req.params) {
41
+ for (const value of Object.values(req.params)) {
42
+ if (typeof value === "string")
43
+ parts.push(value);
44
+ }
45
+ }
46
+ return parts.join(" ");
47
+ }
48
+ function extractHeaders(req) {
49
+ const scannable = ["referer", "user-agent", "cookie", "origin"];
50
+ const parts = [];
51
+ for (const header of scannable) {
52
+ const val = req.headers[header];
53
+ if (typeof val === "string")
54
+ parts.push(val);
55
+ }
56
+ return parts.join(" ");
57
+ }
58
+ function securityWatch(config = {}) {
59
+ const store = new memory_js_1.MemoryStore();
60
+ const engine = new engine_js_1.DetectionEngine(config, store);
61
+ const alertConfig = config.alerts ?? { console: true };
62
+ const alerts = new alert_js_1.AlertService(alertConfig);
63
+ const logEnabled = alertConfig.console !== false;
64
+ const trustProxy = config.trustProxy ?? false;
65
+ const middleware = (req, res, next) => {
66
+ try {
67
+ const ip = getClientIP(req, trustProxy);
68
+ if (engine.isWhitelisted(ip)) {
69
+ next();
70
+ return;
71
+ }
72
+ const input = extractInput(req);
73
+ const headerInput = extractHeaders(req);
74
+ const threat = engine.analyze({
75
+ ip,
76
+ path: req.path,
77
+ method: req.method,
78
+ body: input,
79
+ query: req.url.includes("?") ? req.url.split("?")[1] : undefined,
80
+ headers: headerInput,
81
+ });
82
+ res.on("finish", () => {
83
+ try {
84
+ engine.recordResponse(ip, req.path, res.statusCode);
85
+ }
86
+ catch { }
87
+ });
88
+ if (threat.action !== "allow") {
89
+ if (logEnabled)
90
+ (0, logger_js_1.logThreat)(threat);
91
+ alerts.send(threat);
92
+ }
93
+ switch (threat.action) {
94
+ case "block":
95
+ if (config.onBlock)
96
+ config.onBlock(req, threat);
97
+ res.status(403).json({ error: "Forbidden", message: "Request blocked by SecurityWatch" });
98
+ return;
99
+ case "throttle":
100
+ if (config.onWarn)
101
+ config.onWarn(req, threat);
102
+ res.setHeader("Retry-After", "60");
103
+ res.status(429).json({ error: "Too Many Requests", message: "You are being rate limited" });
104
+ return;
105
+ case "warn":
106
+ if (config.onWarn)
107
+ config.onWarn(req, threat);
108
+ req.securityWatch = threat;
109
+ next();
110
+ return;
111
+ default:
112
+ next();
113
+ }
114
+ }
115
+ catch (err) {
116
+ if (logEnabled)
117
+ console.error("[SecurityWatch] Internal error:", err);
118
+ next();
119
+ }
120
+ };
121
+ middleware.destroy = () => {
122
+ engine.destroy();
123
+ store.destroy();
124
+ };
125
+ return middleware;
126
+ }
@@ -0,0 +1,7 @@
1
+ import type { ThreatInfo, AlertConfig } from "../types.js";
2
+ export declare class AlertService {
3
+ private webhookUrl;
4
+ constructor(config?: AlertConfig);
5
+ send(info: ThreatInfo): void;
6
+ private sendSlack;
7
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AlertService = void 0;
7
+ const https_1 = __importDefault(require("https"));
8
+ const ALLOWED_SLACK_HOSTS = ["hooks.slack.com", "hooks.slack-gov.com"];
9
+ function isAllowedWebhookUrl(urlString) {
10
+ try {
11
+ const url = new URL(urlString);
12
+ if (url.protocol !== "https:")
13
+ return false;
14
+ return ALLOWED_SLACK_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ class AlertService {
21
+ webhookUrl;
22
+ constructor(config = { console: true }) {
23
+ if (config.slackWebhookUrl) {
24
+ if (isAllowedWebhookUrl(config.slackWebhookUrl)) {
25
+ this.webhookUrl = config.slackWebhookUrl;
26
+ }
27
+ else {
28
+ console.warn("[SecurityWatch] Invalid Slack webhook URL — must be HTTPS on hooks.slack.com. Slack alerts disabled.");
29
+ }
30
+ }
31
+ }
32
+ send(info) {
33
+ if (this.webhookUrl && (info.action === "block" || info.action === "throttle")) {
34
+ this.sendSlack(info);
35
+ }
36
+ }
37
+ sendSlack(info) {
38
+ const reasons = info.results.map((r) => `• ${r.reason}`).join("\n");
39
+ const emoji = info.action === "block" ? ":rotating_light:" : ":warning:";
40
+ const payload = JSON.stringify({
41
+ text: `${emoji} *SecurityWatch ${info.action.toUpperCase()}*\n` +
42
+ `*IP:* ${info.ip}\n` +
43
+ `*Request:* ${info.method} ${info.path}\n` +
44
+ `*Score:* ${info.totalScore}\n` +
45
+ `*Reasons:*\n${reasons}\n` +
46
+ `*Time:* ${info.timestamp.toISOString()}`,
47
+ });
48
+ const url = new URL(this.webhookUrl);
49
+ const req = https_1.default.request({
50
+ hostname: url.hostname,
51
+ path: url.pathname,
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ "Content-Length": Buffer.byteLength(payload),
56
+ },
57
+ }, (res) => {
58
+ if (res.statusCode && res.statusCode >= 400) {
59
+ console.warn(`[SecurityWatch] Slack alert failed with status ${res.statusCode}`);
60
+ }
61
+ // Drain the response
62
+ res.resume();
63
+ });
64
+ req.on("error", (err) => {
65
+ console.warn(`[SecurityWatch] Slack alert failed: ${err.message}`);
66
+ });
67
+ req.write(payload);
68
+ req.end();
69
+ }
70
+ }
71
+ exports.AlertService = AlertService;
@@ -0,0 +1,2 @@
1
+ import type { ThreatInfo } from "../types.js";
2
+ export declare function logThreat(info: ThreatInfo): void;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logThreat = logThreat;
4
+ const COLORS = {
5
+ reset: "\x1b[0m",
6
+ red: "\x1b[31m",
7
+ yellow: "\x1b[33m",
8
+ cyan: "\x1b[36m",
9
+ };
10
+ const ACTION_STYLES = {
11
+ block: { color: COLORS.red, label: "BLOCK" },
12
+ throttle: { color: COLORS.yellow, label: "THROTTLE" },
13
+ warn: { color: COLORS.cyan, label: "WARN" },
14
+ };
15
+ function logThreat(info) {
16
+ const style = ACTION_STYLES[info.action];
17
+ if (!style)
18
+ return;
19
+ const reasons = info.results.map((r) => r.reason).join(" | ");
20
+ console.log(`${style.color}[SecurityWatch ${style.label}]${COLORS.reset} ${info.timestamp.toISOString()} ${info.method} ${info.path} from ${info.ip} (score: ${info.totalScore}) — ${reasons}`);
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { Store } from "../types.js";
2
+ export declare class MemoryStore implements Store {
3
+ private data;
4
+ private cleanupInterval;
5
+ constructor(cleanupIntervalMs?: number);
6
+ get<T = unknown>(key: string): T | undefined;
7
+ set<T = unknown>(key: string, value: T, ttlMs: number): void;
8
+ increment(key: string, ttlMs: number): number;
9
+ delete(key: string): void;
10
+ private cleanup;
11
+ destroy(): void;
12
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryStore = void 0;
4
+ class MemoryStore {
5
+ data = new Map();
6
+ cleanupInterval;
7
+ constructor(cleanupIntervalMs = 60_000) {
8
+ this.cleanupInterval = setInterval(() => this.cleanup(), cleanupIntervalMs);
9
+ if (this.cleanupInterval.unref) {
10
+ this.cleanupInterval.unref();
11
+ }
12
+ }
13
+ get(key) {
14
+ const entry = this.data.get(key);
15
+ if (!entry)
16
+ return undefined;
17
+ if (Date.now() > entry.expiresAt) {
18
+ this.data.delete(key);
19
+ return undefined;
20
+ }
21
+ return entry.value;
22
+ }
23
+ set(key, value, ttlMs) {
24
+ this.data.set(key, { value, expiresAt: Date.now() + ttlMs });
25
+ }
26
+ increment(key, ttlMs) {
27
+ const current = this.get(key) ?? 0;
28
+ const next = current + 1;
29
+ this.set(key, next, ttlMs);
30
+ return next;
31
+ }
32
+ delete(key) {
33
+ this.data.delete(key);
34
+ }
35
+ cleanup() {
36
+ const now = Date.now();
37
+ for (const [key, entry] of this.data) {
38
+ if (now > entry.expiresAt) {
39
+ this.data.delete(key);
40
+ }
41
+ }
42
+ }
43
+ destroy() {
44
+ clearInterval(this.cleanupInterval);
45
+ this.data.clear();
46
+ }
47
+ }
48
+ exports.MemoryStore = MemoryStore;
@@ -0,0 +1,75 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ export interface DetectionResult {
3
+ triggered: boolean;
4
+ score: number;
5
+ rule: string;
6
+ reason: string;
7
+ }
8
+ export type Sensitivity = "low" | "medium" | "high" | "critical";
9
+ export type SecurityAction = "allow" | "warn" | "throttle" | "block";
10
+ export interface Thresholds {
11
+ warn: number;
12
+ throttle: number;
13
+ block: number;
14
+ }
15
+ export interface RateLimitConfig {
16
+ windowMs: number;
17
+ maxRequests: number;
18
+ routes?: Record<string, number>;
19
+ }
20
+ export interface BruteForceConfig {
21
+ maxAttempts: number;
22
+ windowMs: number;
23
+ blockDurationMs: number;
24
+ authRoutes?: string[];
25
+ }
26
+ export interface AlertConfig {
27
+ console?: boolean;
28
+ slackWebhookUrl?: string;
29
+ }
30
+ export interface SecurityConfig {
31
+ sqlInjection?: boolean;
32
+ xss?: boolean;
33
+ bruteForce?: boolean | BruteForceConfig;
34
+ rateLimit?: boolean | RateLimitConfig;
35
+ suspiciousBehavior?: boolean;
36
+ payloadAnomaly?: boolean;
37
+ ipReputation?: boolean;
38
+ alerts?: AlertConfig;
39
+ routeSensitivity?: Record<string, Sensitivity>;
40
+ whitelist?: string[];
41
+ trustProxy?: boolean;
42
+ thresholds?: Partial<Thresholds>;
43
+ onBlock?: (req: Request, info: ThreatInfo) => void;
44
+ onWarn?: (req: Request, info: ThreatInfo) => void;
45
+ }
46
+ export interface ThreatInfo {
47
+ action: SecurityAction;
48
+ totalScore: number;
49
+ ip: string;
50
+ path: string;
51
+ method: string;
52
+ results: DetectionResult[];
53
+ timestamp: Date;
54
+ }
55
+ export interface StoreEntry<T = unknown> {
56
+ value: T;
57
+ expiresAt: number;
58
+ }
59
+ export interface Store {
60
+ get<T = unknown>(key: string): T | undefined;
61
+ set<T = unknown>(key: string, value: T, ttlMs: number): void;
62
+ increment(key: string, ttlMs: number): number;
63
+ delete(key: string): void;
64
+ }
65
+ export interface SecurityWatchMiddleware {
66
+ (req: Request, res: Response, next: NextFunction): void;
67
+ destroy: () => void;
68
+ }
69
+ declare global {
70
+ namespace Express {
71
+ interface Request {
72
+ securityWatch?: ThreatInfo;
73
+ }
74
+ }
75
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securitywatch",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Score-based runtime security middleware for Express. SQL injection, XSS, brute force, rate limiting, IP reputation.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",