spoof-d 0.2.0 → 0.4.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/README.md CHANGED
@@ -21,6 +21,7 @@ This repository ([TT5H/spoof-d](https://github.com/TT5H/spoof-d)) is a fork of [
21
21
  - **Modern macOS Support**: Fixed MAC spoofing for macOS Sequoia 15.4+ and Tahoe 26+
22
22
  - **Windows Support**: Full Windows 10/11 support using PowerShell and registry methods with automatic fallback
23
23
  - **Linux Support**: Modern Linux support using `ip link` commands (replaces deprecated `ifconfig`)
24
+ - **DUID Spoofing**: DHCPv6 DUID spoofing with automatic original preservation and cross-platform support
24
25
  - **Enhanced Error Handling**: Custom error classes with actionable suggestions and better error messages
25
26
  - **Automatic Verification**: Verifies MAC address changes after setting them
26
27
  - **Retry Logic**: Automatic retry with exponential backoff for transient failures
@@ -51,10 +52,15 @@ This repository ([TT5H/spoof-d](https://github.com/TT5H/spoof-d)) is a fork of [
51
52
  - **Unified API** across all platforms
52
53
 
53
54
  ### User Experience Features
54
- - **Progress indicators** for long-running operations
55
+ - **Progress indicators** with animated spinners for long-running operations
55
56
  - **Verbose mode** (`--verbose`) for detailed debugging output
56
57
  - **JSON output** (`--json`) for scripting and automation
57
58
  - **Configuration file** support (`.spoofyrc` in home directory)
59
+ - **MAC address vendor lookup** using OUI database
60
+ - **Change history tracking** for both MAC and DUID changes with ability to view history
61
+ - **Batch operations** for changing multiple interfaces at once
62
+ - **DUID (DHCPv6) spoofing** for complete IPv6 network identity management
63
+ - **Automatic verification** of DUID changes with retry logic
58
64
 
59
65
  ## Installation
60
66
 
@@ -165,6 +171,176 @@ sudo spoofy reset wi-fi
165
171
 
166
172
  **Note**: On macOS, restarting your computer will also reset your MAC address to the original hardware address.
167
173
 
174
+ ## DUID Spoofing (DHCPv6)
175
+
176
+ `spoof-d` also supports DHCPv6 DUID (DHCP Unique Identifier) spoofing for complete IPv6 network identity management.
177
+
178
+ ### What is a DUID?
179
+
180
+ A DUID (DHCP Unique Identifier) is used in DHCPv6 to uniquely identify a client on IPv6 networks. Unlike MAC addresses which identify network interfaces, DUIDs identify DHCP clients across all interfaces and persist across reboots.
181
+
182
+ ### Key Feature: Original DUID Preservation
183
+
184
+ The first time you spoof your DUID, your **original DUID is automatically saved** to:
185
+ - macOS: `/var/db/dhcpclient/DUID.original`
186
+ - Linux: `/var/lib/spoofy/duid.original`
187
+ - Windows: `%PROGRAMDATA%\spoofy\duid.original`
188
+
189
+ This allows you to **restore to your pre-spoofing state** at any time using `spoofy duid restore`.
190
+
191
+ ### Show current DUID
192
+
193
+ ```bash
194
+ spoofy duid list
195
+ ```
196
+
197
+ ### Randomize DUID _(requires root)_
198
+
199
+ Generate and set a random DUID (automatically saves your original on first use):
200
+
201
+ ```bash
202
+ sudo spoofy duid randomize en0
203
+ ```
204
+
205
+ You can specify the DUID type:
206
+
207
+ ```bash
208
+ sudo spoofy duid randomize en0 --type=LLT
209
+ ```
210
+
211
+ ### Set specific DUID _(requires root)_
212
+
213
+ ```bash
214
+ sudo spoofy duid set 00:03:00:01:aa:bb:cc:dd:ee:ff en0
215
+ ```
216
+
217
+ ### Sync DUID to current MAC _(requires root)_
218
+
219
+ Match DUID to the current MAC address of an interface (useful after MAC spoofing):
220
+
221
+ ```bash
222
+ sudo spoofy duid sync en0
223
+ ```
224
+
225
+ With specific type:
226
+
227
+ ```bash
228
+ sudo spoofy duid sync en0 --type=LLT
229
+ ```
230
+
231
+ **Typical workflow for complete identity spoofing:**
232
+
233
+ ```bash
234
+ sudo spoofy randomize en0 # Spoof MAC first
235
+ sudo spoofy duid sync en0 # Then sync DUID to match
236
+ ```
237
+
238
+ This ensures both layers show the same spoofed identity on IPv6 networks.
239
+
240
+ ### Restore to original DUID _(requires root)_
241
+
242
+ Return to your original (pre-spoofing) DUID:
243
+
244
+ ```bash
245
+ sudo spoofy duid restore en0
246
+ ```
247
+
248
+ ### Reset DUID _(requires root)_
249
+
250
+ Delete current DUID and let the system generate a NEW random one:
251
+
252
+ ```bash
253
+ sudo spoofy duid reset en0
254
+ ```
255
+
256
+ **Important**: `reset` generates a NEW DUID, while `restore` returns to your ORIGINAL.
257
+
258
+ ### DUID Types
259
+
260
+ | Type | Name | Description |
261
+ |------|------|-------------|
262
+ | 1 | DUID-LLT | Link-layer address + timestamp (most common) |
263
+ | 2 | DUID-EN | Enterprise number + identifier |
264
+ | 3 | DUID-LL | Link-layer address only (default) |
265
+ | 4 | DUID-UUID | UUID-based identifier |
266
+
267
+ ### Show detailed interface information
268
+
269
+ ```bash
270
+ spoofy info en0
271
+ ```
272
+
273
+ Shows detailed information about an interface including hardware MAC, current MAC, vendor information, and change history.
274
+
275
+ ### Validate MAC address format
276
+
277
+ ```bash
278
+ spoofy validate 00:11:22:33:44:55
279
+ ```
280
+
281
+ Validates and normalizes a MAC address, showing vendor information if available.
282
+
283
+ ### Look up vendor from MAC address
284
+
285
+ ```bash
286
+ spoofy vendor 00:11:22:33:44:55
287
+ ```
288
+
289
+ Looks up the vendor/manufacturer of a MAC address using the OUI database.
290
+
291
+ ### Batch operations
292
+
293
+ Create a batch file (e.g., `batch.json`):
294
+
295
+ ```json
296
+ [
297
+ {
298
+ "type": "randomize",
299
+ "device": "en0",
300
+ "local": true
301
+ },
302
+ {
303
+ "type": "set",
304
+ "device": "eth0",
305
+ "mac": "00:11:22:33:44:55"
306
+ },
307
+ {
308
+ "type": "reset",
309
+ "device": "wlan0"
310
+ }
311
+ ]
312
+ ```
313
+
314
+ Then run:
315
+
316
+ ```bash
317
+ sudo spoofy batch batch.json
318
+ ```
319
+
320
+ ### View change history
321
+
322
+ ```bash
323
+ spoofy history
324
+ ```
325
+
326
+ View all MAC address changes, or filter by device:
327
+
328
+ ```bash
329
+ spoofy history en0
330
+ ```
331
+
332
+ ### View DUID change history
333
+
334
+ ```bash
335
+ spoofy duid history
336
+ ```
337
+
338
+ View all DUID changes, or filter by device:
339
+
340
+ ```bash
341
+ spoofy duid history en0
342
+ ```
343
+
168
344
  ## Advanced Usage
169
345
 
170
346
  ### Verbose Mode
@@ -213,6 +389,25 @@ Create a configuration file at `~/.spoofyrc` (or `%USERPROFILE%\.spoofyrc` on Wi
213
389
 
214
390
  The configuration file allows you to set default options that will be used automatically.
215
391
 
392
+ ### Change History
393
+
394
+ All MAC address and DUID changes are automatically logged to `~/.spoofy_history.json`. You can:
395
+
396
+ - View MAC history: `spoofy history`
397
+ - View MAC history for specific device: `spoofy history en0`
398
+ - View DUID history: `spoofy duid history`
399
+ - View DUID history for specific device: `spoofy duid history en0`
400
+ - History includes timestamp, device, old/new values, and operation type
401
+ - Both MAC and DUID changes are tracked in the same history file
402
+
403
+ ### Vendor Lookup
404
+
405
+ The tool includes an OUI (Organizationally Unique Identifier) database to identify device vendors:
406
+
407
+ - Automatically shown in `spoofy list` output
408
+ - Available via `spoofy vendor <mac>` command
409
+ - Helps identify device types and manufacturers
410
+
216
411
  ### Progress Indicators
217
412
 
218
413
  Long-running operations show progress indicators:
@@ -0,0 +1,16 @@
1
+ [
2
+ {
3
+ "type": "randomize",
4
+ "device": "en0",
5
+ "local": true
6
+ },
7
+ {
8
+ "type": "set",
9
+ "device": "eth0",
10
+ "mac": "00:11:22:33:44:55"
11
+ },
12
+ {
13
+ "type": "reset",
14
+ "device": "wlan0"
15
+ }
16
+ ]
package/bin/cmd.js CHANGED
@@ -8,6 +8,10 @@ const cp = require("child_process");
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
+ const ora = require("ora");
12
+ const history = require("../lib/history");
13
+ const oui = require("../lib/oui");
14
+ const duidCli = require("../lib/duid-cli");
11
15
 
12
16
  const argv = minimist(process.argv.slice(2), {
13
17
  alias: {
@@ -53,22 +57,51 @@ function outputJSON(data) {
53
57
  }
54
58
  }
55
59
 
60
+ let currentSpinner = null;
61
+
56
62
  function showProgress(message) {
57
63
  if (JSON_OUTPUT) return;
58
- process.stdout.write(chalk.blue("⏳ ") + message + "... ");
64
+ if (currentSpinner) {
65
+ currentSpinner.text = message;
66
+ } else {
67
+ currentSpinner = ora(message).start();
68
+ }
59
69
  }
60
70
 
61
71
  function hideProgress() {
62
72
  if (JSON_OUTPUT) return;
63
- process.stdout.write("\r" + " ".repeat(80) + "\r");
73
+ if (currentSpinner) {
74
+ currentSpinner.stop();
75
+ currentSpinner = null;
76
+ }
77
+ }
78
+
79
+ function successProgress(message) {
80
+ if (JSON_OUTPUT) return;
81
+ if (currentSpinner) {
82
+ currentSpinner.succeed(message);
83
+ currentSpinner = null;
84
+ }
85
+ }
86
+
87
+ function failProgress(message) {
88
+ if (JSON_OUTPUT) return;
89
+ if (currentSpinner) {
90
+ currentSpinner.fail(message);
91
+ currentSpinner = null;
92
+ }
64
93
  }
65
94
 
66
95
  function progressStep(step, total, message) {
67
96
  if (JSON_OUTPUT) return;
68
97
  const percentage = Math.round((step / total) * 100);
69
- process.stdout.write(`\r${chalk.blue("⏳")} [${step}/${total}] ${percentage}% - ${message}...`);
98
+ if (currentSpinner) {
99
+ currentSpinner.text = `[${step}/${total}] ${percentage}% - ${message}`;
100
+ } else {
101
+ currentSpinner = ora(`[${step}/${total}] ${percentage}% - ${message}`).start();
102
+ }
70
103
  if (step === total) {
71
- process.stdout.write("\r" + " ".repeat(80) + "\r");
104
+ currentSpinner = null;
72
105
  }
73
106
  }
74
107
 
@@ -136,6 +169,22 @@ function init() {
136
169
  } else if (cmd === "normalize") {
137
170
  const mac = argv._[1];
138
171
  normalize(mac);
172
+ } else if (cmd === "info") {
173
+ const device = argv._[1];
174
+ info(device);
175
+ } else if (cmd === "validate") {
176
+ const mac = argv._[1];
177
+ validate(mac);
178
+ } else if (cmd === "vendor") {
179
+ const mac = argv._[1];
180
+ vendor(mac);
181
+ } else if (cmd === "batch") {
182
+ const file = argv._[1];
183
+ batch(file);
184
+ } else if (cmd === "history") {
185
+ historyCmd();
186
+ } else if (cmd === "duid") {
187
+ duidCli.run(argv._.slice(1), VERBOSE, JSON_OUTPUT);
139
188
  } else {
140
189
  help();
141
190
  }
@@ -172,6 +221,19 @@ ${example}${note}
172
221
  spoofy help Shows this help message.
173
222
  spoofy version | --version | -v Show package version.
174
223
 
224
+ Commands:
225
+ list [--wifi] List available devices.
226
+ set <mac> <devices>... Set device MAC address.
227
+ randomize [--local] <devices>... Set device MAC address randomly.
228
+ reset <devices>... Reset device MAC address to default.
229
+ normalize <mac> Normalize a MAC address format.
230
+ info <device> Show detailed interface information.
231
+ validate <mac> Validate MAC address format.
232
+ vendor <mac> Look up vendor from MAC address.
233
+ batch <file> Change multiple interfaces from config file.
234
+ history View MAC address change history.
235
+ duid <command> DHCPv6 DUID spoofing commands (see: spoofy duid help).
236
+
175
237
  Options:
176
238
  --wifi Try to only show wireless interfaces.
177
239
  --local Set the locally administered flag on randomized MACs.
@@ -299,7 +361,7 @@ function randomize(devices) {
299
361
  console.log(chalk.blue("ℹ"), `Generated random MAC address: ${chalk.bold.cyan(mac)}`);
300
362
  }
301
363
 
302
- setMACAddress(it.device, mac, it.port);
364
+ setMACAddress(it.device, mac, it.port, "randomize");
303
365
  });
304
366
  }
305
367
 
@@ -339,7 +401,7 @@ function reset(devices) {
339
401
  console.log(chalk.blue("ℹ"), `Resetting to hardware MAC address: ${chalk.bold.cyan(it.address)}`);
340
402
  }
341
403
 
342
- setMACAddress(it.device, it.address, it.port);
404
+ setMACAddress(it.device, it.address, it.port, "reset");
343
405
  });
344
406
  }
345
407
 
@@ -395,17 +457,28 @@ function list() {
395
457
 
396
458
  if (it.address) {
397
459
  line.push("with MAC address", chalk.bold.cyan(it.address));
460
+ const vendor = oui.lookupVendor(it.address);
461
+ if (vendor && vendor !== "Unknown") {
462
+ line.push(chalk.gray(`[${vendor}]`));
463
+ }
398
464
  }
399
465
  if (it.currentAddress && it.currentAddress !== it.address) {
400
466
  line.push("currently set to", chalk.bold.red(it.currentAddress));
467
+ const currentVendor = oui.lookupVendor(it.currentAddress);
468
+ if (currentVendor && currentVendor !== "Unknown") {
469
+ line.push(chalk.gray(`[${currentVendor}]`));
470
+ }
401
471
  }
402
472
  console.log(line.join(" "));
403
473
  });
404
474
  }
405
475
 
406
- function setMACAddress(device, mac, port) {
476
+ function setMACAddress(device, mac, port, operation = "set") {
407
477
  logVerbose(`Setting MAC address ${mac} on device ${device}`);
408
478
 
479
+ // Get current MAC for history
480
+ const oldMac = spoof.getInterfaceMAC(device);
481
+
409
482
  // Check for admin/root privileges
410
483
  if (process.platform === "win32") {
411
484
  logVerbose("Checking for Administrator privileges...");
@@ -454,13 +527,17 @@ function setMACAddress(device, mac, port) {
454
527
 
455
528
  spoof.setInterfaceMAC(device, mac, port);
456
529
 
457
- hideProgress();
530
+ // Log to history
531
+ history.addHistoryEntry(device, oldMac, mac, operation);
532
+
533
+ successProgress("MAC address changed successfully");
458
534
 
459
535
  if (JSON_OUTPUT) {
460
536
  outputJSON({
461
537
  success: true,
462
538
  device: device,
463
539
  mac: mac,
540
+ oldMac: oldMac,
464
541
  message: "MAC address changed successfully",
465
542
  });
466
543
  } else {
@@ -476,7 +553,7 @@ function setMACAddress(device, mac, port) {
476
553
  logVerbose("MAC address change completed successfully");
477
554
  // Note: Verification is already done in setInterfaceMAC, so we don't need to do it again here
478
555
  } catch (err) {
479
- hideProgress();
556
+ failProgress("Failed to change MAC address");
480
557
  if (JSON_OUTPUT) {
481
558
  outputJSON({
482
559
  success: false,
@@ -490,3 +567,287 @@ function setMACAddress(device, mac, port) {
490
567
  throw err;
491
568
  }
492
569
  }
570
+
571
+ function info(device) {
572
+ if (!device) {
573
+ throw new Error("Device name is required. Usage: spoofy info <device>");
574
+ }
575
+
576
+ logVerbose(`Getting info for device: ${device}`);
577
+ showProgress("Fetching interface information");
578
+
579
+ const it = spoof.findInterface(device);
580
+ hideProgress();
581
+
582
+ if (!it) {
583
+ throw new Error(
584
+ `Could not find device "${device}". ` +
585
+ "List available devices using: spoofy list"
586
+ );
587
+ }
588
+
589
+ const currentMac = spoof.getInterfaceMAC(device);
590
+ const vendorInfo = it.address ? oui.getVendorInfo(it.address) : null;
591
+ const currentVendorInfo = currentMac ? oui.getVendorInfo(currentMac) : null;
592
+ const deviceHistory = history.getHistoryForDevice(device);
593
+
594
+ if (JSON_OUTPUT) {
595
+ outputJSON({
596
+ device: it.device,
597
+ port: it.port || it.device,
598
+ description: it.description,
599
+ hardwareMac: it.address,
600
+ currentMac: currentMac,
601
+ hardwareVendor: vendorInfo ? vendorInfo.vendor : null,
602
+ currentVendor: currentVendorInfo ? currentVendorInfo.vendor : null,
603
+ status: it.status,
604
+ platform: process.platform,
605
+ historyCount: deviceHistory.length,
606
+ lastChange: deviceHistory[0] || null,
607
+ });
608
+ return;
609
+ }
610
+
611
+ console.log(chalk.bold.cyan("\nInterface Information"));
612
+ console.log(chalk.gray("─".repeat(50)));
613
+ console.log(chalk.bold("Device:"), it.device);
614
+ console.log(chalk.bold("Port:"), it.port || it.device);
615
+ if (it.description) {
616
+ console.log(chalk.bold("Description:"), it.description);
617
+ }
618
+ if (it.status) {
619
+ console.log(chalk.bold("Status:"), it.status);
620
+ }
621
+ console.log(chalk.bold("Platform:"), process.platform);
622
+
623
+ if (it.address) {
624
+ console.log(chalk.bold("\nHardware MAC Address:"), chalk.cyan(it.address));
625
+ if (vendorInfo && vendorInfo.vendor !== "Unknown") {
626
+ console.log(chalk.bold("Hardware Vendor:"), chalk.green(vendorInfo.vendor));
627
+ }
628
+ }
629
+
630
+ if (currentMac) {
631
+ console.log(chalk.bold("Current MAC Address:"), chalk.cyan(currentMac));
632
+ if (currentVendorInfo && currentVendorInfo.vendor !== "Unknown") {
633
+ console.log(chalk.bold("Current Vendor:"), chalk.green(currentVendorInfo.vendor));
634
+ }
635
+ if (currentMac !== it.address) {
636
+ console.log(chalk.yellow("⚠ MAC address has been changed from hardware address"));
637
+ }
638
+ }
639
+
640
+ if (deviceHistory.length > 0) {
641
+ console.log(chalk.bold("\nChange History:"), `(${deviceHistory.length} entries)`);
642
+ deviceHistory.slice(0, 5).forEach((entry, index) => {
643
+ const date = new Date(entry.timestamp).toLocaleString();
644
+ console.log(` ${index + 1}. ${date} - ${entry.operation}: ${entry.oldMac || "N/A"} → ${entry.newMac}`);
645
+ });
646
+ if (deviceHistory.length > 5) {
647
+ console.log(chalk.gray(` ... and ${deviceHistory.length - 5} more entries`));
648
+ }
649
+ }
650
+ console.log();
651
+ }
652
+
653
+ function validate(mac) {
654
+ if (!mac) {
655
+ throw new Error("MAC address is required. Usage: spoofy validate <mac>");
656
+ }
657
+
658
+ logVerbose(`Validating MAC address: ${mac}`);
659
+
660
+ const normalized = spoof.normalize(mac);
661
+ const isValid = !!normalized;
662
+ const vendorInfo = normalized ? oui.getVendorInfo(normalized) : null;
663
+
664
+ if (JSON_OUTPUT) {
665
+ outputJSON({
666
+ original: mac,
667
+ normalized: normalized,
668
+ valid: isValid,
669
+ vendor: vendorInfo ? vendorInfo.vendor : null,
670
+ prefix: vendorInfo ? vendorInfo.prefix : null,
671
+ });
672
+ return;
673
+ }
674
+
675
+ if (isValid) {
676
+ console.log(chalk.green("✓ Valid MAC address"));
677
+ console.log(chalk.bold("Normalized:"), normalized);
678
+ if (vendorInfo && vendorInfo.vendor !== "Unknown") {
679
+ console.log(chalk.bold("Vendor:"), chalk.green(vendorInfo.vendor));
680
+ }
681
+ } else {
682
+ console.log(chalk.red("✗ Invalid MAC address"));
683
+ console.log(chalk.yellow("Expected format: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX"));
684
+ }
685
+ }
686
+
687
+ function vendor(mac) {
688
+ if (!mac) {
689
+ throw new Error("MAC address is required. Usage: spoofy vendor <mac>");
690
+ }
691
+
692
+ logVerbose(`Looking up vendor for MAC: ${mac}`);
693
+
694
+ const normalized = spoof.normalize(mac);
695
+ if (!normalized) {
696
+ throw new Error(`"${mac}" is not a valid MAC address`);
697
+ }
698
+
699
+ const vendorInfo = oui.getVendorInfo(normalized);
700
+
701
+ if (JSON_OUTPUT) {
702
+ outputJSON({
703
+ mac: normalized,
704
+ vendor: vendorInfo.vendor,
705
+ prefix: vendorInfo.prefix,
706
+ found: vendorInfo.vendor !== "Unknown",
707
+ });
708
+ return;
709
+ }
710
+
711
+ console.log(chalk.bold("MAC Address:"), normalized);
712
+ console.log(chalk.bold("Vendor:"), vendorInfo.vendor !== "Unknown" ? chalk.green(vendorInfo.vendor) : chalk.yellow("Unknown"));
713
+ console.log(chalk.bold("Prefix:"), vendorInfo.prefix);
714
+
715
+ if (vendorInfo.vendor === "Unknown") {
716
+ console.log(chalk.gray("\nNote: Vendor not found in database. This may be a locally administered address."));
717
+ }
718
+ }
719
+
720
+ function batch(file) {
721
+ if (!file) {
722
+ throw new Error("Batch file is required. Usage: spoofy batch <file>");
723
+ }
724
+
725
+ if (!fs.existsSync(file)) {
726
+ throw new Error(`Batch file not found: ${file}`);
727
+ }
728
+
729
+ logVerbose(`Loading batch file: ${file}`);
730
+ showProgress("Loading batch configuration");
731
+
732
+ let batchConfig;
733
+ try {
734
+ const content = fs.readFileSync(file, "utf8");
735
+ batchConfig = JSON.parse(content);
736
+ } catch (err) {
737
+ hideProgress();
738
+ throw new Error(`Failed to parse batch file: ${err.message}`);
739
+ }
740
+
741
+ hideProgress();
742
+
743
+ if (!Array.isArray(batchConfig)) {
744
+ throw new Error("Batch file must contain an array of operations");
745
+ }
746
+
747
+ logVerbose(`Found ${batchConfig.length} operation(s) in batch file`);
748
+
749
+ const results = [];
750
+ let successCount = 0;
751
+ let failCount = 0;
752
+
753
+ batchConfig.forEach((operation, index) => {
754
+ const step = index + 1;
755
+ const total = batchConfig.length;
756
+ progressStep(step, total, `Processing operation ${step}`);
757
+
758
+ try {
759
+ if (operation.type === "set" && operation.device && operation.mac) {
760
+ const it = spoof.findInterface(operation.device);
761
+ if (!it) {
762
+ throw new Error(`Device not found: ${operation.device}`);
763
+ }
764
+ setMACAddress(it.device, operation.mac, it.port, "batch-set");
765
+ results.push({ success: true, operation: operation, index: index });
766
+ successCount++;
767
+ } else if (operation.type === "randomize" && operation.device) {
768
+ const it = spoof.findInterface(operation.device);
769
+ if (!it) {
770
+ throw new Error(`Device not found: ${operation.device}`);
771
+ }
772
+ const mac = spoof.randomize(operation.local || false);
773
+ setMACAddress(it.device, mac, it.port, "batch-randomize");
774
+ results.push({ success: true, operation: operation, mac: mac, index: index });
775
+ successCount++;
776
+ } else if (operation.type === "reset" && operation.device) {
777
+ const it = spoof.findInterface(operation.device);
778
+ if (!it) {
779
+ throw new Error(`Device not found: ${operation.device}`);
780
+ }
781
+ if (!it.address) {
782
+ throw new Error(`No hardware MAC address for: ${operation.device}`);
783
+ }
784
+ setMACAddress(it.device, it.address, it.port, "batch-reset");
785
+ results.push({ success: true, operation: operation, index: index });
786
+ successCount++;
787
+ } else {
788
+ throw new Error(`Invalid operation type or missing parameters: ${JSON.stringify(operation)}`);
789
+ }
790
+ } catch (err) {
791
+ results.push({ success: false, operation: operation, error: err.message, index: index });
792
+ failCount++;
793
+ }
794
+ });
795
+
796
+ progressStep(batchConfig.length, batchConfig.length, "Completed");
797
+
798
+ if (JSON_OUTPUT) {
799
+ outputJSON({
800
+ total: batchConfig.length,
801
+ success: successCount,
802
+ failed: failCount,
803
+ results: results,
804
+ });
805
+ } else {
806
+ console.log(chalk.bold("\nBatch Operation Summary:"));
807
+ console.log(chalk.green(` ✓ Successful: ${successCount}`));
808
+ if (failCount > 0) {
809
+ console.log(chalk.red(` ✗ Failed: ${failCount}`));
810
+ }
811
+ console.log(chalk.bold(` Total: ${batchConfig.length}`));
812
+ }
813
+ }
814
+
815
+ function historyCmd() {
816
+ const device = argv._[1]; // Optional device filter
817
+ logVerbose(device ? `Getting history for device: ${device}` : "Getting all history");
818
+
819
+ const allHistory = history.getHistory();
820
+ const deviceHistory = device ? history.getHistoryForDevice(device) : allHistory;
821
+
822
+ if (deviceHistory.length === 0) {
823
+ if (JSON_OUTPUT) {
824
+ outputJSON({ history: [], count: 0 });
825
+ } else {
826
+ console.log(chalk.yellow("No history found" + (device ? ` for device "${device}"` : "")));
827
+ }
828
+ return;
829
+ }
830
+
831
+ if (JSON_OUTPUT) {
832
+ outputJSON({
833
+ history: deviceHistory,
834
+ count: deviceHistory.length,
835
+ device: device || "all",
836
+ });
837
+ return;
838
+ }
839
+
840
+ console.log(chalk.bold.cyan("\nMAC Address Change History"));
841
+ console.log(chalk.gray("─".repeat(80)));
842
+
843
+ deviceHistory.forEach((entry, index) => {
844
+ const date = new Date(entry.timestamp).toLocaleString();
845
+ console.log(chalk.bold(`\n${index + 1}. ${date}`));
846
+ console.log(` Device: ${chalk.green(entry.device)}`);
847
+ console.log(` Operation: ${chalk.cyan(entry.operation)}`);
848
+ console.log(` ${chalk.gray(entry.oldMac || "N/A")} → ${chalk.cyan(entry.newMac)}`);
849
+ console.log(` Platform: ${entry.platform}`);
850
+ });
851
+
852
+ console.log();
853
+ }