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 +196 -1
- package/batch.example.json +16 -0
- package/bin/cmd.js +370 -9
- package/index.js +40 -0
- package/lib/duid-cli.js +781 -0
- package/lib/duid.js +1213 -0
- package/lib/history.js +113 -0
- package/lib/oui.js +286 -0
- package/package.json +6 -3
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:
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|