@wanzofc1/aisyah 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.
- package/index.js +38 -0
- package/package.json +28 -0
- package/package.json.bak +28 -0
- package/readme.md +139 -0
- package/src/authManager.js +101 -0
- package/src/csrf.js +28 -0
- package/src/database.js +48 -0
- package/src/encryption.js +28 -0
- package/src/fileGuard.js +82 -0
- package/src/ipBlocker.js +38 -0
- package/src/network.js +76 -0
- package/src/reporting.js +40 -0
- package/src/sanitizer.js +31 -0
- package/src/twoFactor.js +26 -0
- package/src/validator.js +50 -0
package/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const Encryption = require('./src/encryption');
|
|
2
|
+
const TwoFactor = require('./src/twoFactor');
|
|
3
|
+
const IPBlocker = require('./src/ipBlocker');
|
|
4
|
+
const SecurityReport = require('./src/reporting');
|
|
5
|
+
const NetworkGuard = require('./src/network');
|
|
6
|
+
const Sanitizer = require('./src/sanitizer');
|
|
7
|
+
const AuthManager = require('./src/authManager');
|
|
8
|
+
const CsrfGuard = require('./src/csrf');
|
|
9
|
+
const FileGuard = require('./src/fileGuard');
|
|
10
|
+
const Validator = require('./src/validator');
|
|
11
|
+
const Database = require('./src/database');
|
|
12
|
+
|
|
13
|
+
class Aisyah {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.db = new Database(config.mongoUri);
|
|
17
|
+
if (config.autoConnect !== false) {
|
|
18
|
+
this.db.connect().catch(() => {});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.encryption = new Encryption(config.secretKey || 'default_secret');
|
|
22
|
+
this.twoFactor = new TwoFactor();
|
|
23
|
+
this.ipBlocker = new IPBlocker();
|
|
24
|
+
this.reporting = new SecurityReport(config.logDir);
|
|
25
|
+
this.network = new NetworkGuard();
|
|
26
|
+
this.sanitizer = new Sanitizer();
|
|
27
|
+
this.csrf = new CsrfGuard();
|
|
28
|
+
this.auth = new AuthManager(this.encryption);
|
|
29
|
+
this.fileGuard = new FileGuard({
|
|
30
|
+
maxSize: config.maxUploadSize,
|
|
31
|
+
tempDir: config.tempDir
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.validator = new Validator(this.db);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = Aisyah;
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wanzofc1/aisyah",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Comprehensive security utility with wanzofc integration (Android Support).",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"security",
|
|
11
|
+
"encryption",
|
|
12
|
+
"mongodb",
|
|
13
|
+
"file-security",
|
|
14
|
+
"wanzofc"
|
|
15
|
+
],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"speakeasy": "^2.0.0",
|
|
20
|
+
"qrcode": "^1.5.0",
|
|
21
|
+
"uuid": "^9.0.0",
|
|
22
|
+
"mongoose": "^7.0.0",
|
|
23
|
+
"file-type": "^16.5.4",
|
|
24
|
+
"jimp": "^0.16.1",
|
|
25
|
+
"adm-zip": "^0.5.10",
|
|
26
|
+
"dns": "^0.2.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aisyah",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Comprehensive security utility with wanzofc integration (Android Support).",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"security",
|
|
11
|
+
"encryption",
|
|
12
|
+
"mongodb",
|
|
13
|
+
"file-security",
|
|
14
|
+
"wanzofc"
|
|
15
|
+
],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"speakeasy": "^2.0.0",
|
|
20
|
+
"qrcode": "^1.5.0",
|
|
21
|
+
"uuid": "^9.0.0",
|
|
22
|
+
"mongoose": "^7.0.0",
|
|
23
|
+
"file-type": "^16.5.4",
|
|
24
|
+
"jimp": "^0.16.1",
|
|
25
|
+
"adm-zip": "^0.5.10",
|
|
26
|
+
"dns": "^0.2.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const manual = `
|
|
2
|
+
================================================================
|
|
3
|
+
AISYAH - SECURITY TOOLKIT
|
|
4
|
+
================================================================
|
|
5
|
+
|
|
6
|
+
[ KEGUNAAN ]
|
|
7
|
+
Paket ini adalah "All-in-One Security Suite" untuk aplikasi Node.js.
|
|
8
|
+
Dirancang untuk berjalan di berbagai lingkungan termasuk Android (Termux)
|
|
9
|
+
karena menggunakan library "pure javascript" (Jimp) untuk pemrosesan gambar,
|
|
10
|
+
bukan binary native yang sering bermasalah di mobile.
|
|
11
|
+
|
|
12
|
+
Fitur Utama:
|
|
13
|
+
1. Enkripsi Data & 2FA (Google Authenticator)
|
|
14
|
+
2. Proteksi Jaringan (Rate Limit, Bot Detect, CORS, Proxy Detect)
|
|
15
|
+
3. Keamanan File (MIME Sniffing, EXIF Remover, Zip Bomb Defend)
|
|
16
|
+
4. Sanitasi Input (XSS, SQL Injection)
|
|
17
|
+
5. Manajemen Autentikasi (Password Strength, Session, Magic Link)
|
|
18
|
+
6. Logging & Database (Integrasi MongoDB otomatis)
|
|
19
|
+
|
|
20
|
+
----------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
[ CARA INSTALL ]
|
|
23
|
+
Pastikan Node.js sudah terinstall, lalu jalankan:
|
|
24
|
+
|
|
25
|
+
$ npm install aisyah
|
|
26
|
+
|
|
27
|
+
Dependency yang akan terinstall otomatis:
|
|
28
|
+
- mongoose (Database)
|
|
29
|
+
- jimp (Gambar/Android support)
|
|
30
|
+
- speakeasy, qrcode (2FA)
|
|
31
|
+
- file-type, adm-zip (File checking)
|
|
32
|
+
- uuid (ID generation)
|
|
33
|
+
|
|
34
|
+
----------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
[ CARA PENGGUNAAN (INIT) ]
|
|
37
|
+
Import dan jalankan konfigurasi awal. Pastikan MongoDB local/cloud siap.
|
|
38
|
+
|
|
39
|
+
const Aisyah = require('aisyah');
|
|
40
|
+
|
|
41
|
+
const security = new Aisyah({
|
|
42
|
+
secretKey: 'kunci_rahasia_aplikasi_anda', // Wajib diganti
|
|
43
|
+
mongoUri: 'mongodb://localhost:27017/aisyah_security', // Otomatis connect
|
|
44
|
+
maxUploadSize: 5 * 1024 * 1024, // 5MB
|
|
45
|
+
tempDir: './temp_uploads'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
----------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
[ 1. ENKRIPSI & 2FA ]
|
|
51
|
+
|
|
52
|
+
// Enkripsi String
|
|
53
|
+
const rahasia = security.encryption.encrypt("Data Penting");
|
|
54
|
+
const asli = security.encryption.decrypt(rahasia);
|
|
55
|
+
|
|
56
|
+
// 2 Factor Auth (Google Auth)
|
|
57
|
+
const secret = security.twoFactor.generateSecret();
|
|
58
|
+
const qrCode = await security.twoFactor.generateQRCode(secret.otpauth_url);
|
|
59
|
+
// Verifikasi
|
|
60
|
+
const isValid = security.twoFactor.verifyToken(secret.base32, '123456');
|
|
61
|
+
|
|
62
|
+
----------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
[ 2. NETWORK GUARD (Express/Fastify Middleware) ]
|
|
65
|
+
|
|
66
|
+
// Rate Limiter (Cegah Spam Request)
|
|
67
|
+
// Cek IP pengguna, max 100 request per menit
|
|
68
|
+
if (!security.network.checkRateLimit('192.168.1.1', 100)) {
|
|
69
|
+
return 'Too Many Requests';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Bot Detection
|
|
73
|
+
if (security.network.isBot(req.headers['user-agent'])) {
|
|
74
|
+
return 'Bot Detected';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Secure Headers
|
|
78
|
+
const headers = security.network.getSecureHeaders();
|
|
79
|
+
// Gunakan: res.set(headers);
|
|
80
|
+
|
|
81
|
+
----------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
[ 3. FILE SECURITY (Support Android/Jimp) ]
|
|
84
|
+
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
|
|
87
|
+
// Cek Zip Bomb
|
|
88
|
+
const buffer = fs.readFileSync('upload.zip');
|
|
89
|
+
if (security.fileGuard.checkZipBomb(buffer)) {
|
|
90
|
+
throw new Error('Zip Bomb Detected!');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Bersihkan Metadata Foto (GPS/EXIF) - Aman untuk Android
|
|
94
|
+
const cleanImageBuffer = await security.fileGuard.stripImageMetadata(imageBuffer);
|
|
95
|
+
|
|
96
|
+
// Cek Tipe File Asli (MIME Sniffing)
|
|
97
|
+
const mime = await security.fileGuard.getMimeType(buffer); // ex: 'image/png'
|
|
98
|
+
|
|
99
|
+
----------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
[ 4. INPUT SANITIZER & VALIDATOR ]
|
|
102
|
+
|
|
103
|
+
// XSS & SQL Injection
|
|
104
|
+
const cleanInput = security.sanitizer.xssFilter("<script>alert(1)</script>"); // <script>...
|
|
105
|
+
const isSqli = security.sanitizer.checkSqlInjection("' OR '1'='1"); // true
|
|
106
|
+
|
|
107
|
+
// Email & URL Scanner
|
|
108
|
+
const isEmailValid = await security.validator.validateEmail('test@gmail.com'); // Cek MX Record
|
|
109
|
+
const urlCheck = await security.validator.scanUrl('http://malicious-site.com'); // Cek Blacklist DB
|
|
110
|
+
|
|
111
|
+
----------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
[ 5. AUTH MANAGER ]
|
|
114
|
+
|
|
115
|
+
// Cek Password
|
|
116
|
+
const passCheck = security.auth.checkPasswordStrength('admin123');
|
|
117
|
+
// Output: { score: 2, isStrong: false }
|
|
118
|
+
|
|
119
|
+
// Generate API Key
|
|
120
|
+
const apiKey = security.auth.generateApiKey('sk_prod_');
|
|
121
|
+
|
|
122
|
+
// Session Encryption
|
|
123
|
+
const sessId = security.auth.createSession({ userId: 1, role: 'admin' });
|
|
124
|
+
const sessData = security.auth.getSession(sessId);
|
|
125
|
+
|
|
126
|
+
----------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
[ CATATAN PENTING ]
|
|
129
|
+
Paket ini menggunakan 'mongoose'. Saat di-instantiate:
|
|
130
|
+
new Aisyah({ ... })
|
|
131
|
+
Koneksi ke MongoDB akan berjalan otomatis di background.
|
|
132
|
+
Pastikan service MongoDB (mongod) sudah berjalan.
|
|
133
|
+
|
|
134
|
+
================================================================
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
console.log(manual);
|
|
138
|
+
|
|
139
|
+
module.exports = manual;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
|
|
4
|
+
class AuthManager {
|
|
5
|
+
constructor(encryptionModule) {
|
|
6
|
+
this.enc = encryptionModule;
|
|
7
|
+
this.attempts = new Map();
|
|
8
|
+
this.sessions = new Map();
|
|
9
|
+
this.frozenAccounts = new Set();
|
|
10
|
+
this.disposableDomains = new Set(['tempmail.com', '10minutemail.com', 'mailinator.com', 'yopmail.com']);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
checkPasswordStrength(password) {
|
|
14
|
+
let score = 0;
|
|
15
|
+
if (password.length >= 8) score++;
|
|
16
|
+
if (/[A-Z]/.test(password)) score++;
|
|
17
|
+
if (/[0-9]/.test(password)) score++;
|
|
18
|
+
if (/[^A-Za-z0-9]/.test(password)) score++;
|
|
19
|
+
return { score, isStrong: score >= 4 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
checkBruteForce(identifier, maxAttempts = 5) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const record = this.attempts.get(identifier) || { count: 0, time: now };
|
|
25
|
+
|
|
26
|
+
if (now - record.time > 15 * 60 * 1000) {
|
|
27
|
+
record.count = 0;
|
|
28
|
+
record.time = now;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (record.count >= maxAttempts) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
recordFailedAttempt(identifier) {
|
|
36
|
+
const record = this.attempts.get(identifier) || { count: 0, time: Date.now() };
|
|
37
|
+
record.count++;
|
|
38
|
+
this.attempts.set(identifier, record);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
generateMagicLink(email, baseUrl, secret) {
|
|
42
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
43
|
+
const hash = crypto.createHmac('sha256', secret).update(token).digest('hex');
|
|
44
|
+
const expires = Date.now() + 15 * 60 * 1000;
|
|
45
|
+
return {
|
|
46
|
+
url: `${baseUrl}?token=${token}&verify=${hash}&expires=${expires}`,
|
|
47
|
+
token,
|
|
48
|
+
hash
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isDisposableEmail(email) {
|
|
53
|
+
const domain = email.split('@')[1];
|
|
54
|
+
return this.disposableDomains.has(domain);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
createSession(userData) {
|
|
58
|
+
const sessionId = uuidv4();
|
|
59
|
+
const sessionData = JSON.stringify({ ...userData, createdAt: Date.now() });
|
|
60
|
+
const encrypted = this.enc.encrypt(sessionData);
|
|
61
|
+
this.sessions.set(sessionId, { encrypted, active: true });
|
|
62
|
+
return sessionId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getSession(sessionId) {
|
|
66
|
+
if (!this.sessions.has(sessionId)) return null;
|
|
67
|
+
const session = this.sessions.get(sessionId);
|
|
68
|
+
if (!session.active) return null;
|
|
69
|
+
return JSON.parse(this.enc.decrypt(session.encrypted));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
checkRole(sessionId, requiredRole) {
|
|
73
|
+
const data = this.getSession(sessionId);
|
|
74
|
+
return data && data.role === requiredRole;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
generateApiKey(prefix = 'sk_live_') {
|
|
78
|
+
return prefix + crypto.randomBytes(24).toString('hex');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
generateDeviceFingerprint(headers) {
|
|
82
|
+
const data = `${headers['user-agent']}-${headers['accept-language']}-${headers['sec-ch-ua-platform']}`;
|
|
83
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getActiveSessions() {
|
|
87
|
+
return Array.from(this.sessions.entries())
|
|
88
|
+
.filter(([, v]) => v.active)
|
|
89
|
+
.map(([k]) => k);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
freezeAccount(userId) {
|
|
93
|
+
this.frozenAccounts.add(userId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isFrozen(userId) {
|
|
97
|
+
return this.frozenAccounts.has(userId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = AuthManager;
|
package/src/csrf.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class CsrfGuard {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.tokens = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
generateToken(sessionId) {
|
|
9
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
10
|
+
this.tokens.set(sessionId, {
|
|
11
|
+
token,
|
|
12
|
+
expires: Date.now() + (60 * 60 * 1000)
|
|
13
|
+
});
|
|
14
|
+
return token;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
verifyToken(sessionId, token) {
|
|
18
|
+
const stored = this.tokens.get(sessionId);
|
|
19
|
+
if (!stored) return false;
|
|
20
|
+
if (Date.now() > stored.expires) {
|
|
21
|
+
this.tokens.delete(sessionId);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return stored.token === token;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = CsrfGuard;
|
package/src/database.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const LogSchema = new mongoose.Schema({
|
|
4
|
+
type: String,
|
|
5
|
+
message: String,
|
|
6
|
+
metadata: Object,
|
|
7
|
+
timestamp: { type: Date, default: Date.now }
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const BlacklistSchema = new mongoose.Schema({
|
|
11
|
+
url: String,
|
|
12
|
+
reason: String,
|
|
13
|
+
addedAt: { type: Date, default: Date.now }
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
class Database {
|
|
17
|
+
constructor(uri) {
|
|
18
|
+
this.uri = uri || 'mongodb+srv://maverickuniverse405:1m8MIgmKfK2QwBNe@cluster0.il8d4jx.mongodb.net/aisyah?retryWrites=true&w=majority&appName=Cluster0';
|
|
19
|
+
this.models = {
|
|
20
|
+
Log: mongoose.model('SecurityLog', LogSchema),
|
|
21
|
+
Blacklist: mongoose.model('UrlBlacklist', BlacklistSchema)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async connect() {
|
|
26
|
+
try {
|
|
27
|
+
await mongoose.connect(this.uri, {
|
|
28
|
+
useNewUrlParser: true,
|
|
29
|
+
useUnifiedTopology: true
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new Error('Database connection failed');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async addLog(type, message, metadata) {
|
|
37
|
+
if (mongoose.connection.readyState !== 1) return;
|
|
38
|
+
return await this.models.Log.create({ type, message, metadata });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async isUrlBlacklisted(url) {
|
|
42
|
+
if (mongoose.connection.readyState !== 1) return false;
|
|
43
|
+
const found = await this.models.Blacklist.findOne({ url: url });
|
|
44
|
+
return !!found;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = Database;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const algorithm = 'aes-256-cbc';
|
|
3
|
+
|
|
4
|
+
class Encryption {
|
|
5
|
+
constructor(secretKey) {
|
|
6
|
+
this.key = crypto.createHash('sha256').update(String(secretKey)).digest('base64').substr(0, 32);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
encrypt(text) {
|
|
10
|
+
const iv = crypto.randomBytes(16);
|
|
11
|
+
const cipher = crypto.createCipheriv(algorithm, this.key, iv);
|
|
12
|
+
let encrypted = cipher.update(text);
|
|
13
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
14
|
+
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
decrypt(text) {
|
|
18
|
+
const textParts = text.split(':');
|
|
19
|
+
const iv = Buffer.from(textParts.shift(), 'hex');
|
|
20
|
+
const encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
|
21
|
+
const decipher = crypto.createDecipheriv(algorithm, this.key, iv);
|
|
22
|
+
let decrypted = decipher.update(encryptedText);
|
|
23
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
24
|
+
return decrypted.toString();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = Encryption;
|
package/src/fileGuard.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fileType = require('file-type');
|
|
4
|
+
const Jimp = require('jimp');
|
|
5
|
+
const AdmZip = require('adm-zip');
|
|
6
|
+
|
|
7
|
+
class FileGuard {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.maxSize = config.maxSize || 5 * 1024 * 1024;
|
|
10
|
+
this.tempDir = config.tempDir || './temp';
|
|
11
|
+
if (!fs.existsSync(this.tempDir)) fs.mkdirSync(this.tempDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
this.startTempCleaner();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async validateBuffer(buffer) {
|
|
17
|
+
if (buffer.length > this.maxSize) throw new Error('File exceeds maximum allowed size');
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getMimeType(buffer) {
|
|
22
|
+
const type = await fileType.fromBuffer(buffer);
|
|
23
|
+
return type ? type.mime : 'unknown';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
sanitizeFilename(filename) {
|
|
27
|
+
return filename.replace(/[^a-z0-9.]/gi, '_').replace(/_{2,}/g, '_');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
checkDirectoryTraversal(targetPath, rootDir) {
|
|
31
|
+
const resolved = path.resolve(rootDir, targetPath);
|
|
32
|
+
return resolved.startsWith(path.resolve(rootDir));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async stripImageMetadata(buffer) {
|
|
36
|
+
try {
|
|
37
|
+
const image = await Jimp.read(buffer);
|
|
38
|
+
return await image.getBufferAsync(Jimp.AUTO);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return buffer;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
checkZipBomb(buffer) {
|
|
45
|
+
try {
|
|
46
|
+
const zip = new AdmZip(buffer);
|
|
47
|
+
const entries = zip.getEntries();
|
|
48
|
+
let totalSize = 0;
|
|
49
|
+
const compressedSize = buffer.length;
|
|
50
|
+
|
|
51
|
+
entries.forEach(entry => {
|
|
52
|
+
totalSize += entry.header.size;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const ratio = totalSize / compressedSize;
|
|
56
|
+
if (ratio > 50 || totalSize > 1024 * 1024 * 1024) return true;
|
|
57
|
+
return false;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
startTempCleaner() {
|
|
64
|
+
setInterval(() => {
|
|
65
|
+
fs.readdir(this.tempDir, (err, files) => {
|
|
66
|
+
if (err) return;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
files.forEach(file => {
|
|
69
|
+
const filePath = path.join(this.tempDir, file);
|
|
70
|
+
fs.stat(filePath, (err, stats) => {
|
|
71
|
+
if (err) return;
|
|
72
|
+
if (now - stats.mtimeMs > 60 * 60 * 1000) {
|
|
73
|
+
fs.unlink(filePath, () => {});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}, 30 * 60 * 1000);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = FileGuard;
|
package/src/ipBlocker.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class IPBlocker {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.blacklistedIPs = new Set();
|
|
4
|
+
this.suspiciousActivityThreshold = 5;
|
|
5
|
+
this.requestCounts = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
blockIP(ip) {
|
|
9
|
+
this.blacklistedIPs.add(ip);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
unblockIP(ip) {
|
|
13
|
+
this.blacklistedIPs.delete(ip);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
isBlocked(ip) {
|
|
17
|
+
return this.blacklistedIPs.has(ip);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
trackActivity(ip) {
|
|
21
|
+
if (this.isBlocked(ip)) return false;
|
|
22
|
+
|
|
23
|
+
const count = (this.requestCounts.get(ip) || 0) + 1;
|
|
24
|
+
this.requestCounts.set(ip, count);
|
|
25
|
+
|
|
26
|
+
if (count > this.suspiciousActivityThreshold) {
|
|
27
|
+
this.blockIP(ip);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
resetActivity(ip) {
|
|
34
|
+
this.requestCounts.delete(ip);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = IPBlocker;
|
package/src/network.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class NetworkGuard {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.globalHits = new Map();
|
|
4
|
+
this.routeHits = new Map();
|
|
5
|
+
this.windowMs = 60 * 1000;
|
|
6
|
+
this.commonBot patterns = /bot|crawl|spider|slurp|facebook|whatsapp/i;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
checkRateLimit(key, limit, type = 'global') {
|
|
10
|
+
const storage = type === 'global' ? this.globalHits : this.routeHits;
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
|
|
13
|
+
if (!storage.has(key)) {
|
|
14
|
+
storage.set(key, { count: 1, startTime: now });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const data = storage.get(key);
|
|
19
|
+
if (now - data.startTime > this.windowMs) {
|
|
20
|
+
data.count = 1;
|
|
21
|
+
data.startTime = now;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
data.count++;
|
|
26
|
+
return data.count <= limit;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getCorsHeaders(allowedOrigin = '*') {
|
|
30
|
+
return {
|
|
31
|
+
'Access-Control-Allow-Origin': allowedOrigin,
|
|
32
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
33
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
|
|
34
|
+
'Access-Control-Allow-Credentials': 'true'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSecureHeaders() {
|
|
39
|
+
return {
|
|
40
|
+
'X-DNS-Prefetch-Control': 'off',
|
|
41
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
42
|
+
'Strict-Transport-Security': 'max-age=15552000; includeSubDomains',
|
|
43
|
+
'X-Download-Options': 'noopen',
|
|
44
|
+
'X-Content-Type-Options': 'nosniff',
|
|
45
|
+
'X-XSS-Protection': '1; mode=block'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isBot(userAgent) {
|
|
50
|
+
return !userAgent || this.commonBot.test(userAgent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
validateReferer(referer, allowedDomain) {
|
|
54
|
+
if (!referer) return false;
|
|
55
|
+
try {
|
|
56
|
+
const refUrl = new URL(referer);
|
|
57
|
+
return refUrl.hostname === allowedDomain;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
detectProxy(headers) {
|
|
64
|
+
const proxyHeaders = [
|
|
65
|
+
'via',
|
|
66
|
+
'x-forwarded-for',
|
|
67
|
+
'forwarded',
|
|
68
|
+
'proxy-connection',
|
|
69
|
+
'x-proxy-id',
|
|
70
|
+
'from'
|
|
71
|
+
];
|
|
72
|
+
return proxyHeaders.some(h => headers[h]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = NetworkGuard;
|
package/src/reporting.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class SecurityReport {
|
|
5
|
+
constructor(logDir = './logs') {
|
|
6
|
+
this.logDir = logDir;
|
|
7
|
+
if (!fs.existsSync(this.logDir)) {
|
|
8
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
log(type, message, metadata = {}) {
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
const logEntry = {
|
|
15
|
+
timestamp,
|
|
16
|
+
type: type.toUpperCase(),
|
|
17
|
+
message,
|
|
18
|
+
metadata
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const fileName = `security-report-${new Date().toISOString().split('T')[0]}.json`;
|
|
22
|
+
const filePath = path.join(this.logDir, fileName);
|
|
23
|
+
|
|
24
|
+
fs.appendFile(filePath, JSON.stringify(logEntry) + '\n', (err) => {
|
|
25
|
+
if (err) console.error('Failed to write security log', err);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
generateSummary(date) {
|
|
30
|
+
const fileName = `security-report-${date}.json`;
|
|
31
|
+
const filePath = path.join(this.logDir, fileName);
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(filePath)) return [];
|
|
34
|
+
|
|
35
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
36
|
+
return data.trim().split('\n').map(line => JSON.parse(line));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = SecurityReport;
|
package/src/sanitizer.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Sanitizer {
|
|
2
|
+
xssFilter(input) {
|
|
3
|
+
if (typeof input !== 'string') return input;
|
|
4
|
+
return input
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''')
|
|
10
|
+
.replace(/\//g, '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
checkSqlInjection(input) {
|
|
14
|
+
if (typeof input !== 'string') return false;
|
|
15
|
+
const pattern = /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|EXEC|TRUNCATE)\b)|(--)|(;)/i;
|
|
16
|
+
return pattern.test(input);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
sanitizeObject(obj) {
|
|
20
|
+
for (const key in obj) {
|
|
21
|
+
if (typeof obj[key] === 'string') {
|
|
22
|
+
obj[key] = this.xssFilter(obj[key]);
|
|
23
|
+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
24
|
+
this.sanitizeObject(obj[key]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return obj;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = Sanitizer;
|
package/src/twoFactor.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const speakeasy = require('speakeasy');
|
|
2
|
+
const qrcode = require('qrcode');
|
|
3
|
+
|
|
4
|
+
class TwoFactor {
|
|
5
|
+
generateSecret(name = 'AisyahApp') {
|
|
6
|
+
return speakeasy.generateSecret({ length: 20, name: name });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
verifyToken(secret, token) {
|
|
10
|
+
return speakeasy.totp.verify({
|
|
11
|
+
secret: secret,
|
|
12
|
+
encoding: 'base32',
|
|
13
|
+
token: token
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async generateQRCode(otpAuthUrl) {
|
|
18
|
+
try {
|
|
19
|
+
return await qrcode.toDataURL(otpAuthUrl);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = TwoFactor;
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const dns = require('dns');
|
|
2
|
+
|
|
3
|
+
class Validator {
|
|
4
|
+
constructor(databaseModule) {
|
|
5
|
+
this.db = databaseModule;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
validateSchema(object, schema) {
|
|
9
|
+
for (const key in schema) {
|
|
10
|
+
if (!object.hasOwnProperty(key)) return false;
|
|
11
|
+
|
|
12
|
+
const expectedType = schema[key];
|
|
13
|
+
const actualValue = object[key];
|
|
14
|
+
|
|
15
|
+
if (typeof expectedType === 'object' && expectedType !== null) {
|
|
16
|
+
if (typeof actualValue !== 'object' || !this.validateSchema(actualValue, expectedType)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
} else if (typeof actualValue !== expectedType) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async validateEmail(email) {
|
|
27
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
28
|
+
if (!emailRegex.test(email)) return false;
|
|
29
|
+
|
|
30
|
+
const domain = email.split('@')[1];
|
|
31
|
+
return new Promise(resolve => {
|
|
32
|
+
dns.resolveMx(domain, (err, addresses) => {
|
|
33
|
+
resolve(!err && addresses && addresses.length > 0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async scanUrl(url) {
|
|
39
|
+
if (!this.db) return false;
|
|
40
|
+
try {
|
|
41
|
+
const urlObj = new URL(url);
|
|
42
|
+
const isBlacklisted = await this.db.isUrlBlacklisted(urlObj.hostname);
|
|
43
|
+
return { safe: !isBlacklisted, hostname: urlObj.hostname };
|
|
44
|
+
} catch {
|
|
45
|
+
return { safe: false, error: 'Invalid URL' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = Validator;
|