@hadi_ali/warden 1.0.0

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.
@@ -0,0 +1,61 @@
1
+ async function upsertReputation(db, { ip_hash, score, path, action, reason }) {
2
+ if (!db || typeof db.query !== 'function') return null;
3
+
4
+ const query = `
5
+ WITH upserted AS (
6
+ INSERT INTO warden_reputation (ip_hash, score, last_seen)
7
+ VALUES ($1, $2, NOW())
8
+ ON CONFLICT (ip_hash)
9
+ DO UPDATE SET
10
+ score = GREATEST(0, LEAST(100,
11
+ FLOOR(
12
+ warden_reputation.score
13
+ * POWER(
14
+ 0.95,
15
+ EXTRACT(EPOCH FROM (
16
+ NOW() - COALESCE(warden_reputation.last_seen, NOW())
17
+ )) / 3600.0
18
+ )
19
+ ) + EXCLUDED.score
20
+ )),
21
+ last_seen = NOW()
22
+ RETURNING score
23
+ )
24
+ INSERT INTO warden_requests (ip_hash, timestamp, path, score_delta, action, reason)
25
+ VALUES ($1, NOW(), $3, $2, $4, $5)
26
+ RETURNING
27
+ (SELECT score FROM upserted) AS new_score;
28
+ `;
29
+
30
+ const result = await db.query(query, [ip_hash, score, path, action, reason]);
31
+ return result.rows[0];
32
+ }
33
+
34
+ async function upsertSession(db, session_hash, score) {
35
+ if (!db || typeof db.query !== 'function') return null;
36
+
37
+ const query = `
38
+ INSERT INTO warden_sessions (session_hash, score, last_seen)
39
+ VALUES ($1, $2, NOW())
40
+ ON CONFLICT (session_hash)
41
+ DO UPDATE SET
42
+ score = GREATEST(0, LEAST(100,
43
+ FLOOR(
44
+ warden_sessions.score
45
+ * POWER(
46
+ 0.95,
47
+ EXTRACT(EPOCH FROM (
48
+ NOW() - COALESCE(warden_sessions.last_seen, NOW())
49
+ )) / 3600.0
50
+ )
51
+ ) + EXCLUDED.score
52
+ )),
53
+ last_seen = NOW()
54
+ RETURNING score AS new_score
55
+ `;
56
+
57
+ const result = await db.query(query, [session_hash, score]);
58
+ return result.rows[0];
59
+ }
60
+
61
+ module.exports = { upsertReputation, upsertSession };
@@ -0,0 +1,56 @@
1
+ const express = require('express');
2
+ const { pool } = require('../db_connection/db');
3
+ const { createClient } = require('redis');
4
+ const { warden } = require('../middleware/warden');
5
+ const { syncVerifiedBots } = require('../verified-bots/sync');
6
+ const { reloadRanges } = require('../verified-bots/matcher');
7
+
8
+ // Example/demo server wiring up Warden with PostgreSQL + Redis.
9
+ // Run with: CONNECTION_STRING=postgres://... node node_modules/warden/app/demo/server.js
10
+ async function main() {
11
+ const app = express();
12
+ app.set('trust proxy', 1);
13
+
14
+ let redis = null;
15
+ try {
16
+ redis = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });
17
+ redis.on('error', (err) => console.error('[Redis]', err.message));
18
+ await redis.connect();
19
+ } catch (err) {
20
+ console.warn('[Redis] not available, falling back to in-memory store');
21
+ redis = null;
22
+ }
23
+
24
+ app.use(warden({
25
+ db: pool,
26
+ redis,
27
+ allowlist: [],
28
+ failopen: true,
29
+ scorethreshold: 30,
30
+ }));
31
+
32
+ app.get('/', (req, res) => {
33
+ res.json({ message: 'hello' });
34
+ });
35
+
36
+ try {
37
+ const success = await syncVerifiedBots();
38
+ if (success) reloadRanges();
39
+ setInterval(async () => {
40
+ const refreshed = await syncVerifiedBots();
41
+ if (refreshed) reloadRanges();
42
+ }, 24 * 60 * 60 * 1000).unref();
43
+ } catch (err) {
44
+ console.warn('[verified-bots sync] skipped:', err.message);
45
+ }
46
+
47
+ const port = process.env.PORT || 3000;
48
+ app.listen(port, () => {
49
+ console.log(`Server running on port ${port}`);
50
+ });
51
+ }
52
+
53
+ main().catch((err) => {
54
+ console.error('Fatal startup error:', err);
55
+ process.exit(1);
56
+ });
@@ -0,0 +1,51 @@
1
+ function pickFirst(list) {
2
+ if (!list) return null;
3
+ const parts = String(list).split(',');
4
+ for (const part of parts) {
5
+ const trimmed = part.trim();
6
+ if (trimmed) return trimmed;
7
+ }
8
+ return null;
9
+ }
10
+
11
+ /**
12
+ * Resolve the client IP for a request.
13
+ *
14
+ * Security: by default we trust ONLY req.ip, which Express populates
15
+ * from req.socket.remoteAddress unless app.set('trust proxy', N) is configured.
16
+ * Manually parsing X-Forwarded-For / X-Real-IP / CF-Connecting-IP would let
17
+ * attackers spoof IPs (e.g. "X-Forwarded-For: 8.8.8.8" via Postman) and
18
+ * bypass rate limits or get legitimate services banned.
19
+ *
20
+ * Pass trustHeaders: true ONLY if you've already configured
21
+ * app.set('trust proxy', N) with a specific hop count. Warden will not
22
+ * validate that configuration for you.
23
+ */
24
+ function getClientIP(req, { trustHeaders = false } = {}) {
25
+ if (!req) return null;
26
+
27
+ if (trustHeaders) {
28
+ const headers = req.headers || {};
29
+
30
+ const xff = pickFirst(headers['x-forwarded-for']);
31
+ if (xff) return xff;
32
+
33
+ const xri = pickFirst(headers['x-real-ip']);
34
+ if (xri) return xri;
35
+
36
+ const cf = pickFirst(headers['cf-connecting-ip']);
37
+ if (cf) return cf;
38
+ }
39
+
40
+ if (typeof req.ip === 'string' && req.ip.trim()) {
41
+ return req.ip.trim();
42
+ }
43
+
44
+ if (req.socket && typeof req.socket.remoteAddress === 'string') {
45
+ return req.socket.remoteAddress.trim();
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ module.exports = { getClientIP };
package/app/hashIP.js ADDED
@@ -0,0 +1,7 @@
1
+ const crypto = require('crypto');
2
+
3
+ function hashIP(ip) {
4
+ return crypto.createHash('sha256').update(ip).digest('hex');
5
+ }
6
+
7
+ module.exports = { hashIP };
@@ -0,0 +1,29 @@
1
+ function checkHoneypot(req) {
2
+ const honeypots = [
3
+ 'wp-admin', 'wp-login.php', 'wp-config.php',
4
+ 'phpmyadmin', 'pma',
5
+ 'shell.php', 'cmd.php', 'exec.php', 'backdoor', 'webshell',
6
+ 'phpinfo', 'phpinfo.php',
7
+ 'env', 'git', 'htaccess', '.env', '.git', '.htaccess',
8
+ 'config.php', 'web.config',
9
+ ];
10
+
11
+ const path = req.path.toLowerCase().replace(/\/+$/, '');
12
+
13
+ for (const honeypot of honeypots) {
14
+ if (path === `/${honeypot}` || path === honeypot) {
15
+ return 30;
16
+ }
17
+ }
18
+
19
+ const pathSegments = path.split('/').filter(Boolean);
20
+ for (const segment of pathSegments) {
21
+ if (honeypots.includes(segment)) {
22
+ return 30;
23
+ }
24
+ }
25
+
26
+ return 0;
27
+ }
28
+
29
+ module.exports = { checkHoneypot };
@@ -0,0 +1,276 @@
1
+ const { computeScore } = require('../scoring/computeScore');
2
+ const { checkAllowlist } = require('../allowlist');
3
+ const { checkHoneypot } = require('../honeypot');
4
+ const { upsertReputation, upsertSession } = require('../db_connection/queries');
5
+ const { getLimit, isRateLimited, DEFAULTS } = require('../ratelimit');
6
+ const { log } = require('../Loger');
7
+ const { trackFailure } = require('../bruteforce');
8
+ const crypto = require('crypto');
9
+ const { computeSessionScore } = require('../scoring/computeSessionScore');
10
+ const { isVerifiedBot } = require('../verified-bots/matcher');
11
+ const { store } = require('../store');
12
+ const verifiedBotsSync = require('../verified-bots/sync');
13
+
14
+ function logInternalError(message, err, meta = {}) {
15
+ const details = {
16
+ ...meta,
17
+ error: err instanceof Error ? err.message : String(err),
18
+ };
19
+
20
+ if (err instanceof Error && err.stack) {
21
+ details.stack = err.stack;
22
+ }
23
+
24
+ console.error(`[warden] ${message}:`, details);
25
+ }
26
+
27
+ function safeCall(hookName, fn, ...args) {
28
+ if (typeof hookName === 'function') {
29
+ args = [fn, ...args];
30
+ fn = hookName;
31
+ hookName = 'anonymous';
32
+ }
33
+
34
+ if (typeof fn !== 'function') return;
35
+
36
+ try {
37
+ const result = fn(...args);
38
+ if (result && typeof result.then === 'function') {
39
+ return result.catch((err) => {
40
+ logInternalError(`hook "${hookName}" failed`, err);
41
+ });
42
+ }
43
+
44
+ return result;
45
+ } catch (err) {
46
+ logInternalError(`hook "${hookName}" failed`, err);
47
+ }
48
+ }
49
+
50
+ function getSafeTier(score, limits) {
51
+ const tier = getLimit(score, limits) || getLimit(score, DEFAULTS) || DEFAULTS.clean;
52
+
53
+ if (
54
+ !tier ||
55
+ !Number.isFinite(tier.requests) ||
56
+ !Number.isFinite(tier.windowSeconds) ||
57
+ tier.requests <= 0 ||
58
+ tier.windowSeconds <= 0
59
+ ) {
60
+ return DEFAULTS.clean;
61
+ }
62
+
63
+ return tier;
64
+ }
65
+
66
+ async function withFallback(taskName, task, fallbackValue, meta = {}) {
67
+ try {
68
+ return await task();
69
+ } catch (err) {
70
+ logInternalError(taskName, err, meta);
71
+ return fallbackValue;
72
+ }
73
+ }
74
+
75
+ function setHeaderSafe(res, name, value) {
76
+ if (!res || res.headersSent) return;
77
+ if (typeof res.set === 'function') {
78
+ res.set(name, value);
79
+ return;
80
+ }
81
+
82
+ if (typeof res.setHeader === 'function') {
83
+ res.setHeader(name, value);
84
+ }
85
+ }
86
+
87
+ function replySafe(res, statusCode, body) {
88
+ if (!res || res.headersSent) return;
89
+
90
+ if (typeof res.status === 'function') {
91
+ res.status(statusCode);
92
+ } else {
93
+ res.statusCode = statusCode;
94
+ }
95
+
96
+ if (typeof res.json === 'function') {
97
+ return res.json(body);
98
+ }
99
+
100
+ if (typeof res.send === 'function') {
101
+ return res.send(body);
102
+ }
103
+
104
+ if (typeof res.end === 'function') {
105
+ const payload = typeof body === 'string' ? body : JSON.stringify(body);
106
+ return res.end(payload);
107
+ }
108
+ }
109
+
110
+ function warden({
111
+ db = null,
112
+ redis = null,
113
+ allowlist = [],
114
+ failopen = true,
115
+ scorethreshold = 30,
116
+ limits = {},
117
+ onBlock = null,
118
+ knownSubdomains = [],
119
+ onSuspicious = null,
120
+ onRateLimit = null,
121
+ suspiciousThreshold = 30,
122
+ trustHeaders = false,
123
+ getSessionId = null,
124
+ } = {}) {
125
+ const BRUTETHRESHOLD=7
126
+ const mergedLimits = {
127
+ clean: { ...DEFAULTS.clean, ...(limits.clean || {}) },
128
+ suspicious: { ...DEFAULTS.suspicious, ...(limits.suspicious || {}) },
129
+ hostile: { ...DEFAULTS.hostile, ...(limits.hostile || {}) },
130
+ };
131
+
132
+ if (redis && typeof redis === 'object' && typeof store.useRedis === 'function') {
133
+ store.useRedis(redis);
134
+ }
135
+
136
+ if (redis && typeof redis.set === 'function') {
137
+ verifiedBotsSync.setExternalStore(redis);
138
+ } else if (db && typeof db.query === 'function') {
139
+ verifiedBotsSync.setExternalStore(db);
140
+ }
141
+
142
+ return async function(req, res, next) {
143
+ const reqId = crypto.randomUUID();
144
+ const start = Date.now();
145
+ const path = req?.path || req?.originalUrl || req?.url || 'unknown';
146
+
147
+ try {
148
+ if (checkAllowlist(req, allowlist)) return next();
149
+
150
+ const computed = computeScore(req, knownSubdomains, { trustHeaders }) || {};
151
+ const ip_hash = typeof computed.ip_hash === 'string' && computed.ip_hash ? computed.ip_hash : 'unknown';
152
+ const score = Number.isFinite(computed.score) ? computed.score : 0;
153
+ const raw_ip = computed.raw_ip;
154
+
155
+ if (raw_ip && isVerifiedBot(raw_ip)) return next();
156
+ const honeypotScore = checkHoneypot(req);
157
+ const totalScore = score + honeypotScore;
158
+ const action = totalScore >= scorethreshold ? 'blocked' : 'allowed';
159
+ const reason = honeypotScore > 0 ? 'honeypot' : 'score';
160
+
161
+ if (typeof res?.on === 'function') {
162
+ res.on('finish', () => {
163
+ (async () => {
164
+ try {
165
+ if (![400, 401, 404].includes(res.statusCode)) return;
166
+ if (res.statusCode === 404) {
167
+ const isStaticAsset = /\.(png|jpg|jpeg|gif|ico|css|js|woff|woff2|ttf|svg|map)$/i.test(path);
168
+ if (isStaticAsset) return; // Let it go, it's just a broken webpage
169
+ }
170
+
171
+ const count = await withFallback(
172
+ 'post-response brute-force tracking failed',
173
+ () => trackFailure(ip_hash),
174
+ 0,
175
+ { reqId, ip_hash, path }
176
+ );
177
+
178
+ if (count < BRUTETHRESHOLD) return;
179
+
180
+ await withFallback(
181
+ 'post-response brute-force persistence failed',
182
+ () => upsertReputation(db, {
183
+ ip_hash,
184
+ score: 100,
185
+ path,
186
+ action: 'blocked',
187
+ reason: 'bruteforce'
188
+ }),
189
+ null,
190
+ { reqId, ip_hash, path }
191
+ );
192
+ } catch (err) {
193
+ logInternalError('post-response handler crashed', err, { reqId, ip_hash, path });
194
+ }
195
+ })();
196
+ });
197
+ }
198
+
199
+ const session_hash = getSessionId ? computeSessionScore(req, getSessionId) : null;
200
+
201
+ const shouldPersistScore = totalScore > 0;
202
+ const [reputationResult, sessionResult] = await Promise.all([
203
+ shouldPersistScore
204
+ ? withFallback(
205
+ 'reputation persistence failed',
206
+ () => upsertReputation(db, {
207
+ ip_hash,
208
+ score: totalScore,
209
+ path,
210
+ action,
211
+ reason,
212
+ }),
213
+ null,
214
+ { reqId, ip_hash, path }
215
+ )
216
+ : Promise.resolve(null),
217
+ shouldPersistScore && session_hash
218
+ ? withFallback(
219
+ 'session persistence failed',
220
+ () => upsertSession(db, session_hash, totalScore),
221
+ null,
222
+ { reqId, ip_hash, path, session_hash }
223
+ )
224
+ : Promise.resolve(null)
225
+ ]);
226
+
227
+ const new_score = Number.isFinite(reputationResult?.new_score)
228
+ ? reputationResult.new_score
229
+ : Math.max(0, totalScore);
230
+ const session_new_score = Number.isFinite(sessionResult?.new_score)
231
+ ? sessionResult.new_score
232
+ : null;
233
+ const finalScore = session_new_score !== null
234
+ ? Math.max(new_score, session_new_score)
235
+ : new_score;
236
+ const tier = getSafeTier(finalScore, mergedLimits);
237
+ const rateLimitKey = `rl:${ip_hash}:${tier.windowSeconds}:${tier.requests}`;
238
+ const window_requests = await withFallback(
239
+ 'rate limit store increment failed',
240
+ () => store.increment(rateLimitKey, tier.windowSeconds),
241
+ 0,
242
+ { reqId, ip_hash, path, rateLimitKey }
243
+ );
244
+ setHeaderSafe(res, 'X-RateLimit-Limit', tier.requests);
245
+ setHeaderSafe(res, 'X-RateLimit-Remaining', Math.max(0, tier.requests - window_requests));
246
+ setHeaderSafe(res, 'X-RateLimit-Reset', Math.floor(Date.now() / 1000) + tier.windowSeconds);
247
+
248
+ if (isRateLimited(finalScore, window_requests, mergedLimits)) {
249
+ setHeaderSafe(res, 'Retry-After', tier.windowSeconds);
250
+ log({ reqId, ip_hash, score: finalScore, raw_score: totalScore, action: 'blocked', reason: 'rate_limit', path, duration_ms: Date.now() - start });
251
+ safeCall('onRateLimit', onRateLimit, ip_hash, finalScore, req);
252
+ return replySafe(res, 429, { error: 'Too many requests' });
253
+ }
254
+
255
+ if (finalScore >= scorethreshold) {
256
+ log({ reqId, ip_hash, score: finalScore, raw_score: totalScore, action: 'blocked', reason, path, duration_ms: Date.now() - start });
257
+ safeCall('onBlock', onBlock, ip_hash, finalScore, reason, req);
258
+ return replySafe(res, 403, { error: 'forbidden' });
259
+ }
260
+
261
+ log({ reqId, ip_hash, score: finalScore, raw_score: totalScore, action: 'allowed', reason, path, duration_ms: Date.now() - start });
262
+ if (finalScore > suspiciousThreshold) {
263
+ safeCall('onSuspicious', onSuspicious, ip_hash, finalScore, req);
264
+ }
265
+ return next();
266
+
267
+ } catch (err) {
268
+ logInternalError('middleware execution failed', err, { reqId, path });
269
+ if (failopen === true) return next();
270
+ if (res?.headersSent && typeof next === 'function') return next(err);
271
+ return replySafe(res, 500, { error: 'internal server error' });
272
+ }
273
+ };
274
+ }
275
+
276
+ module.exports = { warden, safeCall, logInternalError, replySafe, setHeaderSafe, getSafeTier };
@@ -0,0 +1,18 @@
1
+ const { Address6 } = require('ip-address');
2
+ const net = require('net');
3
+
4
+ function normalizeIP(input) {
5
+ if (!input) return null;
6
+
7
+ const ipVersion = net.isIP(input);
8
+ if (ipVersion === 4) {
9
+ return input;
10
+ } else if (ipVersion === 6) {
11
+ const addr = new Address6(input);
12
+ const sections = addr.canonicalForm().split(':');
13
+ return sections.slice(0, 4).join(':') + '::/64';
14
+ }
15
+ return null;
16
+ }
17
+
18
+ module.exports = { normalizeIP };
@@ -0,0 +1,17 @@
1
+
2
+ const DEFAULTS = {
3
+ clean: { requests: 60, windowSeconds: 60 },
4
+ suspicious: { requests: 20, windowSeconds: 60 },
5
+ hostile: { requests: 5, windowSeconds: 30 }
6
+ };
7
+
8
+ function getLimit(score, limits=DEFAULTS) {
9
+ if (score > 60) return limits.hostile;
10
+ if (score > 30) return limits.suspicious;
11
+ return limits.clean;
12
+ }
13
+
14
+ function isRateLimited(score, windowRequests, customLimits = {}) {
15
+ return windowRequests > getLimit(score, customLimits).requests;
16
+ }
17
+ module.exports={getLimit,isRateLimited,DEFAULTS}
@@ -0,0 +1,90 @@
1
+ function scoreSubdomain(req, knownSubdomains = []) {
2
+ const host = (req.headers?.host || req.hostname || '').toLowerCase();
3
+ if (!host || !host.includes('.')) return 0;
4
+
5
+ const cleanHost = host.split(':')[0];
6
+ const parts = cleanHost.split('.');
7
+ if (parts.length < 3) return 0;
8
+
9
+ const subdomains = parts.slice(0, -2);
10
+ const whitelist = new Set(knownSubdomains.map(s => s.toLowerCase()));
11
+
12
+ // 60 points — no legitimate user hits these, pure recon/attack surface
13
+ const highSensitive = new Set([
14
+ // admin panels
15
+ 'admin', 'administrator', 'panel', 'control', 'manage', 'manager',
16
+ 'phpmyadmin', 'pma', 'adminer', 'myadmin', 'dbadmin', 'sqladmin',
17
+ 'cpanel', 'whm', 'plesk', 'directadmin', 'virtualmin', 'webmin',
18
+ // remote access
19
+ 'ssh', 'rdp', 'vnc', 'telnet', 'bastion', 'console', 'remote', 'access',
20
+ // secrets
21
+ 'vault', 'secrets', 'cred', 'credentials', 'password', 'passwords',
22
+ // backups
23
+ 'backup', 'bak', 'old', 'restore', 'archive',
24
+ // debug
25
+ 'phpinfo', 'debug', 'debugger', 'devmode',
26
+ // database admin
27
+ 'dba',
28
+ // shell
29
+ 'shell', 'cmd', 'exec',
30
+ // internal
31
+ 'intranet', 'internal', 'inside', 'private', 'secret', 'confidential',
32
+ 'corp', 'corporate',
33
+ // vpn/tunnel
34
+ 'vpn', 'tunnel', 'gateway',
35
+ ]);
36
+
37
+ // 15 points — common but signals reconnaissance
38
+ const medSensitive = new Set([
39
+ // dev environments
40
+ 'dev', 'development', 'staging', 'stage', 'test', 'testing',
41
+ 'beta', 'alpha', 'demo', 'uat', 'qa', 'sandbox', 'mock',
42
+ 'preprod', 'pre-prod', 'preview',
43
+ // versioning
44
+ 'v1', 'v2', 'v3', 'v4', 'version',
45
+ // apis
46
+ 'api', 'graphql', 'gql', 'rest', 'soap', 'rpc',
47
+ 'swagger', 'openapi', 'api-docs', 'playground',
48
+ // ci/cd
49
+ 'jenkins', 'ci', 'cd', 'build', 'deploy', 'release', 'pipeline',
50
+ 'travis', 'circleci', 'github', 'gitlab', 'bitbucket', 'git', 'svn',
51
+ // monitoring
52
+ 'grafana', 'prometheus', 'kibana', 'monitor', 'monitoring',
53
+ 'nagios', 'zabbix', 'datadog', 'newrelic', 'splunk',
54
+ 'logs', 'log', 'logging', 'trace', 'tracing', 'metrics',
55
+ 'jaeger', 'zipkin', 'otel',
56
+ // databases
57
+ 'db', 'database', 'sql', 'mysql', 'postgres', 'postgresql',
58
+ 'mongo', 'mongodb', 'redis', 'cassandra', 'oracle', 'elastic',
59
+ // auth
60
+ 'auth', 'sso', 'oauth', 'saml', 'ldap', 'login', 'signin',
61
+ // infra
62
+ 'k8s', 'kubernetes', 'kube', 'docker', 'rancher', 'portainer',
63
+ 'consul', 'nomad',
64
+ // mail
65
+ 'mail', 'email', 'webmail', 'smtp',
66
+ // docs
67
+ 'wiki', 'docs', 'confluence', 'jira',
68
+ // misc
69
+ 'status', 'health', 'ping', 'probe',
70
+ 'ws', 'socket', 'realtime',
71
+ ]);
72
+
73
+ let score = 0;
74
+
75
+ for (const sub of subdomains) {
76
+ if (whitelist.has(sub)) continue;
77
+ if (highSensitive.has(sub)) {
78
+ score += 60;
79
+ continue;
80
+ }
81
+ if (medSensitive.has(sub)) {
82
+ score += 15;
83
+ continue;
84
+ }
85
+ }
86
+
87
+ return Math.min(score, 100);
88
+ }
89
+
90
+ module.exports = { scoreSubdomain };
@@ -0,0 +1,28 @@
1
+ const { getClientIP } = require('../getClientIP');
2
+ const { hashIP } = require('../hashIP');
3
+ const { normalizeIP } = require('../normalizeIP');
4
+ const { scoreFromHeaders } = require('./scoreFromHeaders');
5
+ const {scoreSubdomain}=require('./SubDomainScore')
6
+ function computeScore(req, knownSubdomains, options = {}) {
7
+ const ip = getClientIP(req, options);
8
+ const normalip = normalizeIP(ip);
9
+
10
+ if (!normalip) {
11
+ return {
12
+ ip_hash: 'unknown',
13
+ score: 60,
14
+ raw_ip: null
15
+ };
16
+ }
17
+
18
+ const haship = hashIP(normalip);
19
+ const headerScore = scoreFromHeaders(req);
20
+ const scoreSub = scoreSubdomain(req,knownSubdomains)
21
+ return {
22
+ ip_hash: haship,
23
+ score: headerScore+scoreSub,
24
+ raw_ip: normalip
25
+ };
26
+ }
27
+
28
+ module.exports = { computeScore };
@@ -0,0 +1,52 @@
1
+ const crypto = require('crypto');
2
+
3
+ function hashSession(session) {
4
+ return crypto.createHash('sha256').update(String(session)).digest('hex');
5
+ }
6
+
7
+ function extractBearerToken(req) {
8
+ const auth = req.headers?.authorization
9
+ if (!auth) return null;
10
+ const parts = String(auth).split(' ');
11
+ if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') {
12
+ return parts[1];
13
+ }
14
+ return null;
15
+ }
16
+
17
+ function extractCookie(req, name) {
18
+ if (req.cookies && req.cookies[name]) {
19
+ return req.cookies[name];
20
+ }
21
+ if (req.headers?.cookie) {
22
+ const match = req.headers.cookie.match(
23
+ new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
24
+ );
25
+ if (match) return match[1];
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function getDefaultSessionId(req) {
31
+ // 1. Try connect.sid cookie first
32
+ const sid = extractCookie(req, 'connect.sid');
33
+ if (sid) return sid;
34
+
35
+ // 2. Try Authorization bearer second
36
+ const bearer = extractBearerToken(req);
37
+ if (bearer) return bearer;
38
+
39
+ return null;
40
+ }
41
+
42
+ function computeSessionScore(req, getSessionId) {
43
+ const sessionId = typeof getSessionId === 'function'
44
+ ? getSessionId(req)
45
+ : getDefaultSessionId(req);
46
+
47
+ if (!sessionId) return null;
48
+
49
+ return hashSession(sessionId);
50
+ }
51
+
52
+ module.exports = { computeSessionScore, hashSession };