cipher-security 2.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.
Files changed (76) hide show
  1. package/bin/cipher.js +566 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +238 -0
  45. package/lib/brand.js +105 -0
  46. package/lib/commands.js +100 -0
  47. package/lib/complexity.js +377 -0
  48. package/lib/config.js +213 -0
  49. package/lib/gateway/client.js +309 -0
  50. package/lib/gateway/commands.js +991 -0
  51. package/lib/gateway/config-validate.js +109 -0
  52. package/lib/gateway/gateway.js +367 -0
  53. package/lib/gateway/index.js +62 -0
  54. package/lib/gateway/mode.js +309 -0
  55. package/lib/gateway/plugins.js +222 -0
  56. package/lib/gateway/prompt.js +214 -0
  57. package/lib/mcp/server.js +262 -0
  58. package/lib/memory/compressor.js +425 -0
  59. package/lib/memory/engine.js +763 -0
  60. package/lib/memory/evolution.js +668 -0
  61. package/lib/memory/index.js +58 -0
  62. package/lib/memory/orchestrator.js +506 -0
  63. package/lib/memory/retriever.js +515 -0
  64. package/lib/memory/synthesizer.js +333 -0
  65. package/lib/pipeline/async-scanner.js +510 -0
  66. package/lib/pipeline/binary-analysis.js +1043 -0
  67. package/lib/pipeline/dom-xss-scanner.js +435 -0
  68. package/lib/pipeline/github-actions.js +792 -0
  69. package/lib/pipeline/index.js +124 -0
  70. package/lib/pipeline/osint.js +498 -0
  71. package/lib/pipeline/sarif.js +373 -0
  72. package/lib/pipeline/scanner.js +880 -0
  73. package/lib/pipeline/template-manager.js +525 -0
  74. package/lib/pipeline/xss-scanner.js +353 -0
  75. package/lib/setup-wizard.js +288 -0
  76. package/package.json +31 -0
@@ -0,0 +1,1043 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * Pure-Node.js ELF binary analysis — no native dependencies.
7
+ *
8
+ * Provides:
9
+ * - ELF header parsing (arch, entry point, type, endianness)
10
+ * - Section enumeration (names, sizes, permissions)
11
+ * - Symbol table extraction (imports/exports)
12
+ * - Security feature detection (NX, PIE, RELRO, stack canaries, RPATH)
13
+ * - Format string vulnerability pattern detection in source code
14
+ * - ROP gadget scanning via objdump subprocess
15
+ *
16
+ * Ported from pipeline/binary_analysis.py (799 LOC Python).
17
+ */
18
+
19
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
20
+ import { join, extname } from 'node:path';
21
+ import { execFileSync } from 'node:child_process';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // ELF constants
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF
28
+
29
+ // e_ident offsets
30
+ const EI_CLASS = 4;
31
+ const EI_DATA = 5;
32
+
33
+ // ELF classes
34
+ const ELFCLASS32 = 1;
35
+ const ELFCLASS64 = 2;
36
+ const CLASS_NAMES = Object.freeze({ [ELFCLASS32]: 'ELF32', [ELFCLASS64]: 'ELF64' });
37
+
38
+ // Data encoding
39
+ const ELFDATA2LSB = 1;
40
+ const ELFDATA2MSB = 2;
41
+ const ENDIAN_NAMES = Object.freeze({
42
+ [ELFDATA2LSB]: 'little-endian',
43
+ [ELFDATA2MSB]: 'big-endian',
44
+ });
45
+
46
+ // Object file types
47
+ const TYPE_NAMES = Object.freeze({
48
+ 0: 'NONE', 1: 'REL', 2: 'EXEC', 3: 'DYN', 4: 'CORE',
49
+ });
50
+
51
+ // Machine types
52
+ const MACHINE_NAMES = Object.freeze({
53
+ 0: 'NONE', 3: 'x86', 8: 'MIPS', 20: 'PowerPC', 40: 'ARM',
54
+ 43: 'SPARC v9', 62: 'x86-64', 183: 'AArch64', 243: 'RISC-V',
55
+ });
56
+
57
+ // Section types
58
+ const SHT_SYMTAB = 2;
59
+ const SHT_STRTAB = 3;
60
+ const SHT_DYNSYM = 11;
61
+
62
+ // Program header types
63
+ const PT_DYNAMIC = 2;
64
+ const PT_INTERP = 3;
65
+ const PT_GNU_RELRO = 0x6474e552;
66
+ const PT_GNU_STACK = 0x6474e551;
67
+
68
+ // Dynamic tags
69
+ const DT_NEEDED = 1;
70
+ const DT_RPATH = 15;
71
+ const DT_RUNPATH = 29;
72
+ const DT_FLAGS = 30;
73
+ const DT_FLAGS_1 = 0x6ffffffb;
74
+ const DF_BIND_NOW = 0x8;
75
+ const DF_1_NOW = 0x1;
76
+ const DF_1_PIE = 0x08000000;
77
+
78
+ // Section header flags
79
+ const SHF_WRITE = 0x1;
80
+ const SHF_ALLOC = 0x2;
81
+ const SHF_EXECINSTR = 0x4;
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Buffer read helpers — abstract endianness
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Read unsigned 16-bit integer.
89
+ * @param {Buffer} buf
90
+ * @param {number} off byte offset
91
+ * @param {boolean} le true = little-endian
92
+ * @returns {number}
93
+ */
94
+ function readU16(buf, off, le) {
95
+ return le ? buf.readUInt16LE(off) : buf.readUInt16BE(off);
96
+ }
97
+
98
+ /**
99
+ * Read unsigned 32-bit integer.
100
+ * @param {Buffer} buf
101
+ * @param {number} off byte offset
102
+ * @param {boolean} le true = little-endian
103
+ * @returns {number}
104
+ */
105
+ function readU32(buf, off, le) {
106
+ return le ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
107
+ }
108
+
109
+ /**
110
+ * Read unsigned 64-bit integer as Number (safe for file offsets < 2^53).
111
+ * @param {Buffer} buf
112
+ * @param {number} off byte offset
113
+ * @param {boolean} le true = little-endian
114
+ * @returns {number}
115
+ */
116
+ function readU64(buf, off, le) {
117
+ const big = le ? buf.readBigUInt64LE(off) : buf.readBigUInt64BE(off);
118
+ return Number(big);
119
+ }
120
+
121
+ /**
122
+ * Read signed 32-bit integer.
123
+ * @param {Buffer} buf
124
+ * @param {number} off byte offset
125
+ * @param {boolean} le true = little-endian
126
+ * @returns {number}
127
+ */
128
+ function readI32(buf, off, le) {
129
+ return le ? buf.readInt32LE(off) : buf.readInt32BE(off);
130
+ }
131
+
132
+ /**
133
+ * Read signed 64-bit integer as Number.
134
+ * @param {Buffer} buf
135
+ * @param {number} off byte offset
136
+ * @param {boolean} le true = little-endian
137
+ * @returns {number}
138
+ */
139
+ function readI64(buf, off, le) {
140
+ const big = le ? buf.readBigInt64LE(off) : buf.readBigInt64BE(off);
141
+ return Number(big);
142
+ }
143
+
144
+ /**
145
+ * Read null-terminated ASCII string from buffer.
146
+ * @param {Buffer} data
147
+ * @param {number} offset
148
+ * @returns {string}
149
+ */
150
+ function readStr(data, offset) {
151
+ const end = data.indexOf(0, offset);
152
+ if (end < 0) return '';
153
+ return data.subarray(offset, end).toString('ascii');
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Data classes
158
+ // ---------------------------------------------------------------------------
159
+
160
+ class ELFSection {
161
+ /**
162
+ * @param {object} opts
163
+ * @param {string} opts.name
164
+ * @param {number} opts.typeId
165
+ * @param {number} opts.flags
166
+ * @param {number} opts.addr
167
+ * @param {number} opts.offset
168
+ * @param {number} opts.size
169
+ * @param {number} [opts.link=0]
170
+ * @param {number} [opts.info=0]
171
+ * @param {number} [opts.entsize=0]
172
+ */
173
+ constructor(opts) {
174
+ this.name = opts.name;
175
+ this.typeId = opts.typeId;
176
+ this.flags = opts.flags;
177
+ this.addr = opts.addr;
178
+ this.offset = opts.offset;
179
+ this.size = opts.size;
180
+ this.link = opts.link ?? 0;
181
+ this.info = opts.info ?? 0;
182
+ this.entsize = opts.entsize ?? 0;
183
+ }
184
+
185
+ get isExecutable() {
186
+ return Boolean(this.flags & SHF_EXECINSTR);
187
+ }
188
+
189
+ get isWritable() {
190
+ return Boolean(this.flags & SHF_WRITE);
191
+ }
192
+
193
+ get permissions() {
194
+ const r = this.flags & SHF_ALLOC ? 'R' : '-';
195
+ const w = this.flags & SHF_WRITE ? 'W' : '-';
196
+ const x = this.flags & SHF_EXECINSTR ? 'X' : '-';
197
+ return `${r}${w}${x}`;
198
+ }
199
+
200
+ toDict() {
201
+ return {
202
+ name: this.name,
203
+ size: this.size,
204
+ permissions: this.permissions,
205
+ address: '0x' + this.addr.toString(16),
206
+ };
207
+ }
208
+ }
209
+
210
+ class ELFSymbol {
211
+ /**
212
+ * @param {object} opts
213
+ * @param {string} opts.name
214
+ * @param {number} opts.value
215
+ * @param {number} opts.size
216
+ * @param {string} opts.bind LOCAL | GLOBAL | WEAK
217
+ * @param {string} opts.type FUNC | OBJECT | NOTYPE
218
+ * @param {number} opts.sectionIndex
219
+ */
220
+ constructor(opts) {
221
+ this.name = opts.name;
222
+ this.value = opts.value;
223
+ this.size = opts.size;
224
+ this.bind = opts.bind;
225
+ this.type = opts.type;
226
+ this.sectionIndex = opts.sectionIndex;
227
+ }
228
+
229
+ toDict() {
230
+ return {
231
+ name: this.name,
232
+ value: '0x' + this.value.toString(16),
233
+ bind: this.bind,
234
+ type: this.type,
235
+ };
236
+ }
237
+ }
238
+
239
+ class SecurityFeatures {
240
+ /**
241
+ * @param {object} [opts]
242
+ */
243
+ constructor(opts = {}) {
244
+ this.nx = opts.nx ?? false;
245
+ this.pie = opts.pie ?? false;
246
+ this.relro = opts.relro ?? 'none';
247
+ this.canary = opts.canary ?? false;
248
+ this.fortify = opts.fortify ?? false;
249
+ this.rpath = opts.rpath ?? false;
250
+ this.runpath = opts.runpath ?? false;
251
+ this.stripped = opts.stripped ?? false;
252
+ }
253
+
254
+ /** Hardening score 0-100. */
255
+ get score() {
256
+ let s = 0;
257
+ if (this.nx) s += 20;
258
+ if (this.pie) s += 20;
259
+ if (this.relro === 'full') s += 20;
260
+ else if (this.relro === 'partial') s += 10;
261
+ if (this.canary) s += 20;
262
+ if (this.fortify) s += 10;
263
+ if (!this.rpath) s += 10;
264
+ return Math.min(s, 100);
265
+ }
266
+
267
+ toDict() {
268
+ return {
269
+ NX: this.nx,
270
+ PIE: this.pie,
271
+ RELRO: this.relro,
272
+ Canary: this.canary,
273
+ Fortify: this.fortify,
274
+ RPATH: this.rpath,
275
+ Stripped: this.stripped,
276
+ };
277
+ }
278
+ }
279
+
280
+ class ELFAnalysis {
281
+ /**
282
+ * @param {object} opts
283
+ * @param {string} opts.filename
284
+ */
285
+ constructor(opts) {
286
+ this.filename = opts.filename;
287
+ this.elfClass = opts.elfClass ?? '';
288
+ this.endianness = opts.endianness ?? '';
289
+ this.elfType = opts.elfType ?? '';
290
+ this.machine = opts.machine ?? '';
291
+ this.entryPoint = opts.entryPoint ?? 0;
292
+ this.sections = opts.sections ?? [];
293
+ this.symbols = opts.symbols ?? [];
294
+ this.imports = opts.imports ?? [];
295
+ this.libraries = opts.libraries ?? [];
296
+ this.security = opts.security ?? new SecurityFeatures();
297
+ this.suspicious = opts.suspicious ?? [];
298
+ this.errors = opts.errors ?? [];
299
+ }
300
+
301
+ toDict() {
302
+ return {
303
+ filename: this.filename,
304
+ class: this.elfClass,
305
+ endianness: this.endianness,
306
+ type: this.elfType,
307
+ machine: this.machine,
308
+ entry_point: '0x' + this.entryPoint.toString(16),
309
+ sections: this.sections.length,
310
+ symbols: this.symbols.length,
311
+ imports: this.imports.slice(0, 50),
312
+ libraries: this.libraries,
313
+ security: this.security.toDict(),
314
+ hardening_score: this.security.score,
315
+ suspicious: this.suspicious,
316
+ errors: this.errors,
317
+ };
318
+ }
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // ELF Parser
323
+ // ---------------------------------------------------------------------------
324
+
325
+ class ELFParser {
326
+ /**
327
+ * Parse an ELF binary and return full analysis.
328
+ * @param {string} filepath
329
+ * @returns {ELFAnalysis}
330
+ */
331
+ parse(filepath) {
332
+ const result = new ELFAnalysis({ filename: filepath });
333
+
334
+ let data;
335
+ try {
336
+ data = readFileSync(filepath);
337
+ } catch (err) {
338
+ result.errors.push(`Cannot read file: ${err.message}`);
339
+ return result;
340
+ }
341
+
342
+ if (data.length < 64) {
343
+ result.errors.push('File too small to be an ELF binary');
344
+ return result;
345
+ }
346
+
347
+ if (!data.subarray(0, 4).equals(ELF_MAGIC)) {
348
+ result.errors.push('Not an ELF file (bad magic bytes)');
349
+ return result;
350
+ }
351
+
352
+ // Check for UPX packing in first 4KB
353
+ if (data.subarray(0, 4096).includes(Buffer.from('UPX!'))) {
354
+ result.suspicious.push('UPX-packed binary');
355
+ }
356
+
357
+ const eiClass = data[EI_CLASS];
358
+ const eiData = data[EI_DATA];
359
+ result.elfClass = CLASS_NAMES[eiClass] ?? `unknown(${eiClass})`;
360
+ result.endianness = ENDIAN_NAMES[eiData] ?? `unknown(${eiData})`;
361
+
362
+ const is64 = eiClass === ELFCLASS64;
363
+ const le = eiData === ELFDATA2LSB;
364
+
365
+ try {
366
+ this._parseHeader(data, result, is64, le);
367
+ this._parseSections(data, result, is64, le);
368
+ this._parseProgramHeaders(data, result, is64, le);
369
+ this._checkSecurity(result, data);
370
+ this._detectSuspicious(result, data);
371
+ } catch (err) {
372
+ result.errors.push(`Parse error: ${err.message}`);
373
+ }
374
+
375
+ return result;
376
+ }
377
+
378
+ /**
379
+ * Parse ELF header fields starting at offset 16.
380
+ *
381
+ * ELF32 header (from offset 16):
382
+ * offset 16: e_type (uint16)
383
+ * offset 18: e_machine (uint16)
384
+ * offset 20: e_version (uint32)
385
+ * offset 24: e_entry (uint32) — entry point
386
+ *
387
+ * ELF64 header (from offset 16):
388
+ * offset 16: e_type (uint16)
389
+ * offset 18: e_machine (uint16)
390
+ * offset 20: e_version (uint32)
391
+ * offset 24: e_entry (uint64) — entry point
392
+ */
393
+ _parseHeader(data, result, is64, le) {
394
+ const eType = readU16(data, 16, le);
395
+ const eMachine = readU16(data, 18, le);
396
+ // e_version at 20 (uint32) — skipped
397
+ const eEntry = is64 ? readU64(data, 24, le) : readU32(data, 24, le);
398
+
399
+ result.elfType = TYPE_NAMES[eType] ?? `unknown(${eType})`;
400
+ result.machine = MACHINE_NAMES[eMachine] ?? `unknown(${eMachine})`;
401
+ result.entryPoint = eEntry;
402
+ }
403
+
404
+ /**
405
+ * Parse section headers.
406
+ *
407
+ * ELF64: e_shoff at offset 40 (uint64), e_shentsize at 58 (uint16),
408
+ * e_shnum at 60 (uint16), e_shstrndx at 62 (uint16).
409
+ * ELF32: e_shoff at offset 32 (uint32), e_shentsize at 46 (uint16),
410
+ * e_shnum at 48 (uint16), e_shstrndx at 50 (uint16).
411
+ */
412
+ _parseSections(data, result, is64, le) {
413
+ let eShoff, eShentsize, eShnum, eShstrndx;
414
+ if (is64) {
415
+ eShoff = readU64(data, 40, le);
416
+ eShentsize = readU16(data, 58, le);
417
+ eShnum = readU16(data, 60, le);
418
+ eShstrndx = readU16(data, 62, le);
419
+ } else {
420
+ eShoff = readU32(data, 32, le);
421
+ eShentsize = readU16(data, 46, le);
422
+ eShnum = readU16(data, 48, le);
423
+ eShstrndx = readU16(data, 50, le);
424
+ }
425
+
426
+ if (eShoff === 0 || eShnum === 0) return;
427
+
428
+ // Read section header string table
429
+ const shstrtabOff = eShoff + eShstrndx * eShentsize;
430
+ let strtabOffset, strtabSize;
431
+ if (is64) {
432
+ // sh_offset at +24 (uint64), sh_size at +32 (uint64) in 64-bit section header
433
+ strtabOffset = readU64(data, shstrtabOff + 24, le);
434
+ strtabSize = readU64(data, shstrtabOff + 32, le);
435
+ } else {
436
+ // sh_offset at +16 (uint32), sh_size at +20 (uint32) in 32-bit section header
437
+ strtabOffset = readU32(data, shstrtabOff + 16, le);
438
+ strtabSize = readU32(data, shstrtabOff + 20, le);
439
+ }
440
+
441
+ const strtab = data.subarray(strtabOffset, strtabOffset + strtabSize);
442
+
443
+ for (let i = 0; i < eShnum; i++) {
444
+ const off = eShoff + i * eShentsize;
445
+ let sec;
446
+ if (is64) {
447
+ /**
448
+ * ELF64 section header (64 bytes):
449
+ * +0 sh_name (uint32)
450
+ * +4 sh_type (uint32)
451
+ * +8 sh_flags (uint64)
452
+ * +16 sh_addr (uint64)
453
+ * +24 sh_offset (uint64)
454
+ * +32 sh_size (uint64)
455
+ * +40 sh_link (uint32)
456
+ * +44 sh_info (uint32)
457
+ * +48 sh_addralign (uint64)
458
+ * +56 sh_entsize (uint64)
459
+ */
460
+ sec = new ELFSection({
461
+ name: readStr(strtab, readU32(data, off, le)),
462
+ typeId: readU32(data, off + 4, le),
463
+ flags: readU64(data, off + 8, le),
464
+ addr: readU64(data, off + 16, le),
465
+ offset: readU64(data, off + 24, le),
466
+ size: readU64(data, off + 32, le),
467
+ link: readU32(data, off + 40, le),
468
+ info: readU32(data, off + 44, le),
469
+ entsize: readU64(data, off + 56, le),
470
+ });
471
+ } else {
472
+ /**
473
+ * ELF32 section header (40 bytes):
474
+ * +0 sh_name (uint32)
475
+ * +4 sh_type (uint32)
476
+ * +8 sh_flags (uint32)
477
+ * +12 sh_addr (uint32)
478
+ * +16 sh_offset (uint32)
479
+ * +20 sh_size (uint32)
480
+ * +24 sh_link (uint32)
481
+ * +28 sh_info (uint32)
482
+ * +32 sh_addralign (uint32)
483
+ * +36 sh_entsize (uint32)
484
+ */
485
+ sec = new ELFSection({
486
+ name: readStr(strtab, readU32(data, off, le)),
487
+ typeId: readU32(data, off + 4, le),
488
+ flags: readU32(data, off + 8, le),
489
+ addr: readU32(data, off + 12, le),
490
+ offset: readU32(data, off + 16, le),
491
+ size: readU32(data, off + 20, le),
492
+ link: readU32(data, off + 24, le),
493
+ info: readU32(data, off + 28, le),
494
+ entsize: readU32(data, off + 36, le),
495
+ });
496
+ }
497
+ result.sections.push(sec);
498
+
499
+ // Extract symbols from .dynsym
500
+ if (sec.typeId === SHT_DYNSYM && sec.entsize > 0) {
501
+ this._parseSymbols(data, sec, result, is64, le);
502
+ }
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Parse symbols from a .dynsym section.
508
+ */
509
+ _parseSymbols(data, sec, result, is64, le) {
510
+ // Find .dynstr section
511
+ let dynstrSec = null;
512
+ for (const s of result.sections) {
513
+ if (s.typeId === SHT_STRTAB && s.name === '.dynstr') {
514
+ dynstrSec = s;
515
+ break;
516
+ }
517
+ }
518
+ if (!dynstrSec) return;
519
+
520
+ const symStrtab = data.subarray(dynstrSec.offset, dynstrSec.offset + dynstrSec.size);
521
+ const nSyms = sec.entsize ? Math.floor(sec.size / sec.entsize) : 0;
522
+ const bindNames = { 0: 'LOCAL', 1: 'GLOBAL', 2: 'WEAK' };
523
+ const typeNames = { 0: 'NOTYPE', 1: 'OBJECT', 2: 'FUNC', 10: 'IFUNC' };
524
+
525
+ for (let i = 0; i < nSyms; i++) {
526
+ const off = sec.offset + i * sec.entsize;
527
+ let nameIdx, info, shndx, value, size;
528
+
529
+ if (is64) {
530
+ /**
531
+ * ELF64 symbol (24 bytes):
532
+ * +0 st_name (uint32)
533
+ * +4 st_info (uint8)
534
+ * +5 st_other (uint8)
535
+ * +6 st_shndx (uint16)
536
+ * +8 st_value (uint64)
537
+ * +16 st_size (uint64)
538
+ */
539
+ nameIdx = readU32(data, off, le);
540
+ info = data[off + 4];
541
+ shndx = readU16(data, off + 6, le);
542
+ value = readU64(data, off + 8, le);
543
+ size = readU64(data, off + 16, le);
544
+ } else {
545
+ /**
546
+ * ELF32 symbol (16 bytes):
547
+ * +0 st_name (uint32)
548
+ * +4 st_value (uint32)
549
+ * +8 st_size (uint32)
550
+ * +12 st_info (uint8)
551
+ * +13 st_other (uint8)
552
+ * +14 st_shndx (uint16)
553
+ */
554
+ nameIdx = readU32(data, off, le);
555
+ value = readU32(data, off + 4, le);
556
+ size = readU32(data, off + 8, le);
557
+ info = data[off + 12];
558
+ shndx = readU16(data, off + 14, le);
559
+ }
560
+
561
+ const name = readStr(symStrtab, nameIdx);
562
+ if (!name) continue;
563
+
564
+ const bind = bindNames[info >> 4] ?? 'OTHER';
565
+ const stype = typeNames[info & 0xf] ?? 'OTHER';
566
+
567
+ const sym = new ELFSymbol({ name, value, size, bind, type: stype, sectionIndex: shndx });
568
+ result.symbols.push(sym);
569
+
570
+ // Import = FUNC with section index 0 (UND)
571
+ if (shndx === 0 && stype === 'FUNC') {
572
+ result.imports.push(name);
573
+ }
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Parse program headers.
579
+ *
580
+ * ELF64: e_phoff at 32 (uint64), e_phentsize at 54 (uint16), e_phnum at 56 (uint16).
581
+ * ELF32: e_phoff at 28 (uint32), e_phentsize at 42 (uint16), e_phnum at 44 (uint16).
582
+ */
583
+ _parseProgramHeaders(data, result, is64, le) {
584
+ let ePhoff, ePhentsize, ePhnum;
585
+ if (is64) {
586
+ ePhoff = readU64(data, 32, le);
587
+ ePhentsize = readU16(data, 54, le);
588
+ ePhnum = readU16(data, 56, le);
589
+ } else {
590
+ ePhoff = readU32(data, 28, le);
591
+ ePhentsize = readU16(data, 42, le);
592
+ ePhnum = readU16(data, 44, le);
593
+ }
594
+
595
+ if (ePhoff === 0 || ePhnum === 0) return;
596
+
597
+ for (let i = 0; i < ePhnum; i++) {
598
+ const off = ePhoff + i * ePhentsize;
599
+ let pType, pFlags, pOffset, pFilesz;
600
+
601
+ if (is64) {
602
+ /**
603
+ * ELF64 program header (56 bytes):
604
+ * +0 p_type (uint32)
605
+ * +4 p_flags (uint32)
606
+ * +8 p_offset (uint64)
607
+ * +32 p_filesz (uint64)
608
+ */
609
+ pType = readU32(data, off, le);
610
+ pFlags = readU32(data, off + 4, le);
611
+ pOffset = readU64(data, off + 8, le);
612
+ pFilesz = readU64(data, off + 32, le);
613
+ } else {
614
+ /**
615
+ * ELF32 program header (32 bytes):
616
+ * +0 p_type (uint32)
617
+ * +4 p_offset (uint32)
618
+ * +16 p_filesz (uint32)
619
+ * +24 p_flags (uint32)
620
+ */
621
+ pType = readU32(data, off, le);
622
+ pOffset = readU32(data, off + 4, le);
623
+ pFilesz = readU32(data, off + 16, le);
624
+ pFlags = readU32(data, off + 24, le);
625
+ }
626
+
627
+ if (pType === PT_GNU_STACK) {
628
+ // PF_X = 0x1 — if executable bit set, NX is NOT enabled
629
+ result.security.nx = !(pFlags & 0x1);
630
+ } else if (pType === PT_GNU_RELRO) {
631
+ result.security.relro = 'partial';
632
+ } else if (pType === PT_INTERP) {
633
+ const interp = data.subarray(pOffset, pOffset + pFilesz).toString('ascii').replace(/\0+$/, '');
634
+ result.suspicious.push(`interpreter: ${interp}`);
635
+ } else if (pType === PT_DYNAMIC) {
636
+ this._parseDynamic(data, pOffset, pFilesz, result, is64, le);
637
+ }
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Parse dynamic section entries for library dependencies and security flags.
643
+ */
644
+ _parseDynamic(data, offset, size, result, is64, le) {
645
+ // Find .dynstr for library name resolution
646
+ let dynstr = Buffer.alloc(0);
647
+ for (const sec of result.sections) {
648
+ if (sec.name === '.dynstr') {
649
+ dynstr = data.subarray(sec.offset, sec.offset + sec.size);
650
+ break;
651
+ }
652
+ }
653
+
654
+ const entrySize = is64 ? 16 : 8;
655
+ const nEntries = Math.floor(size / entrySize);
656
+ let bindNow = false;
657
+
658
+ for (let i = 0; i < nEntries; i++) {
659
+ const entOff = offset + i * entrySize;
660
+ let dTag, dVal;
661
+
662
+ if (is64) {
663
+ dTag = readI64(data, entOff, le);
664
+ dVal = readU64(data, entOff + 8, le);
665
+ } else {
666
+ dTag = readI32(data, entOff, le);
667
+ dVal = readU32(data, entOff + 4, le);
668
+ }
669
+
670
+ if (dTag === 0) break; // DT_NULL
671
+ if (dTag === DT_NEEDED && dynstr.length > 0) {
672
+ result.libraries.push(readStr(dynstr, dVal));
673
+ } else if (dTag === DT_RPATH) {
674
+ result.security.rpath = true;
675
+ } else if (dTag === DT_RUNPATH) {
676
+ result.security.runpath = true;
677
+ } else if (dTag === DT_FLAGS && (dVal & DF_BIND_NOW)) {
678
+ bindNow = true;
679
+ } else if (dTag === DT_FLAGS_1) {
680
+ if (dVal & DF_1_NOW) bindNow = true;
681
+ if (dVal & DF_1_PIE) result.security.pie = true;
682
+ }
683
+ }
684
+
685
+ if (bindNow && result.security.relro === 'partial') {
686
+ result.security.relro = 'full';
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Additional security feature detection.
692
+ */
693
+ _checkSecurity(result, data) {
694
+ // PIE heuristic: DYN type + entry != 0
695
+ if (result.elfType === 'DYN' && result.entryPoint !== 0) {
696
+ result.security.pie = true;
697
+ }
698
+
699
+ // Stack canary: check for __stack_chk_fail import
700
+ for (const sym of result.symbols) {
701
+ if (sym.name.includes('__stack_chk_fail')) {
702
+ result.security.canary = true;
703
+ }
704
+ if (sym.name.startsWith('__') && sym.name.includes('_chk')) {
705
+ result.security.fortify = true;
706
+ }
707
+ }
708
+
709
+ // Stripped: no .symtab section
710
+ const hasSymtab = result.sections.some((s) => s.name === '.symtab');
711
+ result.security.stripped = !hasSymtab;
712
+ }
713
+
714
+ /**
715
+ * Detect suspicious binary indicators.
716
+ */
717
+ _detectSuspicious(result, data) {
718
+ const sectionNames = new Set(result.sections.map((s) => s.name));
719
+
720
+ if (sectionNames.has('.upx0') || sectionNames.has('.upx1')) {
721
+ result.suspicious.push('UPX-packed (section names)');
722
+ }
723
+
724
+ // All sections stripped — only empty name and .shstrtab remain
725
+ const meaningful = new Set(sectionNames);
726
+ meaningful.delete('');
727
+ meaningful.delete('.shstrtab');
728
+ if (meaningful.size === 0 && result.sections.length > 0) {
729
+ result.suspicious.push('All sections stripped — possibly packed');
730
+ }
731
+
732
+ // W+X sections
733
+ for (const sec of result.sections) {
734
+ if (sec.isWritable && sec.isExecutable && sec.name) {
735
+ result.suspicious.push(`W+X section: ${sec.name} (${sec.size} bytes)`);
736
+ }
737
+ }
738
+
739
+ // Anti-debug imports
740
+ const antiDebugFuncs = new Set(['ptrace', 'prctl', 'getppid', 'isDebuggerPresent']);
741
+ for (const imp of result.imports) {
742
+ if (antiDebugFuncs.has(imp)) {
743
+ result.suspicious.push(`Anti-debug function: ${imp}`);
744
+ }
745
+ }
746
+ }
747
+ }
748
+
749
+ // ---------------------------------------------------------------------------
750
+ // Format String Scanner
751
+ // ---------------------------------------------------------------------------
752
+
753
+ const PRINTF_FAMILY = /\b(s?n?printf|fprintf|vprintf|vsprintf|vsnprintf|vfprintf|syslog|err|errx|warn|warnx)\s*\(/i;
754
+
755
+ const C_EXTENSIONS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp']);
756
+
757
+ class FormatStringFinding {
758
+ /**
759
+ * @param {object} opts
760
+ * @param {string} opts.file
761
+ * @param {number} opts.lineNumber
762
+ * @param {string} opts.lineContent
763
+ * @param {string} opts.function
764
+ * @param {string} opts.severity
765
+ * @param {string} opts.reason
766
+ */
767
+ constructor(opts) {
768
+ this.file = opts.file;
769
+ this.lineNumber = opts.lineNumber;
770
+ this.lineContent = opts.lineContent;
771
+ this.function = opts.function;
772
+ this.severity = opts.severity;
773
+ this.reason = opts.reason;
774
+ }
775
+
776
+ toDict() {
777
+ return {
778
+ file: this.file,
779
+ line: this.lineNumber,
780
+ function: this.function,
781
+ severity: this.severity,
782
+ reason: this.reason,
783
+ content: this.lineContent.trim().slice(0, 200),
784
+ };
785
+ }
786
+ }
787
+
788
+ class FormatStringScanner {
789
+ /**
790
+ * Scan a source file for format string vulnerabilities.
791
+ * @param {string} filepath
792
+ * @returns {FormatStringFinding[]}
793
+ */
794
+ scanFile(filepath) {
795
+ const ext = extname(filepath).toLowerCase();
796
+ if (!C_EXTENSIONS.has(ext)) return [];
797
+
798
+ let content;
799
+ try {
800
+ content = readFileSync(filepath, 'utf-8');
801
+ } catch {
802
+ return [];
803
+ }
804
+
805
+ const lines = content.split('\n');
806
+ const findings = [];
807
+
808
+ for (let i = 0; i < lines.length; i++) {
809
+ const line = lines[i];
810
+ const stripped = line.trim();
811
+ if (stripped.startsWith('//') || stripped.startsWith('/*')) continue;
812
+
813
+ const m = PRINTF_FAMILY.exec(line);
814
+ if (!m) continue;
815
+
816
+ const func = m[1];
817
+ const after = line.slice(m.index + m[0].length).trimStart();
818
+
819
+ // String literal format = safe
820
+ if (after.startsWith('"') || after.startsWith("'")) continue;
821
+
822
+ // Variable as format string — dangerous
823
+ const severity = ['printf', 'sprintf', 'fprintf'].includes(func) ? 'critical' : 'high';
824
+
825
+ findings.push(
826
+ new FormatStringFinding({
827
+ file: filepath,
828
+ lineNumber: i + 1,
829
+ lineContent: line,
830
+ function: func,
831
+ severity,
832
+ reason: `Variable passed as format string to ${func}()`,
833
+ }),
834
+ );
835
+ }
836
+
837
+ return findings;
838
+ }
839
+
840
+ /**
841
+ * Scan a directory recursively for format string vulnerabilities.
842
+ * @param {string} directory
843
+ * @returns {FormatStringFinding[]}
844
+ */
845
+ scanDirectory(directory) {
846
+ const findings = [];
847
+ const walk = (dir) => {
848
+ let entries;
849
+ try {
850
+ entries = readdirSync(dir, { withFileTypes: true });
851
+ } catch {
852
+ return;
853
+ }
854
+ for (const ent of entries.sort((a, b) => a.name.localeCompare(b.name))) {
855
+ const full = join(dir, ent.name);
856
+ if (ent.isDirectory()) {
857
+ walk(full);
858
+ } else if (ent.isFile() && C_EXTENSIONS.has(extname(ent.name).toLowerCase())) {
859
+ findings.push(...this.scanFile(full));
860
+ }
861
+ }
862
+ };
863
+ walk(directory);
864
+ return findings;
865
+ }
866
+ }
867
+
868
+ // ---------------------------------------------------------------------------
869
+ // ROP Gadget Scanner — uses objdump subprocess per D050
870
+ // ---------------------------------------------------------------------------
871
+
872
+ class ROPGadget {
873
+ /**
874
+ * @param {object} opts
875
+ * @param {number} opts.address
876
+ * @param {string} opts.instructions
877
+ * @param {string} opts.bytesHex
878
+ * @param {number} opts.length number of instructions
879
+ */
880
+ constructor(opts) {
881
+ this.address = opts.address;
882
+ this.instructions = opts.instructions;
883
+ this.bytesHex = opts.bytesHex;
884
+ this.length = opts.length;
885
+ }
886
+
887
+ toDict() {
888
+ return {
889
+ address: '0x' + this.address.toString(16),
890
+ instructions: this.instructions,
891
+ bytes: this.bytesHex,
892
+ length: this.length,
893
+ };
894
+ }
895
+ }
896
+
897
+ class ROPScanner {
898
+ /** Max instructions per gadget. */
899
+ static MAX_GADGET_INSNS = 6;
900
+
901
+ /**
902
+ * Check if objdump is available.
903
+ * @returns {boolean}
904
+ */
905
+ static available() {
906
+ try {
907
+ execFileSync('objdump', ['--version'], { stdio: 'pipe', timeout: 5000 });
908
+ return true;
909
+ } catch {
910
+ return false;
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Scan an ELF binary for ROP gadgets using objdump disassembly.
916
+ * @param {string} filepath
917
+ * @param {object} [opts]
918
+ * @param {number} [opts.maxGadgets=200]
919
+ * @param {number} [opts.maxDepth=5] max instructions before gadget-ending instruction
920
+ * @returns {ROPGadget[]}
921
+ */
922
+ scan(filepath, opts = {}) {
923
+ const maxGadgets = opts.maxGadgets ?? 200;
924
+ const maxDepth = opts.maxDepth ?? 5;
925
+
926
+ let output;
927
+ try {
928
+ output = execFileSync('objdump', ['-d', '-j', '.text', filepath], {
929
+ encoding: 'utf-8',
930
+ stdio: ['pipe', 'pipe', 'pipe'],
931
+ timeout: 30000,
932
+ maxBuffer: 50 * 1024 * 1024, // 50MB
933
+ });
934
+ } catch {
935
+ return [];
936
+ }
937
+
938
+ // Parse objdump output lines
939
+ // Format: " 4004c3: c3 ret"
940
+ // " addr: hex_bytes mnemonic operands"
941
+ const lines = output.split('\n');
942
+ const parsed = []; // { addr, bytes, mnemonic, operands }
943
+
944
+ for (const line of lines) {
945
+ const m = line.match(/^\s*([0-9a-f]+):\s+((?:[0-9a-f]{2}\s?)+)\s+(\S+)(?:\s+(.*))?$/);
946
+ if (!m) continue;
947
+ const addr = parseInt(m[1], 16);
948
+ const bytes = m[2].trim();
949
+ const mnemonic = m[3];
950
+ const operands = (m[4] ?? '').trim();
951
+ parsed.push({ addr, bytes, mnemonic, operands });
952
+ }
953
+
954
+ // Find gadget-ending instructions
955
+ const gadgetEnds = new Set(['ret', 'retq']);
956
+ const syscallEnds = new Set(['syscall']);
957
+ // int 0x80 represented as "int" with operand "$0x80"
958
+
959
+ const gadgets = [];
960
+ const seen = new Set();
961
+
962
+ for (let i = 0; i < parsed.length; i++) {
963
+ const ins = parsed[i];
964
+ const isEnd =
965
+ gadgetEnds.has(ins.mnemonic) ||
966
+ syscallEnds.has(ins.mnemonic) ||
967
+ (ins.mnemonic === 'int' && ins.operands === '$0x80');
968
+
969
+ if (!isEnd) continue;
970
+
971
+ // Collect preceding instruction sequences of varying depths
972
+ for (let depth = 1; depth <= maxDepth && depth <= i; depth++) {
973
+ const startIdx = i - depth;
974
+ const sequence = parsed.slice(startIdx, i + 1);
975
+
976
+ const insnStr = sequence
977
+ .map((s) => (s.operands ? `${s.mnemonic} ${s.operands}` : s.mnemonic))
978
+ .join(' ; ');
979
+
980
+ if (seen.has(insnStr)) continue;
981
+ seen.add(insnStr);
982
+
983
+ const bytesHex = sequence.map((s) => s.bytes.replace(/\s/g, '')).join('');
984
+
985
+ gadgets.push(
986
+ new ROPGadget({
987
+ address: sequence[0].addr,
988
+ instructions: insnStr,
989
+ bytesHex,
990
+ length: sequence.length,
991
+ }),
992
+ );
993
+
994
+ if (gadgets.length >= maxGadgets) {
995
+ return gadgets.sort((a, b) => a.address - b.address);
996
+ }
997
+ }
998
+ }
999
+
1000
+ return gadgets.sort((a, b) => a.address - b.address);
1001
+ }
1002
+
1003
+ /**
1004
+ * Find gadgets matching an instruction pattern (regex).
1005
+ * @param {ROPGadget[]} gadgets
1006
+ * @param {string|RegExp} pattern
1007
+ * @returns {ROPGadget[]}
1008
+ */
1009
+ static findGadgetsByPattern(gadgets, pattern) {
1010
+ const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
1011
+ return gadgets.filter((g) => re.test(g.instructions));
1012
+ }
1013
+ }
1014
+
1015
+ // ---------------------------------------------------------------------------
1016
+ // Exports
1017
+ // ---------------------------------------------------------------------------
1018
+
1019
+ export {
1020
+ // Constants
1021
+ ELF_MAGIC,
1022
+ SHF_WRITE,
1023
+ SHF_ALLOC,
1024
+ SHF_EXECINSTR,
1025
+ // Helpers
1026
+ readU16,
1027
+ readU32,
1028
+ readU64,
1029
+ readStr,
1030
+ // Data classes
1031
+ ELFSection,
1032
+ ELFSymbol,
1033
+ SecurityFeatures,
1034
+ ELFAnalysis,
1035
+ // Parser
1036
+ ELFParser,
1037
+ // Format string scanner
1038
+ FormatStringFinding,
1039
+ FormatStringScanner,
1040
+ // ROP
1041
+ ROPGadget,
1042
+ ROPScanner,
1043
+ };