stegdoc 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/LICENSE +21 -0
- package/README.md +214 -0
- package/bootstrap.js +33 -0
- package/package.json +60 -0
- package/src/commands/decode.js +201 -0
- package/src/commands/encode.js +346 -0
- package/src/commands/info.js +113 -0
- package/src/commands/verify.js +169 -0
- package/src/index.js +87 -0
- package/src/lib/compression.js +97 -0
- package/src/lib/crypto.js +118 -0
- package/src/lib/decoy-generator.js +306 -0
- package/src/lib/docx-handler.js +161 -0
- package/src/lib/file-handler.js +113 -0
- package/src/lib/file-utils.js +150 -0
- package/src/lib/interactive.js +190 -0
- package/src/lib/metadata.js +111 -0
- package/src/lib/utils.js +227 -0
- package/src/lib/xlsx-handler.js +359 -0
- package/src/lib/xml-utils.js +115 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const encodeCommand = require('./commands/encode');
|
|
6
|
+
const decodeCommand = require('./commands/decode');
|
|
7
|
+
const infoCommand = require('./commands/info');
|
|
8
|
+
const verifyCommand = require('./commands/verify');
|
|
9
|
+
|
|
10
|
+
// CLI Configuration
|
|
11
|
+
program
|
|
12
|
+
.name('stegdoc')
|
|
13
|
+
.description('Hide files inside Office documents with AES-256 encryption and steganography')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
// Encode command
|
|
17
|
+
program
|
|
18
|
+
.command('encode <file>')
|
|
19
|
+
.description('Encode a file into XLSX/DOCX format with compression and optional encryption')
|
|
20
|
+
.option('-o, --output-dir <dir>', 'Output directory for files', process.cwd())
|
|
21
|
+
.option('-s, --chunk-size <size>', 'Maximum size per output file (e.g., "5MB", "25MB")', '5MB')
|
|
22
|
+
.option('-f, --format <format>', 'Output format: xlsx (default) or docx', 'xlsx')
|
|
23
|
+
.option('-p, --password <password>', 'Encryption password (optional, but recommended)')
|
|
24
|
+
.option('--force', 'Overwrite existing files without asking')
|
|
25
|
+
.option('-q, --quiet', 'Minimal output (for scripting)')
|
|
26
|
+
.option('-y, --yes', 'Skip interactive prompts, use defaults')
|
|
27
|
+
.action(async (file, options) => {
|
|
28
|
+
try {
|
|
29
|
+
await encodeCommand(file, options);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Decode command
|
|
37
|
+
program
|
|
38
|
+
.command('decode <file>')
|
|
39
|
+
.description('Decode and decrypt an XLSX/DOCX file back to original format')
|
|
40
|
+
.option('-o, --output <path>', 'Output file path (defaults to original filename)')
|
|
41
|
+
.option('-p, --password <password>', 'Decryption password (required for encrypted files)')
|
|
42
|
+
.option('--force', 'Overwrite existing files without asking')
|
|
43
|
+
.option('-q, --quiet', 'Minimal output (for scripting)')
|
|
44
|
+
.option('-y, --yes', 'Skip interactive prompts, fail if password needed')
|
|
45
|
+
.action(async (file, options) => {
|
|
46
|
+
try {
|
|
47
|
+
await decodeCommand(file, options);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Info command
|
|
55
|
+
program
|
|
56
|
+
.command('info <file>')
|
|
57
|
+
.description('Show information about an encoded file without decoding')
|
|
58
|
+
.action(async (file, options) => {
|
|
59
|
+
try {
|
|
60
|
+
await infoCommand(file, options);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Verify command
|
|
68
|
+
program
|
|
69
|
+
.command('verify <file>')
|
|
70
|
+
.description('Verify that a file can be decoded without actually decoding')
|
|
71
|
+
.option('-p, --password <password>', 'Password to verify (optional)')
|
|
72
|
+
.action(async (file, options) => {
|
|
73
|
+
try {
|
|
74
|
+
await verifyCommand(file, options);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Show help by default if no command is provided
|
|
82
|
+
if (!process.argv.slice(2).length) {
|
|
83
|
+
program.outputHelp();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse arguments
|
|
87
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MIME types that are already compressed - no benefit from additional compression
|
|
5
|
+
*/
|
|
6
|
+
const COMPRESSED_MIMES = new Set([
|
|
7
|
+
// Archives
|
|
8
|
+
'application/zip',
|
|
9
|
+
'application/x-7z-compressed',
|
|
10
|
+
'application/x-rar-compressed',
|
|
11
|
+
'application/gzip',
|
|
12
|
+
'application/x-gzip',
|
|
13
|
+
'application/x-bzip2',
|
|
14
|
+
'application/x-xz',
|
|
15
|
+
'application/x-tar',
|
|
16
|
+
'application/x-lzip',
|
|
17
|
+
'application/x-lzma',
|
|
18
|
+
'application/zstd',
|
|
19
|
+
|
|
20
|
+
// Images (lossy compressed)
|
|
21
|
+
'image/jpeg',
|
|
22
|
+
'image/png',
|
|
23
|
+
'image/gif',
|
|
24
|
+
'image/webp',
|
|
25
|
+
'image/avif',
|
|
26
|
+
'image/heic',
|
|
27
|
+
'image/heif',
|
|
28
|
+
'image/jxl',
|
|
29
|
+
|
|
30
|
+
// Audio
|
|
31
|
+
'audio/mpeg', // mp3
|
|
32
|
+
'audio/ogg',
|
|
33
|
+
'audio/flac',
|
|
34
|
+
'audio/aac',
|
|
35
|
+
'audio/mp4',
|
|
36
|
+
'audio/x-m4a',
|
|
37
|
+
'audio/opus',
|
|
38
|
+
|
|
39
|
+
// Video
|
|
40
|
+
'video/mp4',
|
|
41
|
+
'video/webm',
|
|
42
|
+
'video/x-matroska', // mkv
|
|
43
|
+
'video/quicktime', // mov
|
|
44
|
+
'video/x-msvideo', // avi
|
|
45
|
+
'video/mpeg',
|
|
46
|
+
|
|
47
|
+
// Documents (OOXML are zip-based)
|
|
48
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
|
|
49
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
|
|
50
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // pptx
|
|
51
|
+
'application/pdf',
|
|
52
|
+
'application/epub+zip',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a file type is already compressed
|
|
57
|
+
* @param {string|null} mime - MIME type from file-type detection
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function isCompressedMime(mime) {
|
|
61
|
+
return mime ? COMPRESSED_MIMES.has(mime) : false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compress data using gzip
|
|
66
|
+
* @param {Buffer} buffer - Data to compress
|
|
67
|
+
* @returns {Promise<Buffer>} Compressed data
|
|
68
|
+
*/
|
|
69
|
+
function compress(buffer) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
zlib.gzip(buffer, { level: 9 }, (err, result) => {
|
|
72
|
+
if (err) reject(err);
|
|
73
|
+
else resolve(result);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decompress gzip data
|
|
80
|
+
* @param {Buffer} buffer - Compressed data
|
|
81
|
+
* @returns {Promise<Buffer>} Decompressed data
|
|
82
|
+
*/
|
|
83
|
+
function decompress(buffer) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
zlib.gunzip(buffer, (err, result) => {
|
|
86
|
+
if (err) reject(err);
|
|
87
|
+
else resolve(result);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
isCompressedMime,
|
|
94
|
+
compress,
|
|
95
|
+
decompress,
|
|
96
|
+
COMPRESSED_MIMES,
|
|
97
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
// AES-256-GCM Configuration
|
|
4
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
5
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
6
|
+
const IV_LENGTH = 12; // 96 bits (recommended for GCM)
|
|
7
|
+
const SALT_LENGTH = 16; // 128 bits
|
|
8
|
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
9
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Derive a key from password using PBKDF2
|
|
13
|
+
* @param {string} password - User password
|
|
14
|
+
* @param {Buffer} salt - Salt for key derivation
|
|
15
|
+
* @returns {Buffer} Derived key
|
|
16
|
+
*/
|
|
17
|
+
function deriveKey(password, salt) {
|
|
18
|
+
return crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Encrypt plaintext using AES-256-GCM
|
|
23
|
+
* @param {string} plaintext - Data to encrypt
|
|
24
|
+
* @param {string} password - User password
|
|
25
|
+
* @returns {object} { ciphertext, iv, salt, authTag } - all as base64 strings
|
|
26
|
+
*/
|
|
27
|
+
function encrypt(plaintext, password) {
|
|
28
|
+
// Generate random salt and IV
|
|
29
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
30
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
31
|
+
|
|
32
|
+
// Derive key from password
|
|
33
|
+
const key = deriveKey(password, salt);
|
|
34
|
+
|
|
35
|
+
// Create cipher and encrypt
|
|
36
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
37
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
|
|
41
|
+
ciphertext += cipher.final('base64');
|
|
42
|
+
|
|
43
|
+
// Get authentication tag
|
|
44
|
+
const authTag = cipher.getAuthTag();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
ciphertext,
|
|
48
|
+
iv: iv.toString('base64'),
|
|
49
|
+
salt: salt.toString('base64'),
|
|
50
|
+
authTag: authTag.toString('base64'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt ciphertext using AES-256-GCM
|
|
56
|
+
* @param {string} ciphertext - Encrypted data (base64)
|
|
57
|
+
* @param {string} password - User password
|
|
58
|
+
* @param {string} ivBase64 - Initialization vector (base64)
|
|
59
|
+
* @param {string} saltBase64 - Salt used for key derivation (base64)
|
|
60
|
+
* @param {string} authTagBase64 - Authentication tag (base64)
|
|
61
|
+
* @returns {string} Decrypted plaintext
|
|
62
|
+
* @throws {Error} If decryption fails (wrong password or tampered data)
|
|
63
|
+
*/
|
|
64
|
+
function decrypt(ciphertext, password, ivBase64, saltBase64, authTagBase64) {
|
|
65
|
+
// Convert from base64
|
|
66
|
+
const iv = Buffer.from(ivBase64, 'base64');
|
|
67
|
+
const salt = Buffer.from(saltBase64, 'base64');
|
|
68
|
+
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
69
|
+
|
|
70
|
+
// Derive key from password
|
|
71
|
+
const key = deriveKey(password, salt);
|
|
72
|
+
|
|
73
|
+
// Create decipher
|
|
74
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
75
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Set auth tag for verification
|
|
79
|
+
decipher.setAuthTag(authTag);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
|
|
83
|
+
plaintext += decipher.final('utf8');
|
|
84
|
+
return plaintext;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error('Decryption failed: Invalid password or corrupted data');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pack encryption metadata into a single string for storage
|
|
92
|
+
* @param {object} encryptionData - { iv, salt, authTag }
|
|
93
|
+
* @returns {string} Packed string (iv:salt:authTag in base64)
|
|
94
|
+
*/
|
|
95
|
+
function packEncryptionMeta(encryptionData) {
|
|
96
|
+
return `${encryptionData.iv}:${encryptionData.salt}:${encryptionData.authTag}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Unpack encryption metadata from storage string
|
|
101
|
+
* @param {string} packed - Packed string (iv:salt:authTag)
|
|
102
|
+
* @returns {object} { iv, salt, authTag }
|
|
103
|
+
*/
|
|
104
|
+
function unpackEncryptionMeta(packed) {
|
|
105
|
+
const [iv, salt, authTag] = packed.split(':');
|
|
106
|
+
if (!iv || !salt || !authTag) {
|
|
107
|
+
throw new Error('Invalid encryption metadata format');
|
|
108
|
+
}
|
|
109
|
+
return { iv, salt, authTag };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
encrypt,
|
|
114
|
+
decrypt,
|
|
115
|
+
deriveKey,
|
|
116
|
+
packEncryptionMeta,
|
|
117
|
+
unpackEncryptionMeta,
|
|
118
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decoy Data Generator - Server Metrics Report
|
|
3
|
+
* Generates realistic-looking server monitoring/metrics data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Fixed list of servers (small dev team setup - ~10 servers total)
|
|
7
|
+
const SERVERS = [
|
|
8
|
+
'app-prod-01',
|
|
9
|
+
'app-prod-02',
|
|
10
|
+
'api-prod-01',
|
|
11
|
+
'db-prod-01',
|
|
12
|
+
'redis-prod-01',
|
|
13
|
+
'nginx-prod-01',
|
|
14
|
+
'app-dev-01',
|
|
15
|
+
'db-dev-01',
|
|
16
|
+
'jenkins-01',
|
|
17
|
+
'gitlab-01',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const STATUS_VALUES = [
|
|
21
|
+
{ status: 'healthy', weight: 85 },
|
|
22
|
+
{ status: 'warning', weight: 10 },
|
|
23
|
+
{ status: 'critical', weight: 3 },
|
|
24
|
+
{ status: 'maintenance', weight: 2 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a random float between min and max with specified decimal places
|
|
29
|
+
*/
|
|
30
|
+
function randomFloat(min, max, decimals = 1) {
|
|
31
|
+
const val = Math.random() * (max - min) + min;
|
|
32
|
+
return parseFloat(val.toFixed(decimals));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate a random integer between min and max (inclusive)
|
|
37
|
+
*/
|
|
38
|
+
function randomInt(min, max) {
|
|
39
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a random element from an array
|
|
44
|
+
*/
|
|
45
|
+
function randomElement(arr) {
|
|
46
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate a weighted random status
|
|
51
|
+
*/
|
|
52
|
+
function generateStatus() {
|
|
53
|
+
const totalWeight = STATUS_VALUES.reduce((sum, s) => sum + s.weight, 0);
|
|
54
|
+
let random = Math.random() * totalWeight;
|
|
55
|
+
|
|
56
|
+
for (const item of STATUS_VALUES) {
|
|
57
|
+
random -= item.weight;
|
|
58
|
+
if (random <= 0) {
|
|
59
|
+
return item.status;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 'healthy';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pick a server from the fixed list
|
|
67
|
+
*/
|
|
68
|
+
function generateServerId() {
|
|
69
|
+
return randomElement(SERVERS);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Time window configuration (last 4 hours of data)
|
|
73
|
+
const HOURS_WINDOW = 4;
|
|
74
|
+
|
|
75
|
+
// Shared time window for multi-part consistency
|
|
76
|
+
let sharedTimeWindow = null;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize or get the shared time window
|
|
80
|
+
* This ensures multi-part files have continuous timestamps
|
|
81
|
+
*/
|
|
82
|
+
function getTimeWindow() {
|
|
83
|
+
if (!sharedTimeWindow) {
|
|
84
|
+
const now = new Date();
|
|
85
|
+
sharedTimeWindow = {
|
|
86
|
+
end: now.getTime(),
|
|
87
|
+
start: now.getTime() - (HOURS_WINDOW * 60 * 60 * 1000),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return sharedTimeWindow;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reset the time window (called between separate encode sessions)
|
|
95
|
+
*/
|
|
96
|
+
function resetTimeWindow() {
|
|
97
|
+
sharedTimeWindow = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate a realistic timestamp within the configured time window
|
|
102
|
+
*/
|
|
103
|
+
function generateTimestamp() {
|
|
104
|
+
const window = getTimeWindow();
|
|
105
|
+
const timestamp = new Date(window.start + Math.random() * (window.end - window.start));
|
|
106
|
+
|
|
107
|
+
// Format: YYYY-MM-DD HH:MM:SS
|
|
108
|
+
const year = timestamp.getFullYear();
|
|
109
|
+
const month = (timestamp.getMonth() + 1).toString().padStart(2, '0');
|
|
110
|
+
const day = timestamp.getDate().toString().padStart(2, '0');
|
|
111
|
+
const hours = timestamp.getHours().toString().padStart(2, '0');
|
|
112
|
+
const minutes = timestamp.getMinutes().toString().padStart(2, '0');
|
|
113
|
+
const seconds = timestamp.getSeconds().toString().padStart(2, '0');
|
|
114
|
+
|
|
115
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate CPU usage based on status
|
|
120
|
+
*/
|
|
121
|
+
function generateCpuUsage(status) {
|
|
122
|
+
switch (status) {
|
|
123
|
+
case 'critical':
|
|
124
|
+
return randomFloat(90, 99);
|
|
125
|
+
case 'warning':
|
|
126
|
+
return randomFloat(70, 89);
|
|
127
|
+
case 'maintenance':
|
|
128
|
+
return randomFloat(0, 10);
|
|
129
|
+
default:
|
|
130
|
+
return randomFloat(15, 69);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate memory usage based on status
|
|
136
|
+
*/
|
|
137
|
+
function generateMemoryUsage(status) {
|
|
138
|
+
switch (status) {
|
|
139
|
+
case 'critical':
|
|
140
|
+
return randomFloat(92, 99);
|
|
141
|
+
case 'warning':
|
|
142
|
+
return randomFloat(75, 91);
|
|
143
|
+
case 'maintenance':
|
|
144
|
+
return randomFloat(5, 20);
|
|
145
|
+
default:
|
|
146
|
+
return randomFloat(30, 74);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate disk usage (generally more stable than CPU/memory)
|
|
152
|
+
*/
|
|
153
|
+
function generateDiskUsage(status) {
|
|
154
|
+
switch (status) {
|
|
155
|
+
case 'critical':
|
|
156
|
+
return randomFloat(95, 99);
|
|
157
|
+
case 'warning':
|
|
158
|
+
return randomFloat(80, 94);
|
|
159
|
+
default:
|
|
160
|
+
return randomFloat(25, 79);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generate network I/O in MB/s
|
|
166
|
+
*/
|
|
167
|
+
function generateNetworkIO() {
|
|
168
|
+
return randomFloat(0.5, 150, 2);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate request count (for web/api servers)
|
|
173
|
+
*/
|
|
174
|
+
function generateRequestCount(serverId) {
|
|
175
|
+
// Higher for app/api/nginx servers
|
|
176
|
+
if (serverId.match(/^(app|api|nginx)/)) {
|
|
177
|
+
return randomInt(500, 15000);
|
|
178
|
+
}
|
|
179
|
+
// Low for CI/CD servers
|
|
180
|
+
if (serverId.match(/^(jenkins|gitlab)/)) {
|
|
181
|
+
return randomInt(10, 200);
|
|
182
|
+
}
|
|
183
|
+
return randomInt(50, 1000);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generate response time in ms
|
|
188
|
+
*/
|
|
189
|
+
function generateResponseTime(status) {
|
|
190
|
+
switch (status) {
|
|
191
|
+
case 'critical':
|
|
192
|
+
return randomInt(2000, 10000);
|
|
193
|
+
case 'warning':
|
|
194
|
+
return randomInt(500, 1999);
|
|
195
|
+
default:
|
|
196
|
+
return randomInt(5, 499);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate uptime in hours
|
|
202
|
+
*/
|
|
203
|
+
function generateUptime(status) {
|
|
204
|
+
if (status === 'maintenance') {
|
|
205
|
+
return randomFloat(0, 2, 1);
|
|
206
|
+
}
|
|
207
|
+
// Servers typically have long uptimes
|
|
208
|
+
return randomFloat(24, 2160, 1); // Up to 90 days
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate column headers
|
|
213
|
+
*/
|
|
214
|
+
function generateDecoyHeaders() {
|
|
215
|
+
return [
|
|
216
|
+
'Timestamp',
|
|
217
|
+
'Server ID',
|
|
218
|
+
'Status',
|
|
219
|
+
'CPU %',
|
|
220
|
+
'Memory %',
|
|
221
|
+
'Disk %',
|
|
222
|
+
'Network (MB/s)',
|
|
223
|
+
'Requests',
|
|
224
|
+
'Resp Time (ms)',
|
|
225
|
+
'Uptime (hrs)',
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Generate a single row of server metrics
|
|
231
|
+
*/
|
|
232
|
+
function generateMetricsRow() {
|
|
233
|
+
const serverId = generateServerId();
|
|
234
|
+
const status = generateStatus();
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
timestamp: generateTimestamp(),
|
|
238
|
+
serverId,
|
|
239
|
+
status,
|
|
240
|
+
cpu: generateCpuUsage(status),
|
|
241
|
+
memory: generateMemoryUsage(status),
|
|
242
|
+
disk: generateDiskUsage(status),
|
|
243
|
+
network: generateNetworkIO(),
|
|
244
|
+
requests: generateRequestCount(serverId),
|
|
245
|
+
responseTime: generateResponseTime(status),
|
|
246
|
+
uptime: generateUptime(status),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Generate decoy data with specified number of rows
|
|
252
|
+
* @param {number} rowCount - Number of rows to generate (default 100)
|
|
253
|
+
* @returns {Array<object>} Array of metrics row objects
|
|
254
|
+
*/
|
|
255
|
+
function generateDecoyData(rowCount = 100) {
|
|
256
|
+
const rows = [];
|
|
257
|
+
for (let i = 0; i < rowCount; i++) {
|
|
258
|
+
rows.push(generateMetricsRow());
|
|
259
|
+
}
|
|
260
|
+
// Sort by timestamp descending (most recent first)
|
|
261
|
+
rows.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
262
|
+
return rows;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Calculate appropriate row count based on payload size
|
|
267
|
+
* @param {number} payloadSizeBytes - Size of encrypted payload
|
|
268
|
+
* @returns {number} Recommended number of decoy rows
|
|
269
|
+
*/
|
|
270
|
+
function calculateDecoyRowCount(payloadSizeBytes) {
|
|
271
|
+
const estimatedBytesPerRow = 150;
|
|
272
|
+
const targetDecoyRatio = 0.4;
|
|
273
|
+
|
|
274
|
+
const minRows = 50;
|
|
275
|
+
const maxRows = 10000;
|
|
276
|
+
|
|
277
|
+
const calculatedRows = Math.floor((payloadSizeBytes * targetDecoyRatio) / estimatedBytesPerRow);
|
|
278
|
+
|
|
279
|
+
return Math.max(minRows, Math.min(maxRows, calculatedRows));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Convert row object to array of values (for Excel)
|
|
284
|
+
*/
|
|
285
|
+
function rowToArray(row) {
|
|
286
|
+
return [
|
|
287
|
+
row.timestamp,
|
|
288
|
+
row.serverId,
|
|
289
|
+
row.status,
|
|
290
|
+
row.cpu,
|
|
291
|
+
row.memory,
|
|
292
|
+
row.disk,
|
|
293
|
+
row.network,
|
|
294
|
+
row.requests,
|
|
295
|
+
row.responseTime,
|
|
296
|
+
row.uptime,
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
generateDecoyHeaders,
|
|
302
|
+
generateDecoyData,
|
|
303
|
+
calculateDecoyRowCount,
|
|
304
|
+
rowToArray,
|
|
305
|
+
resetTimeWindow,
|
|
306
|
+
};
|