stegdoc 4.0.0 → 5.0.1
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 -21
- package/README.md +200 -214
- package/package.json +59 -59
- package/src/commands/decode.js +485 -343
- package/src/commands/encode.js +567 -449
- package/src/commands/info.js +118 -114
- package/src/commands/verify.js +207 -204
- package/src/index.js +89 -87
- package/src/lib/compression.js +177 -115
- package/src/lib/crypto.js +172 -172
- package/src/lib/decoy-generator.js +306 -306
- package/src/lib/docx-handler.js +587 -161
- package/src/lib/docx-templates.js +355 -0
- package/src/lib/file-handler.js +113 -113
- package/src/lib/file-utils.js +160 -150
- package/src/lib/interactive.js +190 -190
- package/src/lib/log-generator.js +764 -0
- package/src/lib/metadata.js +151 -122
- package/src/lib/streams.js +197 -197
- package/src/lib/utils.js +227 -227
- package/src/lib/xlsx-handler.js +597 -416
- package/src/lib/xml-utils.js +115 -115
package/src/lib/xlsx-handler.js
CHANGED
|
@@ -1,416 +1,597 @@
|
|
|
1
|
-
const ExcelJS = require('exceljs');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const AdmZip = require('adm-zip');
|
|
5
|
-
const { generateDecoyHeaders, generateDecoyData, calculateDecoyRowCount, rowToArray, resetTimeWindow } = require('./decoy-generator');
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
workbook
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ width:
|
|
56
|
-
{ width:
|
|
57
|
-
{ width:
|
|
58
|
-
{ width:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
*
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
1
|
+
const ExcelJS = require('exceljs');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const AdmZip = require('adm-zip');
|
|
5
|
+
const { generateDecoyHeaders, generateDecoyData, calculateDecoyRowCount, rowToArray, resetTimeWindow } = require('./decoy-generator');
|
|
6
|
+
const { generateLogHeaders, encodePayloadToLogLines, decodeLogLines, resetTimeState } = require('./log-generator');
|
|
7
|
+
const { parseXmlFromZip, ensureArray, extractTextContent } = require('./xml-utils');
|
|
8
|
+
|
|
9
|
+
// Constants for data storage
|
|
10
|
+
const HIDDEN_SHEET_NAME = 'Data';
|
|
11
|
+
const VISIBLE_SHEET_NAME = 'Server Metrics';
|
|
12
|
+
const V5_SHEET_NAME = 'Access Logs';
|
|
13
|
+
const CELL_CHUNK_SIZE = 32000; // Max characters per cell (~32KB)
|
|
14
|
+
|
|
15
|
+
// ─── v5 Log-Embed Format ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create an XLSX file using v5 log-embed format (streaming).
|
|
19
|
+
* Single sheet "Access Logs" with payload embedded in log line fields.
|
|
20
|
+
* No hidden sheets.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} options
|
|
23
|
+
* @param {Buffer} options.payloadBuffer - Encrypted binary payload
|
|
24
|
+
* @param {string} options.encryptionMeta - Packed encryption metadata (iv:salt:authTag) or ''
|
|
25
|
+
* @param {string} options.metadataJson - Serialized metadata JSON string
|
|
26
|
+
* @param {string} options.outputPath - Output file path
|
|
27
|
+
* @returns {Promise<string>} Path to created file
|
|
28
|
+
*/
|
|
29
|
+
async function createXlsxPartV5(options) {
|
|
30
|
+
const { payloadBuffer, encryptionMeta, metadataJson, outputPath } = options;
|
|
31
|
+
|
|
32
|
+
// Ensure output directory exists
|
|
33
|
+
const outputDir = path.dirname(outputPath);
|
|
34
|
+
if (!fs.existsSync(outputDir)) {
|
|
35
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
39
|
+
filename: outputPath,
|
|
40
|
+
useSharedStrings: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
workbook.creator = 'Microsoft Excel';
|
|
44
|
+
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
45
|
+
workbook.created = new Date();
|
|
46
|
+
workbook.modified = new Date();
|
|
47
|
+
|
|
48
|
+
// === Single Sheet: Access Logs ===
|
|
49
|
+
const sheet = workbook.addWorksheet(V5_SHEET_NAME, {
|
|
50
|
+
properties: { tabColor: { argb: '2F5496' } },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Column widths for log data
|
|
54
|
+
sheet.columns = [
|
|
55
|
+
{ width: 16 }, // Remote Address
|
|
56
|
+
{ width: 28 }, // Timestamp
|
|
57
|
+
{ width: 8 }, // Method
|
|
58
|
+
{ width: 90 }, // Request (contains URL with payload)
|
|
59
|
+
{ width: 7 }, // Status
|
|
60
|
+
{ width: 8 }, // Bytes
|
|
61
|
+
{ width: 65 }, // Referer
|
|
62
|
+
{ width: 85 }, // User-Agent
|
|
63
|
+
{ width: 38 }, // X-Request-ID
|
|
64
|
+
{ width: 34 }, // X-Trace-ID
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Header row with styling
|
|
68
|
+
const headers = generateLogHeaders();
|
|
69
|
+
const headerRow = sheet.addRow(headers);
|
|
70
|
+
for (let col = 1; col <= headers.length; col++) {
|
|
71
|
+
const cell = headerRow.getCell(col);
|
|
72
|
+
cell.font = { bold: true, size: 10, color: { argb: 'FFFFFF' }, name: 'Consolas' };
|
|
73
|
+
cell.fill = {
|
|
74
|
+
type: 'pattern',
|
|
75
|
+
pattern: 'solid',
|
|
76
|
+
fgColor: { argb: '2F5496' },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
headerRow.commit();
|
|
80
|
+
|
|
81
|
+
// Generate all log lines (header + data + filler)
|
|
82
|
+
const { headerRows, dataRows, fillerRows } = encodePayloadToLogLines(
|
|
83
|
+
payloadBuffer, metadataJson, encryptionMeta
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Write header lines (metadata)
|
|
87
|
+
for (const row of headerRows) {
|
|
88
|
+
const r = sheet.addRow(row);
|
|
89
|
+
r.commit();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write data lines (payload)
|
|
93
|
+
for (const row of dataRows) {
|
|
94
|
+
const r = sheet.addRow(row);
|
|
95
|
+
r.commit();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Write filler lines (realistic padding)
|
|
99
|
+
for (const row of fillerRows) {
|
|
100
|
+
const r = sheet.addRow(row);
|
|
101
|
+
r.commit();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const totalRows = headerRows.length + dataRows.length + fillerRows.length;
|
|
105
|
+
|
|
106
|
+
// Add filters
|
|
107
|
+
sheet.autoFilter = {
|
|
108
|
+
from: 'A1',
|
|
109
|
+
to: `J${totalRows + 1}`,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await sheet.commit();
|
|
113
|
+
await workbook.commit();
|
|
114
|
+
|
|
115
|
+
return outputPath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read a v5 log-embed XLSX file and extract payload.
|
|
120
|
+
* @param {string} xlsxPath - Path to XLSX file
|
|
121
|
+
* @returns {object} { payloadBuffer, metadataJson, encryptionMeta, metadata }
|
|
122
|
+
*/
|
|
123
|
+
async function readXlsxV5(xlsxPath) {
|
|
124
|
+
if (!fs.existsSync(xlsxPath)) {
|
|
125
|
+
throw new Error(`XLSX file not found: ${xlsxPath}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse shared strings
|
|
129
|
+
let sharedStrings = [];
|
|
130
|
+
const ssParsed = parseXmlFromZip(xlsxPath, 'xl/sharedStrings.xml');
|
|
131
|
+
if (ssParsed && ssParsed.sst && ssParsed.sst.si) {
|
|
132
|
+
const siArray = ensureArray(ssParsed.sst.si);
|
|
133
|
+
sharedStrings = siArray.map((si) => extractTextContent(si.t));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Parse sheet1 (the only sheet in v5)
|
|
137
|
+
const sheetParsed = parseXmlFromZip(xlsxPath, 'xl/worksheets/sheet1.xml');
|
|
138
|
+
if (!sheetParsed) {
|
|
139
|
+
throw new Error('Sheet not found in XLSX file.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Extract all rows as arrays of cell values
|
|
143
|
+
const allRows = [];
|
|
144
|
+
const sheetData = sheetParsed.worksheet?.sheetData;
|
|
145
|
+
|
|
146
|
+
if (sheetData && sheetData.row) {
|
|
147
|
+
const rows = ensureArray(sheetData.row);
|
|
148
|
+
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
if (!row.c) continue;
|
|
151
|
+
const cells = ensureArray(row.c);
|
|
152
|
+
|
|
153
|
+
// Build a sparse array for this row
|
|
154
|
+
const rowValues = [];
|
|
155
|
+
for (const cell of cells) {
|
|
156
|
+
const cellRef = cell['@_r'];
|
|
157
|
+
if (!cellRef) continue;
|
|
158
|
+
|
|
159
|
+
// Parse column index from cell reference (e.g., "A2" -> col 0, "J2" -> col 9)
|
|
160
|
+
const colMatch = cellRef.match(/^([A-Z]+)/);
|
|
161
|
+
if (!colMatch) continue;
|
|
162
|
+
const colIdx = colLetterToIndex(colMatch[1]);
|
|
163
|
+
|
|
164
|
+
const cellType = cell['@_t'];
|
|
165
|
+
const cellValue = cell.v;
|
|
166
|
+
|
|
167
|
+
let value;
|
|
168
|
+
if (cellType === 's' && cellValue !== undefined) {
|
|
169
|
+
const ssIndex = parseInt(cellValue, 10);
|
|
170
|
+
value = ssIndex < sharedStrings.length ? sharedStrings[ssIndex] : '';
|
|
171
|
+
} else if (cellType === 'inlineStr' && cell.is) {
|
|
172
|
+
value = extractTextContent(cell.is.t);
|
|
173
|
+
} else if (cellValue !== undefined) {
|
|
174
|
+
value = String(cellValue);
|
|
175
|
+
} else {
|
|
176
|
+
value = '';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
rowValues[colIdx] = value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
allRows.push(rowValues);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Skip the first row (column headers)
|
|
187
|
+
if (allRows.length < 2) {
|
|
188
|
+
throw new Error('Not enough rows in XLSX file for v5 format.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const dataRows = allRows.slice(1); // Skip header row
|
|
192
|
+
|
|
193
|
+
// Decode log lines
|
|
194
|
+
return decodeLogLines(dataRows);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert column letter to 0-based index (A=0, B=1, ..., Z=25, AA=26)
|
|
199
|
+
*/
|
|
200
|
+
function colLetterToIndex(letters) {
|
|
201
|
+
let index = 0;
|
|
202
|
+
for (let i = 0; i < letters.length; i++) {
|
|
203
|
+
index = index * 26 + (letters.charCodeAt(i) - 64);
|
|
204
|
+
}
|
|
205
|
+
return index - 1; // 0-based
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Detect whether an XLSX file is v5 (log-embed) or v3/v4 (hidden sheet) format.
|
|
210
|
+
* @param {string} xlsxPath - Path to XLSX file
|
|
211
|
+
* @returns {string} 'v5' or 'legacy'
|
|
212
|
+
*/
|
|
213
|
+
function detectXlsxVersion(xlsxPath) {
|
|
214
|
+
const zip = new AdmZip(xlsxPath);
|
|
215
|
+
|
|
216
|
+
// v5 files have only sheet1.xml, v3/v4 have sheet2.xml (hidden data sheet)
|
|
217
|
+
const sheet2 = zip.getEntry('xl/worksheets/sheet2.xml');
|
|
218
|
+
if (sheet2) {
|
|
219
|
+
return 'legacy';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Double-check: look at sheet name in workbook.xml
|
|
223
|
+
const wbEntry = zip.getEntry('xl/workbook.xml');
|
|
224
|
+
if (wbEntry) {
|
|
225
|
+
const wbXml = wbEntry.getData().toString('utf8');
|
|
226
|
+
if (wbXml.includes('Access Logs') || wbXml.includes('access_log')) {
|
|
227
|
+
return 'v5';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Default: try v5 if only one sheet exists
|
|
232
|
+
return 'v5';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── v3/v4 Legacy Format ────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create an XLSX file using streaming WorkbookWriter (v4 format).
|
|
239
|
+
* Memory-efficient: rows are freed after commit.
|
|
240
|
+
* @param {object} options
|
|
241
|
+
* @param {string} options.base64Content - Base64 content to store in this part
|
|
242
|
+
* @param {string} options.encryptionMeta - Packed encryption metadata (iv:salt:authTag) or ''
|
|
243
|
+
* @param {string} options.metadataJson - Serialized metadata JSON string
|
|
244
|
+
* @param {string} options.outputPath - Output file path
|
|
245
|
+
* @returns {Promise<string>} Path to created file
|
|
246
|
+
*/
|
|
247
|
+
async function createXlsxPartStreaming(options) {
|
|
248
|
+
const { base64Content, encryptionMeta, metadataJson, outputPath } = options;
|
|
249
|
+
|
|
250
|
+
// Ensure output directory exists
|
|
251
|
+
const outputDir = path.dirname(outputPath);
|
|
252
|
+
if (!fs.existsSync(outputDir)) {
|
|
253
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
257
|
+
filename: outputPath,
|
|
258
|
+
useSharedStrings: false, // Inline strings for easier post-processing
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
workbook.creator = 'Microsoft Excel';
|
|
262
|
+
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
263
|
+
workbook.created = new Date();
|
|
264
|
+
workbook.modified = new Date();
|
|
265
|
+
|
|
266
|
+
// === Sheet 1: Visible decoy data (server metrics) ===
|
|
267
|
+
const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
|
|
268
|
+
properties: { tabColor: { argb: '4472C4' } },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Set column widths before adding rows
|
|
272
|
+
visibleSheet.columns = [
|
|
273
|
+
{ width: 20 }, // Timestamp
|
|
274
|
+
{ width: 16 }, // Server ID
|
|
275
|
+
{ width: 12 }, // Status
|
|
276
|
+
{ width: 8 }, // CPU %
|
|
277
|
+
{ width: 10 }, // Memory %
|
|
278
|
+
{ width: 8 }, // Disk %
|
|
279
|
+
{ width: 14 }, // Network (MB/s)
|
|
280
|
+
{ width: 10 }, // Requests
|
|
281
|
+
{ width: 14 }, // Resp Time (ms)
|
|
282
|
+
{ width: 12 }, // Uptime (hrs)
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
// Add headers
|
|
286
|
+
const headers = generateDecoyHeaders();
|
|
287
|
+
const headerRow = visibleSheet.addRow(headers);
|
|
288
|
+
|
|
289
|
+
const headerCount = headers.length;
|
|
290
|
+
for (let col = 1; col <= headerCount; col++) {
|
|
291
|
+
const cell = headerRow.getCell(col);
|
|
292
|
+
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
|
|
293
|
+
cell.fill = {
|
|
294
|
+
type: 'pattern',
|
|
295
|
+
pattern: 'solid',
|
|
296
|
+
fgColor: { argb: '2E7D32' },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
headerRow.commit();
|
|
300
|
+
|
|
301
|
+
// Generate and write decoy data rows, committing each to free memory
|
|
302
|
+
const payloadSize = base64Content.length;
|
|
303
|
+
const rowCount = calculateDecoyRowCount(payloadSize);
|
|
304
|
+
const decoyData = generateDecoyData(rowCount);
|
|
305
|
+
|
|
306
|
+
for (const row of decoyData) {
|
|
307
|
+
const dataRow = visibleSheet.addRow(rowToArray(row));
|
|
308
|
+
dataRow.commit();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Add filters
|
|
312
|
+
visibleSheet.autoFilter = {
|
|
313
|
+
from: 'A1',
|
|
314
|
+
to: `J${rowCount + 1}`,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await visibleSheet.commit();
|
|
318
|
+
|
|
319
|
+
// === Sheet 2: Hidden payload (veryHidden) ===
|
|
320
|
+
const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
|
|
321
|
+
state: 'veryHidden',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Split base64 content into cell-sized chunks
|
|
325
|
+
const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
|
|
326
|
+
|
|
327
|
+
// Row 1: metadata
|
|
328
|
+
const metaRow = hiddenSheet.getRow(1);
|
|
329
|
+
metaRow.getCell(1).value = encryptionMeta;
|
|
330
|
+
metaRow.getCell(2).value = metadataJson;
|
|
331
|
+
metaRow.getCell(3).value = chunks.length.toString();
|
|
332
|
+
metaRow.commit();
|
|
333
|
+
|
|
334
|
+
// Write cell chunks in rows starting from row 2, columns A-Z
|
|
335
|
+
const totalChunks = chunks.length;
|
|
336
|
+
const totalRows = Math.ceil(totalChunks / 26);
|
|
337
|
+
|
|
338
|
+
for (let rowIdx = 0; rowIdx < totalRows; rowIdx++) {
|
|
339
|
+
const sheetRow = hiddenSheet.getRow(rowIdx + 2);
|
|
340
|
+
const startChunk = rowIdx * 26;
|
|
341
|
+
const endChunk = Math.min(startChunk + 26, totalChunks);
|
|
342
|
+
|
|
343
|
+
for (let i = startChunk; i < endChunk; i++) {
|
|
344
|
+
const col = (i % 26) + 1;
|
|
345
|
+
sheetRow.getCell(col).value = chunks[i];
|
|
346
|
+
}
|
|
347
|
+
sheetRow.commit();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await hiddenSheet.commit();
|
|
351
|
+
await workbook.commit();
|
|
352
|
+
|
|
353
|
+
// WorkbookWriter natively supports veryHidden state — no post-processing needed.
|
|
354
|
+
return outputPath;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Create an XLSX file with encrypted base64 content hidden in a veryHidden sheet (legacy v3)
|
|
359
|
+
*/
|
|
360
|
+
async function createXlsxWithBase64(options) {
|
|
361
|
+
const { base64Content, encryptionMeta, metadata, outputPath } = options;
|
|
362
|
+
|
|
363
|
+
const workbook = new ExcelJS.Workbook();
|
|
364
|
+
|
|
365
|
+
workbook.creator = 'Microsoft Excel';
|
|
366
|
+
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
367
|
+
workbook.created = new Date();
|
|
368
|
+
workbook.modified = new Date();
|
|
369
|
+
|
|
370
|
+
const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
|
|
371
|
+
properties: { tabColor: { argb: '4472C4' } },
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const headers = generateDecoyHeaders();
|
|
375
|
+
const headerRow = visibleSheet.addRow(headers);
|
|
376
|
+
|
|
377
|
+
const headerCount = headers.length;
|
|
378
|
+
for (let col = 1; col <= headerCount; col++) {
|
|
379
|
+
const cell = headerRow.getCell(col);
|
|
380
|
+
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
|
|
381
|
+
cell.fill = {
|
|
382
|
+
type: 'pattern',
|
|
383
|
+
pattern: 'solid',
|
|
384
|
+
fgColor: { argb: '2E7D32' },
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const payloadSize = base64Content.length;
|
|
389
|
+
const rowCount = calculateDecoyRowCount(payloadSize);
|
|
390
|
+
|
|
391
|
+
const decoyData = generateDecoyData(rowCount);
|
|
392
|
+
decoyData.forEach((row) => {
|
|
393
|
+
visibleSheet.addRow(rowToArray(row));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
visibleSheet.columns = [
|
|
397
|
+
{ width: 20 }, { width: 16 }, { width: 12 }, { width: 8 },
|
|
398
|
+
{ width: 10 }, { width: 8 }, { width: 14 }, { width: 10 },
|
|
399
|
+
{ width: 14 }, { width: 12 },
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
visibleSheet.autoFilter = { from: 'A1', to: `J${rowCount + 1}` };
|
|
403
|
+
|
|
404
|
+
const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
|
|
405
|
+
state: 'veryHidden',
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
hiddenSheet.getCell('A1').value = encryptionMeta;
|
|
409
|
+
hiddenSheet.getCell('B1').value = metadata;
|
|
410
|
+
|
|
411
|
+
const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
|
|
412
|
+
hiddenSheet.getCell('C1').value = chunks.length.toString();
|
|
413
|
+
|
|
414
|
+
chunks.forEach((chunk, index) => {
|
|
415
|
+
const row = Math.floor(index / 26) + 2;
|
|
416
|
+
const col = (index % 26) + 1;
|
|
417
|
+
hiddenSheet.getCell(row, col).value = chunk;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const outputDir = path.dirname(outputPath);
|
|
421
|
+
if (!fs.existsSync(outputDir)) {
|
|
422
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await workbook.xlsx.writeFile(outputPath);
|
|
426
|
+
await ensureVeryHidden(outputPath);
|
|
427
|
+
|
|
428
|
+
return outputPath;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Unified Reader ─────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Read an XLSX file and extract content. Auto-detects v5 vs v3/v4 format.
|
|
435
|
+
* @param {string} xlsxPath - Path to XLSX file
|
|
436
|
+
* @returns {Promise<object>} For v5: { payloadBuffer, metadataJson, encryptionMeta, metadata, formatVersion: 'v5' }
|
|
437
|
+
* For legacy: { base64Content, encryptionMeta, metadata, formatVersion: 'legacy' }
|
|
438
|
+
*/
|
|
439
|
+
async function readXlsxBase64(xlsxPath) {
|
|
440
|
+
if (!fs.existsSync(xlsxPath)) {
|
|
441
|
+
throw new Error(`XLSX file not found: ${xlsxPath}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const version = detectXlsxVersion(xlsxPath);
|
|
445
|
+
|
|
446
|
+
if (version === 'v5') {
|
|
447
|
+
const result = await readXlsxV5(xlsxPath);
|
|
448
|
+
return {
|
|
449
|
+
...result,
|
|
450
|
+
formatVersion: 'v5',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const result = await extractFromXml(xlsxPath);
|
|
455
|
+
return {
|
|
456
|
+
...result,
|
|
457
|
+
formatVersion: 'legacy',
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Legacy XML Extraction ──────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
async function extractFromXml(xlsxPath) {
|
|
464
|
+
let sharedStrings = [];
|
|
465
|
+
const ssParsed = parseXmlFromZip(xlsxPath, 'xl/sharedStrings.xml');
|
|
466
|
+
|
|
467
|
+
if (ssParsed && ssParsed.sst && ssParsed.sst.si) {
|
|
468
|
+
const siArray = ensureArray(ssParsed.sst.si);
|
|
469
|
+
sharedStrings = siArray.map((si) => extractTextContent(si.t));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const sheetParsed = parseXmlFromZip(xlsxPath, 'xl/worksheets/sheet2.xml');
|
|
473
|
+
|
|
474
|
+
if (!sheetParsed) {
|
|
475
|
+
throw new Error('Hidden sheet not found in XLSX file. This may not be a stegdoc-encoded file.');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const cellValues = new Map();
|
|
479
|
+
const sheetData = sheetParsed.worksheet?.sheetData;
|
|
480
|
+
|
|
481
|
+
if (sheetData && sheetData.row) {
|
|
482
|
+
const rows = ensureArray(sheetData.row);
|
|
483
|
+
|
|
484
|
+
for (const row of rows) {
|
|
485
|
+
if (!row.c) continue;
|
|
486
|
+
const cells = ensureArray(row.c);
|
|
487
|
+
|
|
488
|
+
for (const cell of cells) {
|
|
489
|
+
const cellRef = cell['@_r'];
|
|
490
|
+
const cellType = cell['@_t'];
|
|
491
|
+
const cellValue = cell.v;
|
|
492
|
+
|
|
493
|
+
if (cellRef === undefined) continue;
|
|
494
|
+
|
|
495
|
+
if (cellType === 's' && cellValue !== undefined) {
|
|
496
|
+
const ssIndex = parseInt(cellValue, 10);
|
|
497
|
+
if (ssIndex < sharedStrings.length) {
|
|
498
|
+
cellValues.set(cellRef, sharedStrings[ssIndex]);
|
|
499
|
+
}
|
|
500
|
+
} else if (cellType === 'inlineStr' && cell.is) {
|
|
501
|
+
const text = extractTextContent(cell.is.t);
|
|
502
|
+
if (text !== undefined) {
|
|
503
|
+
cellValues.set(cellRef, text);
|
|
504
|
+
}
|
|
505
|
+
} else if (cellValue !== undefined) {
|
|
506
|
+
cellValues.set(cellRef, String(cellValue));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const encryptionMeta = cellValues.get('A1') || '';
|
|
513
|
+
const metadata = cellValues.get('B1');
|
|
514
|
+
const chunkCountStr = cellValues.get('C1');
|
|
515
|
+
|
|
516
|
+
if (!metadata) {
|
|
517
|
+
throw new Error('No metadata found in XLSX file.');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const chunkCount = parseInt(chunkCountStr, 10);
|
|
521
|
+
if (isNaN(chunkCount) || chunkCount <= 0) {
|
|
522
|
+
throw new Error('Invalid chunk count in XLSX file.');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const chunks = [];
|
|
526
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
527
|
+
const row = Math.floor(i / 26) + 2;
|
|
528
|
+
const col = (i % 26) + 1;
|
|
529
|
+
const cellRef = `${columnToLetter(col)}${row}`;
|
|
530
|
+
const chunk = cellValues.get(cellRef);
|
|
531
|
+
if (chunk) {
|
|
532
|
+
chunks.push(chunk);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
base64Content: chunks.join(''),
|
|
538
|
+
encryptionMeta,
|
|
539
|
+
metadata,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
async function ensureVeryHidden(xlsxPath) {
|
|
546
|
+
const zip = new AdmZip(xlsxPath);
|
|
547
|
+
|
|
548
|
+
const workbookEntry = zip.getEntry('xl/workbook.xml');
|
|
549
|
+
if (workbookEntry) {
|
|
550
|
+
let workbookXml = workbookEntry.getData().toString('utf8');
|
|
551
|
+
|
|
552
|
+
workbookXml = workbookXml.replace(
|
|
553
|
+
/(<sheet[^>]*name="Data"[^>]*)\s+state="[^"]*"([^>]*\/>)/gi,
|
|
554
|
+
'$1 state="veryHidden"$2'
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (!workbookXml.match(/<sheet[^>]*name="Data"[^>]*state="/i)) {
|
|
558
|
+
workbookXml = workbookXml.replace(
|
|
559
|
+
/(<sheet[^>]*name="Data")([^>]*\/>)/gi,
|
|
560
|
+
'$1 state="veryHidden"$2'
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
zip.updateFile('xl/workbook.xml', Buffer.from(workbookXml, 'utf8'));
|
|
565
|
+
zip.writeZip(xlsxPath);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function splitIntoChunks(str, size) {
|
|
570
|
+
const chunks = [];
|
|
571
|
+
for (let i = 0; i < str.length; i += size) {
|
|
572
|
+
chunks.push(str.slice(i, i + size));
|
|
573
|
+
}
|
|
574
|
+
return chunks;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function columnToLetter(col) {
|
|
578
|
+
let letter = '';
|
|
579
|
+
while (col > 0) {
|
|
580
|
+
const mod = (col - 1) % 26;
|
|
581
|
+
letter = String.fromCharCode(65 + mod) + letter;
|
|
582
|
+
col = Math.floor((col - 1) / 26);
|
|
583
|
+
}
|
|
584
|
+
return letter;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
module.exports = {
|
|
588
|
+
// v5
|
|
589
|
+
createXlsxPartV5,
|
|
590
|
+
readXlsxV5,
|
|
591
|
+
detectXlsxVersion,
|
|
592
|
+
// v3/v4 legacy
|
|
593
|
+
createXlsxPartStreaming,
|
|
594
|
+
createXlsxWithBase64,
|
|
595
|
+
// Unified reader
|
|
596
|
+
readXlsxBase64,
|
|
597
|
+
};
|