spoof-d 0.1.0 → 0.3.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/.spoofyrc.example +10 -0
- package/README.md +146 -0
- package/batch.example.json +16 -0
- package/bin/cmd.js +556 -20
- package/index.js +40 -0
- package/lib/history.js +62 -0
- package/lib/oui.js +286 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -50,6 +50,15 @@ This repository ([TT5H/spoof-d](https://github.com/TT5H/spoof-d)) is a fork of [
|
|
|
50
50
|
- **Linux**: Modern ip link commands with ifconfig fallback
|
|
51
51
|
- **Unified API** across all platforms
|
|
52
52
|
|
|
53
|
+
### User Experience Features
|
|
54
|
+
- **Progress indicators** with animated spinners for long-running operations
|
|
55
|
+
- **Verbose mode** (`--verbose`) for detailed debugging output
|
|
56
|
+
- **JSON output** (`--json`) for scripting and automation
|
|
57
|
+
- **Configuration file** support (`.spoofyrc` in home directory)
|
|
58
|
+
- **MAC address vendor lookup** using OUI database
|
|
59
|
+
- **Change history tracking** with ability to view and revert changes
|
|
60
|
+
- **Batch operations** for changing multiple interfaces at once
|
|
61
|
+
|
|
53
62
|
## Installation
|
|
54
63
|
|
|
55
64
|
### From npm (recommended)
|
|
@@ -159,6 +168,143 @@ sudo spoofy reset wi-fi
|
|
|
159
168
|
|
|
160
169
|
**Note**: On macOS, restarting your computer will also reset your MAC address to the original hardware address.
|
|
161
170
|
|
|
171
|
+
### Show detailed interface information
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
spoofy info en0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Shows detailed information about an interface including hardware MAC, current MAC, vendor information, and change history.
|
|
178
|
+
|
|
179
|
+
### Validate MAC address format
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
spoofy validate 00:11:22:33:44:55
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Validates and normalizes a MAC address, showing vendor information if available.
|
|
186
|
+
|
|
187
|
+
### Look up vendor from MAC address
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
spoofy vendor 00:11:22:33:44:55
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Looks up the vendor/manufacturer of a MAC address using the OUI database.
|
|
194
|
+
|
|
195
|
+
### Batch operations
|
|
196
|
+
|
|
197
|
+
Create a batch file (e.g., `batch.json`):
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
[
|
|
201
|
+
{
|
|
202
|
+
"type": "randomize",
|
|
203
|
+
"device": "en0",
|
|
204
|
+
"local": true
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"type": "set",
|
|
208
|
+
"device": "eth0",
|
|
209
|
+
"mac": "00:11:22:33:44:55"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"type": "reset",
|
|
213
|
+
"device": "wlan0"
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Then run:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
sudo spoofy batch batch.json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### View change history
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
spoofy history
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
View all MAC address changes, or filter by device:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
spoofy history en0
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Advanced Usage
|
|
237
|
+
|
|
238
|
+
### Verbose Mode
|
|
239
|
+
|
|
240
|
+
Get detailed debugging information:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
spoofy list --verbose
|
|
244
|
+
spoofy randomize en0 --verbose
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### JSON Output
|
|
248
|
+
|
|
249
|
+
Output results in JSON format for scripting:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
spoofy list --json
|
|
253
|
+
spoofy randomize en0 --json
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Example JSON output:
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"success": true,
|
|
260
|
+
"device": "en0",
|
|
261
|
+
"mac": "00:11:22:33:44:55",
|
|
262
|
+
"message": "MAC address changed successfully"
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Configuration File
|
|
267
|
+
|
|
268
|
+
Create a configuration file at `~/.spoofyrc` (or `%USERPROFILE%\.spoofyrc` on Windows):
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"randomize": {
|
|
273
|
+
"local": true
|
|
274
|
+
},
|
|
275
|
+
"defaults": {
|
|
276
|
+
"verbose": false,
|
|
277
|
+
"json": false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The configuration file allows you to set default options that will be used automatically.
|
|
283
|
+
|
|
284
|
+
### Change History
|
|
285
|
+
|
|
286
|
+
All MAC address changes are automatically logged to `~/.spoofy_history.json`. You can:
|
|
287
|
+
|
|
288
|
+
- View history: `spoofy history`
|
|
289
|
+
- View history for specific device: `spoofy history en0`
|
|
290
|
+
- History includes timestamp, device, old MAC, new MAC, and operation type
|
|
291
|
+
|
|
292
|
+
### Vendor Lookup
|
|
293
|
+
|
|
294
|
+
The tool includes an OUI (Organizationally Unique Identifier) database to identify device vendors:
|
|
295
|
+
|
|
296
|
+
- Automatically shown in `spoofy list` output
|
|
297
|
+
- Available via `spoofy vendor <mac>` command
|
|
298
|
+
- Helps identify device types and manufacturers
|
|
299
|
+
|
|
300
|
+
### Progress Indicators
|
|
301
|
+
|
|
302
|
+
Long-running operations show progress indicators:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
⏳ Changing MAC address... ✓ Successfully set MAC address
|
|
306
|
+
```
|
|
307
|
+
|
|
162
308
|
## Platform Support
|
|
163
309
|
|
|
164
310
|
### macOS ✅
|
package/bin/cmd.js
CHANGED
|
@@ -5,15 +5,105 @@ const minimist = require("minimist");
|
|
|
5
5
|
const spoof = require("../");
|
|
6
6
|
const { stripIndent } = require("common-tags");
|
|
7
7
|
const cp = require("child_process");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const ora = require("ora");
|
|
12
|
+
const history = require("../lib/history");
|
|
13
|
+
const oui = require("../lib/oui");
|
|
8
14
|
|
|
9
15
|
const argv = minimist(process.argv.slice(2), {
|
|
10
16
|
alias: {
|
|
11
17
|
v: "version",
|
|
18
|
+
V: "verbose",
|
|
19
|
+
j: "json",
|
|
12
20
|
},
|
|
13
|
-
boolean: ["version"],
|
|
21
|
+
boolean: ["version", "verbose", "json"],
|
|
14
22
|
});
|
|
15
23
|
const cmd = argv._[0];
|
|
16
24
|
|
|
25
|
+
// Global flags
|
|
26
|
+
const VERBOSE = argv.verbose || false;
|
|
27
|
+
const JSON_OUTPUT = argv.json || false;
|
|
28
|
+
|
|
29
|
+
// Configuration file support
|
|
30
|
+
let config = null;
|
|
31
|
+
const configPath = path.join(os.homedir(), ".spoofyrc");
|
|
32
|
+
if (fs.existsSync(configPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const configContent = fs.readFileSync(configPath, "utf8");
|
|
35
|
+
config = JSON.parse(configContent);
|
|
36
|
+
if (VERBOSE) {
|
|
37
|
+
logVerbose(`Loaded configuration from ${configPath}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (VERBOSE) {
|
|
41
|
+
logVerbose(`Failed to load config: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Helper functions
|
|
47
|
+
function logVerbose(message) {
|
|
48
|
+
if (VERBOSE && !JSON_OUTPUT) {
|
|
49
|
+
console.error(chalk.gray(`[VERBOSE] ${message}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function outputJSON(data) {
|
|
54
|
+
if (JSON_OUTPUT) {
|
|
55
|
+
console.log(JSON.stringify(data, null, 2));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let currentSpinner = null;
|
|
60
|
+
|
|
61
|
+
function showProgress(message) {
|
|
62
|
+
if (JSON_OUTPUT) return;
|
|
63
|
+
if (currentSpinner) {
|
|
64
|
+
currentSpinner.text = message;
|
|
65
|
+
} else {
|
|
66
|
+
currentSpinner = ora(message).start();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hideProgress() {
|
|
71
|
+
if (JSON_OUTPUT) return;
|
|
72
|
+
if (currentSpinner) {
|
|
73
|
+
currentSpinner.stop();
|
|
74
|
+
currentSpinner = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function successProgress(message) {
|
|
79
|
+
if (JSON_OUTPUT) return;
|
|
80
|
+
if (currentSpinner) {
|
|
81
|
+
currentSpinner.succeed(message);
|
|
82
|
+
currentSpinner = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function failProgress(message) {
|
|
87
|
+
if (JSON_OUTPUT) return;
|
|
88
|
+
if (currentSpinner) {
|
|
89
|
+
currentSpinner.fail(message);
|
|
90
|
+
currentSpinner = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function progressStep(step, total, message) {
|
|
95
|
+
if (JSON_OUTPUT) return;
|
|
96
|
+
const percentage = Math.round((step / total) * 100);
|
|
97
|
+
if (currentSpinner) {
|
|
98
|
+
currentSpinner.text = `[${step}/${total}] ${percentage}% - ${message}`;
|
|
99
|
+
} else {
|
|
100
|
+
currentSpinner = ora(`[${step}/${total}] ${percentage}% - ${message}`).start();
|
|
101
|
+
}
|
|
102
|
+
if (step === total) {
|
|
103
|
+
currentSpinner = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
17
107
|
try {
|
|
18
108
|
init();
|
|
19
109
|
} catch (err) {
|
|
@@ -78,6 +168,20 @@ function init() {
|
|
|
78
168
|
} else if (cmd === "normalize") {
|
|
79
169
|
const mac = argv._[1];
|
|
80
170
|
normalize(mac);
|
|
171
|
+
} else if (cmd === "info") {
|
|
172
|
+
const device = argv._[1];
|
|
173
|
+
info(device);
|
|
174
|
+
} else if (cmd === "validate") {
|
|
175
|
+
const mac = argv._[1];
|
|
176
|
+
validate(mac);
|
|
177
|
+
} else if (cmd === "vendor") {
|
|
178
|
+
const mac = argv._[1];
|
|
179
|
+
vendor(mac);
|
|
180
|
+
} else if (cmd === "batch") {
|
|
181
|
+
const file = argv._[1];
|
|
182
|
+
batch(file);
|
|
183
|
+
} else if (cmd === "history") {
|
|
184
|
+
historyCmd();
|
|
81
185
|
} else {
|
|
82
186
|
help();
|
|
83
187
|
}
|
|
@@ -114,9 +218,23 @@ ${example}${note}
|
|
|
114
218
|
spoofy help Shows this help message.
|
|
115
219
|
spoofy version | --version | -v Show package version.
|
|
116
220
|
|
|
221
|
+
Commands:
|
|
222
|
+
list [--wifi] List available devices.
|
|
223
|
+
set <mac> <devices>... Set device MAC address.
|
|
224
|
+
randomize [--local] <devices>... Set device MAC address randomly.
|
|
225
|
+
reset <devices>... Reset device MAC address to default.
|
|
226
|
+
normalize <mac> Normalize a MAC address format.
|
|
227
|
+
info <device> Show detailed interface information.
|
|
228
|
+
validate <mac> Validate MAC address format.
|
|
229
|
+
vendor <mac> Look up vendor from MAC address.
|
|
230
|
+
batch <file> Change multiple interfaces from config file.
|
|
231
|
+
history View MAC address change history.
|
|
232
|
+
|
|
117
233
|
Options:
|
|
118
234
|
--wifi Try to only show wireless interfaces.
|
|
119
235
|
--local Set the locally administered flag on randomized MACs.
|
|
236
|
+
--verbose, -V Show verbose output for debugging.
|
|
237
|
+
--json, -j Output results in JSON format.
|
|
120
238
|
|
|
121
239
|
Platform Support:
|
|
122
240
|
✅ macOS (Sequoia 15.4+, Tahoe 26+)
|
|
@@ -127,7 +245,16 @@ ${example}${note}
|
|
|
127
245
|
}
|
|
128
246
|
|
|
129
247
|
function version() {
|
|
130
|
-
|
|
248
|
+
const pkg = require("../package.json");
|
|
249
|
+
if (JSON_OUTPUT) {
|
|
250
|
+
outputJSON({
|
|
251
|
+
version: pkg.version,
|
|
252
|
+
name: pkg.name,
|
|
253
|
+
description: pkg.description,
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
console.log(pkg.version);
|
|
257
|
+
}
|
|
131
258
|
}
|
|
132
259
|
|
|
133
260
|
function set(mac, devices) {
|
|
@@ -139,8 +266,14 @@ function set(mac, devices) {
|
|
|
139
266
|
throw new Error("Device name is required. Usage: spoofy set <mac> <device>");
|
|
140
267
|
}
|
|
141
268
|
|
|
142
|
-
devices.
|
|
269
|
+
logVerbose(`Setting MAC address ${mac} on ${devices.length} device(s)`);
|
|
270
|
+
|
|
271
|
+
devices.forEach((device, index) => {
|
|
272
|
+
logVerbose(`Processing device ${index + 1}/${devices.length}: ${device}`);
|
|
273
|
+
showProgress(`Finding interface ${device}`);
|
|
274
|
+
|
|
143
275
|
const it = spoof.findInterface(device);
|
|
276
|
+
hideProgress();
|
|
144
277
|
|
|
145
278
|
if (!it) {
|
|
146
279
|
throw new Error(
|
|
@@ -149,6 +282,7 @@ function set(mac, devices) {
|
|
|
149
282
|
);
|
|
150
283
|
}
|
|
151
284
|
|
|
285
|
+
logVerbose(`Found interface: ${it.device} (port: ${it.port})`);
|
|
152
286
|
setMACAddress(it.device, mac, it.port);
|
|
153
287
|
});
|
|
154
288
|
}
|
|
@@ -158,13 +292,34 @@ function normalize(mac) {
|
|
|
158
292
|
throw new Error("MAC address is required. Usage: spoofy normalize <mac>");
|
|
159
293
|
}
|
|
160
294
|
|
|
295
|
+
logVerbose(`Normalizing MAC address: ${mac}`);
|
|
296
|
+
|
|
161
297
|
try {
|
|
162
298
|
const normalized = spoof.normalize(mac);
|
|
163
299
|
if (!normalized) {
|
|
164
300
|
throw new Error(`"${mac}" is not a valid MAC address`);
|
|
165
301
|
}
|
|
166
|
-
|
|
302
|
+
|
|
303
|
+
if (JSON_OUTPUT) {
|
|
304
|
+
outputJSON({
|
|
305
|
+
original: mac,
|
|
306
|
+
normalized: normalized,
|
|
307
|
+
valid: true,
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
console.log(normalized);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
logVerbose(`Normalized: ${mac} -> ${normalized}`);
|
|
167
314
|
} catch (err) {
|
|
315
|
+
if (JSON_OUTPUT) {
|
|
316
|
+
outputJSON({
|
|
317
|
+
original: mac,
|
|
318
|
+
normalized: null,
|
|
319
|
+
valid: false,
|
|
320
|
+
error: err.message,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
168
323
|
throw new Error(
|
|
169
324
|
`Could not normalize MAC address "${mac}": ${err.message}`
|
|
170
325
|
);
|
|
@@ -176,8 +331,15 @@ function randomize(devices) {
|
|
|
176
331
|
throw new Error("Device name is required. Usage: spoofy randomize <device>");
|
|
177
332
|
}
|
|
178
333
|
|
|
179
|
-
|
|
334
|
+
const useLocal = argv.local || (config && config.randomize && config.randomize.local);
|
|
335
|
+
logVerbose(`Randomizing MAC address (local: ${useLocal})`);
|
|
336
|
+
|
|
337
|
+
devices.forEach((device, index) => {
|
|
338
|
+
logVerbose(`Processing device ${index + 1}/${devices.length}: ${device}`);
|
|
339
|
+
showProgress(`Finding interface ${device}`);
|
|
340
|
+
|
|
180
341
|
const it = spoof.findInterface(device);
|
|
342
|
+
hideProgress();
|
|
181
343
|
|
|
182
344
|
if (!it) {
|
|
183
345
|
throw new Error(
|
|
@@ -186,9 +348,16 @@ function randomize(devices) {
|
|
|
186
348
|
);
|
|
187
349
|
}
|
|
188
350
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
351
|
+
logVerbose(`Found interface: ${it.device} (port: ${it.port})`);
|
|
352
|
+
const mac = spoof.randomize(useLocal);
|
|
353
|
+
|
|
354
|
+
if (JSON_OUTPUT) {
|
|
355
|
+
// Will be output in setMACAddress
|
|
356
|
+
} else {
|
|
357
|
+
console.log(chalk.blue("ℹ"), `Generated random MAC address: ${chalk.bold.cyan(mac)}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
setMACAddress(it.device, mac, it.port, "randomize");
|
|
192
361
|
});
|
|
193
362
|
}
|
|
194
363
|
|
|
@@ -197,8 +366,14 @@ function reset(devices) {
|
|
|
197
366
|
throw new Error("Device name is required. Usage: spoofy reset <device>");
|
|
198
367
|
}
|
|
199
368
|
|
|
200
|
-
devices.
|
|
369
|
+
logVerbose(`Resetting MAC address on ${devices.length} device(s)`);
|
|
370
|
+
|
|
371
|
+
devices.forEach((device, index) => {
|
|
372
|
+
logVerbose(`Processing device ${index + 1}/${devices.length}: ${device}`);
|
|
373
|
+
showProgress(`Finding interface ${device}`);
|
|
374
|
+
|
|
201
375
|
const it = spoof.findInterface(device);
|
|
376
|
+
hideProgress();
|
|
202
377
|
|
|
203
378
|
if (!it) {
|
|
204
379
|
throw new Error(
|
|
@@ -214,12 +389,22 @@ function reset(devices) {
|
|
|
214
389
|
);
|
|
215
390
|
}
|
|
216
391
|
|
|
217
|
-
|
|
218
|
-
|
|
392
|
+
logVerbose(`Hardware MAC address: ${it.address}`);
|
|
393
|
+
|
|
394
|
+
if (JSON_OUTPUT) {
|
|
395
|
+
// Will be output in setMACAddress
|
|
396
|
+
} else {
|
|
397
|
+
console.log(chalk.blue("ℹ"), `Resetting to hardware MAC address: ${chalk.bold.cyan(it.address)}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
setMACAddress(it.device, it.address, it.port, "reset");
|
|
219
401
|
});
|
|
220
402
|
}
|
|
221
403
|
|
|
222
404
|
function list() {
|
|
405
|
+
logVerbose("Starting interface discovery...");
|
|
406
|
+
showProgress("Discovering network interfaces");
|
|
407
|
+
|
|
223
408
|
const targets = [];
|
|
224
409
|
if (argv.wifi) {
|
|
225
410
|
if (process.platform === "win32") {
|
|
@@ -230,6 +415,23 @@ function list() {
|
|
|
230
415
|
}
|
|
231
416
|
|
|
232
417
|
const interfaces = spoof.findInterfaces(targets);
|
|
418
|
+
hideProgress();
|
|
419
|
+
logVerbose(`Found ${interfaces.length} interface(s)`);
|
|
420
|
+
|
|
421
|
+
if (JSON_OUTPUT) {
|
|
422
|
+
outputJSON({
|
|
423
|
+
interfaces: interfaces.map((it) => ({
|
|
424
|
+
port: it.port || it.device,
|
|
425
|
+
device: it.device,
|
|
426
|
+
address: it.address,
|
|
427
|
+
currentAddress: it.currentAddress,
|
|
428
|
+
status: it.status,
|
|
429
|
+
description: it.description,
|
|
430
|
+
})),
|
|
431
|
+
count: interfaces.length,
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
233
435
|
|
|
234
436
|
if (interfaces.length === 0) {
|
|
235
437
|
console.log(chalk.yellow("No network interfaces found."));
|
|
@@ -251,17 +453,31 @@ function list() {
|
|
|
251
453
|
|
|
252
454
|
if (it.address) {
|
|
253
455
|
line.push("with MAC address", chalk.bold.cyan(it.address));
|
|
456
|
+
const vendor = oui.lookupVendor(it.address);
|
|
457
|
+
if (vendor && vendor !== "Unknown") {
|
|
458
|
+
line.push(chalk.gray(`[${vendor}]`));
|
|
459
|
+
}
|
|
254
460
|
}
|
|
255
461
|
if (it.currentAddress && it.currentAddress !== it.address) {
|
|
256
462
|
line.push("currently set to", chalk.bold.red(it.currentAddress));
|
|
463
|
+
const currentVendor = oui.lookupVendor(it.currentAddress);
|
|
464
|
+
if (currentVendor && currentVendor !== "Unknown") {
|
|
465
|
+
line.push(chalk.gray(`[${currentVendor}]`));
|
|
466
|
+
}
|
|
257
467
|
}
|
|
258
468
|
console.log(line.join(" "));
|
|
259
469
|
});
|
|
260
470
|
}
|
|
261
471
|
|
|
262
|
-
function setMACAddress(device, mac, port) {
|
|
472
|
+
function setMACAddress(device, mac, port, operation = "set") {
|
|
473
|
+
logVerbose(`Setting MAC address ${mac} on device ${device}`);
|
|
474
|
+
|
|
475
|
+
// Get current MAC for history
|
|
476
|
+
const oldMac = spoof.getInterfaceMAC(device);
|
|
477
|
+
|
|
263
478
|
// Check for admin/root privileges
|
|
264
479
|
if (process.platform === "win32") {
|
|
480
|
+
logVerbose("Checking for Administrator privileges...");
|
|
265
481
|
// On Windows, check if running as administrator
|
|
266
482
|
try {
|
|
267
483
|
const output = cp
|
|
@@ -279,35 +495,355 @@ function setMACAddress(device, mac, port) {
|
|
|
279
495
|
"Right-click Command Prompt or PowerShell and select 'Run as Administrator'"
|
|
280
496
|
);
|
|
281
497
|
}
|
|
498
|
+
logVerbose("Administrator privileges confirmed");
|
|
282
499
|
} catch (err) {
|
|
283
500
|
if (err.message.includes("Must run as Administrator")) {
|
|
284
501
|
throw err;
|
|
285
502
|
}
|
|
286
503
|
// If check fails, warn but continue (might work anyway)
|
|
287
|
-
|
|
504
|
+
if (!JSON_OUTPUT) {
|
|
505
|
+
console.warn(chalk.yellow("Warning: Could not verify administrator privileges. Operation may fail."));
|
|
506
|
+
}
|
|
507
|
+
logVerbose("Could not verify privileges, continuing anyway");
|
|
288
508
|
}
|
|
289
509
|
} else if (process.platform !== "win32") {
|
|
510
|
+
logVerbose("Checking for root privileges...");
|
|
290
511
|
// Unix-like systems (macOS, Linux)
|
|
291
512
|
if (process.getuid && process.getuid() !== 0) {
|
|
292
513
|
throw new Error(
|
|
293
514
|
"Must run as root (or using sudo) to change network settings"
|
|
294
515
|
);
|
|
295
516
|
}
|
|
517
|
+
logVerbose("Root privileges confirmed");
|
|
296
518
|
}
|
|
297
519
|
|
|
298
520
|
try {
|
|
521
|
+
showProgress("Changing MAC address");
|
|
522
|
+
logVerbose(`Calling setInterfaceMAC(${device}, ${mac}, ${port})`);
|
|
523
|
+
|
|
299
524
|
spoof.setInterfaceMAC(device, mac, port);
|
|
300
|
-
console.log(
|
|
301
|
-
chalk.green("✓") +
|
|
302
|
-
" Successfully set MAC address to " +
|
|
303
|
-
chalk.bold.cyan(mac) +
|
|
304
|
-
" on " +
|
|
305
|
-
chalk.bold.green(device)
|
|
306
|
-
);
|
|
307
525
|
|
|
526
|
+
// Log to history
|
|
527
|
+
history.addHistoryEntry(device, oldMac, mac, operation);
|
|
528
|
+
|
|
529
|
+
successProgress("MAC address changed successfully");
|
|
530
|
+
|
|
531
|
+
if (JSON_OUTPUT) {
|
|
532
|
+
outputJSON({
|
|
533
|
+
success: true,
|
|
534
|
+
device: device,
|
|
535
|
+
mac: mac,
|
|
536
|
+
oldMac: oldMac,
|
|
537
|
+
message: "MAC address changed successfully",
|
|
538
|
+
});
|
|
539
|
+
} else {
|
|
540
|
+
console.log(
|
|
541
|
+
chalk.green("✓") +
|
|
542
|
+
" Successfully set MAC address to " +
|
|
543
|
+
chalk.bold.cyan(mac) +
|
|
544
|
+
" on " +
|
|
545
|
+
chalk.bold.green(device)
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
logVerbose("MAC address change completed successfully");
|
|
308
550
|
// Note: Verification is already done in setInterfaceMAC, so we don't need to do it again here
|
|
309
551
|
} catch (err) {
|
|
552
|
+
failProgress("Failed to change MAC address");
|
|
553
|
+
if (JSON_OUTPUT) {
|
|
554
|
+
outputJSON({
|
|
555
|
+
success: false,
|
|
556
|
+
device: device,
|
|
557
|
+
mac: mac,
|
|
558
|
+
error: err.message,
|
|
559
|
+
code: err.code,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
310
562
|
// Error is already formatted by handleError in the catch block above
|
|
311
563
|
throw err;
|
|
312
564
|
}
|
|
313
565
|
}
|
|
566
|
+
|
|
567
|
+
function info(device) {
|
|
568
|
+
if (!device) {
|
|
569
|
+
throw new Error("Device name is required. Usage: spoofy info <device>");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
logVerbose(`Getting info for device: ${device}`);
|
|
573
|
+
showProgress("Fetching interface information");
|
|
574
|
+
|
|
575
|
+
const it = spoof.findInterface(device);
|
|
576
|
+
hideProgress();
|
|
577
|
+
|
|
578
|
+
if (!it) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Could not find device "${device}". ` +
|
|
581
|
+
"List available devices using: spoofy list"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const currentMac = spoof.getInterfaceMAC(device);
|
|
586
|
+
const vendorInfo = it.address ? oui.getVendorInfo(it.address) : null;
|
|
587
|
+
const currentVendorInfo = currentMac ? oui.getVendorInfo(currentMac) : null;
|
|
588
|
+
const deviceHistory = history.getHistoryForDevice(device);
|
|
589
|
+
|
|
590
|
+
if (JSON_OUTPUT) {
|
|
591
|
+
outputJSON({
|
|
592
|
+
device: it.device,
|
|
593
|
+
port: it.port || it.device,
|
|
594
|
+
description: it.description,
|
|
595
|
+
hardwareMac: it.address,
|
|
596
|
+
currentMac: currentMac,
|
|
597
|
+
hardwareVendor: vendorInfo ? vendorInfo.vendor : null,
|
|
598
|
+
currentVendor: currentVendorInfo ? currentVendorInfo.vendor : null,
|
|
599
|
+
status: it.status,
|
|
600
|
+
platform: process.platform,
|
|
601
|
+
historyCount: deviceHistory.length,
|
|
602
|
+
lastChange: deviceHistory[0] || null,
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
console.log(chalk.bold.cyan("\nInterface Information"));
|
|
608
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
609
|
+
console.log(chalk.bold("Device:"), it.device);
|
|
610
|
+
console.log(chalk.bold("Port:"), it.port || it.device);
|
|
611
|
+
if (it.description) {
|
|
612
|
+
console.log(chalk.bold("Description:"), it.description);
|
|
613
|
+
}
|
|
614
|
+
if (it.status) {
|
|
615
|
+
console.log(chalk.bold("Status:"), it.status);
|
|
616
|
+
}
|
|
617
|
+
console.log(chalk.bold("Platform:"), process.platform);
|
|
618
|
+
|
|
619
|
+
if (it.address) {
|
|
620
|
+
console.log(chalk.bold("\nHardware MAC Address:"), chalk.cyan(it.address));
|
|
621
|
+
if (vendorInfo && vendorInfo.vendor !== "Unknown") {
|
|
622
|
+
console.log(chalk.bold("Hardware Vendor:"), chalk.green(vendorInfo.vendor));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (currentMac) {
|
|
627
|
+
console.log(chalk.bold("Current MAC Address:"), chalk.cyan(currentMac));
|
|
628
|
+
if (currentVendorInfo && currentVendorInfo.vendor !== "Unknown") {
|
|
629
|
+
console.log(chalk.bold("Current Vendor:"), chalk.green(currentVendorInfo.vendor));
|
|
630
|
+
}
|
|
631
|
+
if (currentMac !== it.address) {
|
|
632
|
+
console.log(chalk.yellow("⚠ MAC address has been changed from hardware address"));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (deviceHistory.length > 0) {
|
|
637
|
+
console.log(chalk.bold("\nChange History:"), `(${deviceHistory.length} entries)`);
|
|
638
|
+
deviceHistory.slice(0, 5).forEach((entry, index) => {
|
|
639
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
640
|
+
console.log(` ${index + 1}. ${date} - ${entry.operation}: ${entry.oldMac || "N/A"} → ${entry.newMac}`);
|
|
641
|
+
});
|
|
642
|
+
if (deviceHistory.length > 5) {
|
|
643
|
+
console.log(chalk.gray(` ... and ${deviceHistory.length - 5} more entries`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
console.log();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function validate(mac) {
|
|
650
|
+
if (!mac) {
|
|
651
|
+
throw new Error("MAC address is required. Usage: spoofy validate <mac>");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
logVerbose(`Validating MAC address: ${mac}`);
|
|
655
|
+
|
|
656
|
+
const normalized = spoof.normalize(mac);
|
|
657
|
+
const isValid = !!normalized;
|
|
658
|
+
const vendorInfo = normalized ? oui.getVendorInfo(normalized) : null;
|
|
659
|
+
|
|
660
|
+
if (JSON_OUTPUT) {
|
|
661
|
+
outputJSON({
|
|
662
|
+
original: mac,
|
|
663
|
+
normalized: normalized,
|
|
664
|
+
valid: isValid,
|
|
665
|
+
vendor: vendorInfo ? vendorInfo.vendor : null,
|
|
666
|
+
prefix: vendorInfo ? vendorInfo.prefix : null,
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (isValid) {
|
|
672
|
+
console.log(chalk.green("✓ Valid MAC address"));
|
|
673
|
+
console.log(chalk.bold("Normalized:"), normalized);
|
|
674
|
+
if (vendorInfo && vendorInfo.vendor !== "Unknown") {
|
|
675
|
+
console.log(chalk.bold("Vendor:"), chalk.green(vendorInfo.vendor));
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
console.log(chalk.red("✗ Invalid MAC address"));
|
|
679
|
+
console.log(chalk.yellow("Expected format: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX"));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function vendor(mac) {
|
|
684
|
+
if (!mac) {
|
|
685
|
+
throw new Error("MAC address is required. Usage: spoofy vendor <mac>");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
logVerbose(`Looking up vendor for MAC: ${mac}`);
|
|
689
|
+
|
|
690
|
+
const normalized = spoof.normalize(mac);
|
|
691
|
+
if (!normalized) {
|
|
692
|
+
throw new Error(`"${mac}" is not a valid MAC address`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const vendorInfo = oui.getVendorInfo(normalized);
|
|
696
|
+
|
|
697
|
+
if (JSON_OUTPUT) {
|
|
698
|
+
outputJSON({
|
|
699
|
+
mac: normalized,
|
|
700
|
+
vendor: vendorInfo.vendor,
|
|
701
|
+
prefix: vendorInfo.prefix,
|
|
702
|
+
found: vendorInfo.vendor !== "Unknown",
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
console.log(chalk.bold("MAC Address:"), normalized);
|
|
708
|
+
console.log(chalk.bold("Vendor:"), vendorInfo.vendor !== "Unknown" ? chalk.green(vendorInfo.vendor) : chalk.yellow("Unknown"));
|
|
709
|
+
console.log(chalk.bold("Prefix:"), vendorInfo.prefix);
|
|
710
|
+
|
|
711
|
+
if (vendorInfo.vendor === "Unknown") {
|
|
712
|
+
console.log(chalk.gray("\nNote: Vendor not found in database. This may be a locally administered address."));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function batch(file) {
|
|
717
|
+
if (!file) {
|
|
718
|
+
throw new Error("Batch file is required. Usage: spoofy batch <file>");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!fs.existsSync(file)) {
|
|
722
|
+
throw new Error(`Batch file not found: ${file}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
logVerbose(`Loading batch file: ${file}`);
|
|
726
|
+
showProgress("Loading batch configuration");
|
|
727
|
+
|
|
728
|
+
let batchConfig;
|
|
729
|
+
try {
|
|
730
|
+
const content = fs.readFileSync(file, "utf8");
|
|
731
|
+
batchConfig = JSON.parse(content);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
hideProgress();
|
|
734
|
+
throw new Error(`Failed to parse batch file: ${err.message}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
hideProgress();
|
|
738
|
+
|
|
739
|
+
if (!Array.isArray(batchConfig)) {
|
|
740
|
+
throw new Error("Batch file must contain an array of operations");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
logVerbose(`Found ${batchConfig.length} operation(s) in batch file`);
|
|
744
|
+
|
|
745
|
+
const results = [];
|
|
746
|
+
let successCount = 0;
|
|
747
|
+
let failCount = 0;
|
|
748
|
+
|
|
749
|
+
batchConfig.forEach((operation, index) => {
|
|
750
|
+
const step = index + 1;
|
|
751
|
+
const total = batchConfig.length;
|
|
752
|
+
progressStep(step, total, `Processing operation ${step}`);
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
if (operation.type === "set" && operation.device && operation.mac) {
|
|
756
|
+
const it = spoof.findInterface(operation.device);
|
|
757
|
+
if (!it) {
|
|
758
|
+
throw new Error(`Device not found: ${operation.device}`);
|
|
759
|
+
}
|
|
760
|
+
setMACAddress(it.device, operation.mac, it.port, "batch-set");
|
|
761
|
+
results.push({ success: true, operation: operation, index: index });
|
|
762
|
+
successCount++;
|
|
763
|
+
} else if (operation.type === "randomize" && operation.device) {
|
|
764
|
+
const it = spoof.findInterface(operation.device);
|
|
765
|
+
if (!it) {
|
|
766
|
+
throw new Error(`Device not found: ${operation.device}`);
|
|
767
|
+
}
|
|
768
|
+
const mac = spoof.randomize(operation.local || false);
|
|
769
|
+
setMACAddress(it.device, mac, it.port, "batch-randomize");
|
|
770
|
+
results.push({ success: true, operation: operation, mac: mac, index: index });
|
|
771
|
+
successCount++;
|
|
772
|
+
} else if (operation.type === "reset" && operation.device) {
|
|
773
|
+
const it = spoof.findInterface(operation.device);
|
|
774
|
+
if (!it) {
|
|
775
|
+
throw new Error(`Device not found: ${operation.device}`);
|
|
776
|
+
}
|
|
777
|
+
if (!it.address) {
|
|
778
|
+
throw new Error(`No hardware MAC address for: ${operation.device}`);
|
|
779
|
+
}
|
|
780
|
+
setMACAddress(it.device, it.address, it.port, "batch-reset");
|
|
781
|
+
results.push({ success: true, operation: operation, index: index });
|
|
782
|
+
successCount++;
|
|
783
|
+
} else {
|
|
784
|
+
throw new Error(`Invalid operation type or missing parameters: ${JSON.stringify(operation)}`);
|
|
785
|
+
}
|
|
786
|
+
} catch (err) {
|
|
787
|
+
results.push({ success: false, operation: operation, error: err.message, index: index });
|
|
788
|
+
failCount++;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
progressStep(batchConfig.length, batchConfig.length, "Completed");
|
|
793
|
+
|
|
794
|
+
if (JSON_OUTPUT) {
|
|
795
|
+
outputJSON({
|
|
796
|
+
total: batchConfig.length,
|
|
797
|
+
success: successCount,
|
|
798
|
+
failed: failCount,
|
|
799
|
+
results: results,
|
|
800
|
+
});
|
|
801
|
+
} else {
|
|
802
|
+
console.log(chalk.bold("\nBatch Operation Summary:"));
|
|
803
|
+
console.log(chalk.green(` ✓ Successful: ${successCount}`));
|
|
804
|
+
if (failCount > 0) {
|
|
805
|
+
console.log(chalk.red(` ✗ Failed: ${failCount}`));
|
|
806
|
+
}
|
|
807
|
+
console.log(chalk.bold(` Total: ${batchConfig.length}`));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function historyCmd() {
|
|
812
|
+
const device = argv._[1]; // Optional device filter
|
|
813
|
+
logVerbose(device ? `Getting history for device: ${device}` : "Getting all history");
|
|
814
|
+
|
|
815
|
+
const allHistory = history.getHistory();
|
|
816
|
+
const deviceHistory = device ? history.getHistoryForDevice(device) : allHistory;
|
|
817
|
+
|
|
818
|
+
if (deviceHistory.length === 0) {
|
|
819
|
+
if (JSON_OUTPUT) {
|
|
820
|
+
outputJSON({ history: [], count: 0 });
|
|
821
|
+
} else {
|
|
822
|
+
console.log(chalk.yellow("No history found" + (device ? ` for device "${device}"` : "")));
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (JSON_OUTPUT) {
|
|
828
|
+
outputJSON({
|
|
829
|
+
history: deviceHistory,
|
|
830
|
+
count: deviceHistory.length,
|
|
831
|
+
device: device || "all",
|
|
832
|
+
});
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
console.log(chalk.bold.cyan("\nMAC Address Change History"));
|
|
837
|
+
console.log(chalk.gray("─".repeat(80)));
|
|
838
|
+
|
|
839
|
+
deviceHistory.forEach((entry, index) => {
|
|
840
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
841
|
+
console.log(chalk.bold(`\n${index + 1}. ${date}`));
|
|
842
|
+
console.log(` Device: ${chalk.green(entry.device)}`);
|
|
843
|
+
console.log(` Operation: ${chalk.cyan(entry.operation)}`);
|
|
844
|
+
console.log(` ${chalk.gray(entry.oldMac || "N/A")} → ${chalk.cyan(entry.newMac)}`);
|
|
845
|
+
console.log(` Platform: ${entry.platform}`);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
console.log();
|
|
849
|
+
}
|
package/index.js
CHANGED
|
@@ -6,8 +6,48 @@ module.exports = {
|
|
|
6
6
|
randomize,
|
|
7
7
|
setInterfaceMAC,
|
|
8
8
|
getInterfaceMAC,
|
|
9
|
+
validateMAC,
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Validates a MAC address format
|
|
14
|
+
* @param {string} mac
|
|
15
|
+
* @return {Object} {valid: boolean, normalized: string|null, error: string|null}
|
|
16
|
+
*/
|
|
17
|
+
function validateMAC(mac) {
|
|
18
|
+
if (!mac || typeof mac !== "string") {
|
|
19
|
+
return {
|
|
20
|
+
valid: false,
|
|
21
|
+
normalized: null,
|
|
22
|
+
error: "MAC address must be a non-empty string",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalized = normalize(mac);
|
|
27
|
+
if (!normalized) {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
normalized: null,
|
|
31
|
+
error: "Invalid MAC address format",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for invalid addresses
|
|
36
|
+
if (normalized === "00:00:00:00:00:00" || normalized === "FF:FF:FF:FF:FF:FF") {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
normalized: normalized,
|
|
40
|
+
error: "Cannot be all zeros or broadcast address",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
valid: true,
|
|
46
|
+
normalized: normalized,
|
|
47
|
+
error: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
11
51
|
const cp = require("child_process");
|
|
12
52
|
const quote = require("shell-quote").quote;
|
|
13
53
|
const zeroFill = require("zero-fill");
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
const HISTORY_FILE = path.join(os.homedir(), ".spoofy_history.json");
|
|
6
|
+
|
|
7
|
+
function getHistory() {
|
|
8
|
+
if (!fs.existsSync(HISTORY_FILE)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const content = fs.readFileSync(HISTORY_FILE, "utf8");
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function saveHistory(history) {
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), "utf8");
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// Silently fail if we can't write history
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function addHistoryEntry(device, oldMac, newMac, operation) {
|
|
28
|
+
const history = getHistory();
|
|
29
|
+
const entry = {
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
device: device,
|
|
32
|
+
oldMac: oldMac,
|
|
33
|
+
newMac: newMac,
|
|
34
|
+
operation: operation, // 'set', 'randomize', 'reset'
|
|
35
|
+
platform: process.platform,
|
|
36
|
+
};
|
|
37
|
+
history.unshift(entry); // Add to beginning
|
|
38
|
+
// Keep only last 100 entries
|
|
39
|
+
if (history.length > 100) {
|
|
40
|
+
history.splice(100);
|
|
41
|
+
}
|
|
42
|
+
saveHistory(history);
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getHistoryForDevice(device) {
|
|
47
|
+
const history = getHistory();
|
|
48
|
+
return history.filter((entry) => entry.device === device);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getLastEntryForDevice(device) {
|
|
52
|
+
const history = getHistory();
|
|
53
|
+
return history.find((entry) => entry.device === device);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
getHistory,
|
|
58
|
+
addHistoryEntry,
|
|
59
|
+
getHistoryForDevice,
|
|
60
|
+
getLastEntryForDevice,
|
|
61
|
+
HISTORY_FILE,
|
|
62
|
+
};
|
package/lib/oui.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// OUI (Organizationally Unique Identifier) database
|
|
2
|
+
// Common MAC address vendor prefixes
|
|
3
|
+
// This is a subset - can be expanded with full IEEE OUI database
|
|
4
|
+
|
|
5
|
+
const OUI_DB = {
|
|
6
|
+
"00:05:69": "VMware",
|
|
7
|
+
"00:0C:29": "VMware",
|
|
8
|
+
"00:50:56": "VMware",
|
|
9
|
+
"00:16:3E": "Xen",
|
|
10
|
+
"00:03:FF": "Microsoft Hyper-V",
|
|
11
|
+
"00:1C:42": "Parallels",
|
|
12
|
+
"00:0F:4B": "Virtual Iron",
|
|
13
|
+
"08:00:27": "VirtualBox",
|
|
14
|
+
"00:1B:21": "Intel",
|
|
15
|
+
"00:1E:67": "Intel",
|
|
16
|
+
"00:21:70": "Intel",
|
|
17
|
+
"00:25:00": "Intel",
|
|
18
|
+
"00:26:B9": "Intel",
|
|
19
|
+
"00:1A:79": "Intel",
|
|
20
|
+
"00:15:17": "Intel",
|
|
21
|
+
"00:13:CE": "Intel",
|
|
22
|
+
"00:0D:3A": "Intel",
|
|
23
|
+
"00:0E:0C": "Intel",
|
|
24
|
+
"00:11:11": "Intel",
|
|
25
|
+
"00:14:4F": "Intel",
|
|
26
|
+
"00:16:76": "Intel",
|
|
27
|
+
"00:18:DE": "Intel",
|
|
28
|
+
"00:1B:77": "Intel",
|
|
29
|
+
"00:1C:C0": "Intel",
|
|
30
|
+
"00:1D:7E": "Intel",
|
|
31
|
+
"00:1E:67": "Intel",
|
|
32
|
+
"00:21:5C": "Intel",
|
|
33
|
+
"00:23:14": "Intel",
|
|
34
|
+
"00:23:DF": "Intel",
|
|
35
|
+
"00:24:D6": "Intel",
|
|
36
|
+
"00:25:00": "Intel",
|
|
37
|
+
"00:26:18": "Intel",
|
|
38
|
+
"00:26:4A": "Intel",
|
|
39
|
+
"00:26:B9": "Intel",
|
|
40
|
+
"00:26:C7": "Intel",
|
|
41
|
+
"00:27:13": "Intel",
|
|
42
|
+
"00:50:56": "VMware",
|
|
43
|
+
"00:0C:29": "VMware",
|
|
44
|
+
"00:05:69": "VMware",
|
|
45
|
+
"00:1B:21": "Intel",
|
|
46
|
+
"00:1E:67": "Intel",
|
|
47
|
+
"00:21:70": "Intel",
|
|
48
|
+
"00:25:00": "Intel",
|
|
49
|
+
"00:26:B9": "Intel",
|
|
50
|
+
"00:1A:79": "Intel",
|
|
51
|
+
"00:15:17": "Intel",
|
|
52
|
+
"00:13:CE": "Intel",
|
|
53
|
+
"00:0D:3A": "Intel",
|
|
54
|
+
"00:0E:0C": "Intel",
|
|
55
|
+
"00:11:11": "Intel",
|
|
56
|
+
"00:14:4F": "Intel",
|
|
57
|
+
"00:16:76": "Intel",
|
|
58
|
+
"00:18:DE": "Intel",
|
|
59
|
+
"00:1B:77": "Intel",
|
|
60
|
+
"00:1C:C0": "Intel",
|
|
61
|
+
"00:1D:7E": "Intel",
|
|
62
|
+
"00:1E:67": "Intel",
|
|
63
|
+
"00:21:5C": "Intel",
|
|
64
|
+
"00:23:14": "Intel",
|
|
65
|
+
"00:23:DF": "Intel",
|
|
66
|
+
"00:24:D6": "Intel",
|
|
67
|
+
"00:25:00": "Intel",
|
|
68
|
+
"00:26:18": "Intel",
|
|
69
|
+
"00:26:4A": "Intel",
|
|
70
|
+
"00:26:B9": "Intel",
|
|
71
|
+
"00:26:C7": "Intel",
|
|
72
|
+
"00:27:13": "Intel",
|
|
73
|
+
"00:50:F2": "Apple",
|
|
74
|
+
"00:1E:C2": "Apple",
|
|
75
|
+
"00:23:12": "Apple",
|
|
76
|
+
"00:23:6C": "Apple",
|
|
77
|
+
"00:23:DF": "Apple",
|
|
78
|
+
"00:25:00": "Apple",
|
|
79
|
+
"00:25:4B": "Apple",
|
|
80
|
+
"00:26:08": "Apple",
|
|
81
|
+
"00:26:4A": "Apple",
|
|
82
|
+
"00:26:BB": "Apple",
|
|
83
|
+
"00:26:CA": "Apple",
|
|
84
|
+
"00:50:E4": "Apple",
|
|
85
|
+
"00:56:CD": "Apple",
|
|
86
|
+
"00:61:71": "Apple",
|
|
87
|
+
"00:6D:52": "Apple",
|
|
88
|
+
"00:88:65": "Apple",
|
|
89
|
+
"00:A0:40": "Apple",
|
|
90
|
+
"00:C6:10": "Apple",
|
|
91
|
+
"00:CD:FE": "Apple",
|
|
92
|
+
"00:F4:B9": "Apple",
|
|
93
|
+
"00:F7:6F": "Apple",
|
|
94
|
+
"04:0C:CE": "Apple",
|
|
95
|
+
"04:15:52": "Apple",
|
|
96
|
+
"04:1E:64": "Apple",
|
|
97
|
+
"04:26:65": "Apple",
|
|
98
|
+
"04:4B:ED": "Apple",
|
|
99
|
+
"04:54:53": "Apple",
|
|
100
|
+
"04:7D:7B": "Apple",
|
|
101
|
+
"04:8D:38": "Apple",
|
|
102
|
+
"04:9A:4F": "Apple",
|
|
103
|
+
"04:9F:CA": "Apple",
|
|
104
|
+
"04:A3:16": "Apple",
|
|
105
|
+
"04:DB:56": "Apple",
|
|
106
|
+
"04:E5:36": "Apple",
|
|
107
|
+
"04:F1:3E": "Apple",
|
|
108
|
+
"04:F7:E4": "Apple",
|
|
109
|
+
"08:00:27": "VirtualBox",
|
|
110
|
+
"08:66:98": "Apple",
|
|
111
|
+
"08:70:45": "Apple",
|
|
112
|
+
"08:74:02": "Apple",
|
|
113
|
+
"08:9E:01": "Apple",
|
|
114
|
+
"08:CC:68": "Apple",
|
|
115
|
+
"0C:3E:9F": "Apple",
|
|
116
|
+
"0C:4D:E9": "Apple",
|
|
117
|
+
"0C:51:01": "Apple",
|
|
118
|
+
"0C:74:C2": "Apple",
|
|
119
|
+
"0C:BC:9F": "Apple",
|
|
120
|
+
"0C:D2:92": "Apple",
|
|
121
|
+
"0C:E5:D3": "Apple",
|
|
122
|
+
"10:93:E9": "Apple",
|
|
123
|
+
"10:9A:DD": "Apple",
|
|
124
|
+
"10:DD:B1": "Apple",
|
|
125
|
+
"14:10:9F": "Apple",
|
|
126
|
+
"14:7D:DA": "Apple",
|
|
127
|
+
"14:99:E2": "Apple",
|
|
128
|
+
"14:CC:20": "Apple",
|
|
129
|
+
"18:20:32": "Apple",
|
|
130
|
+
"18:65:90": "Apple",
|
|
131
|
+
"18:AF:61": "Apple",
|
|
132
|
+
"1C:1A:C0": "Apple",
|
|
133
|
+
"1C:AB:A7": "Apple",
|
|
134
|
+
"20:78:F0": "Apple",
|
|
135
|
+
"20:AB:37": "Apple",
|
|
136
|
+
"20:C9:D0": "Apple",
|
|
137
|
+
"24:A0:74": "Apple",
|
|
138
|
+
"24:AB:81": "Apple",
|
|
139
|
+
"24:E3:14": "Apple",
|
|
140
|
+
"28:37:37": "Apple",
|
|
141
|
+
"28:6A:B8": "Apple",
|
|
142
|
+
"28:CF:DA": "Apple",
|
|
143
|
+
"28:CF:E9": "Apple",
|
|
144
|
+
"2C:1F:23": "Apple",
|
|
145
|
+
"2C:33:7A": "Apple",
|
|
146
|
+
"2C:BE:08": "Apple",
|
|
147
|
+
"30:90:AB": "Apple",
|
|
148
|
+
"34:15:9E": "Apple",
|
|
149
|
+
"34:A3:95": "Apple",
|
|
150
|
+
"34:C0:59": "Apple",
|
|
151
|
+
"38:CA:DA": "Apple",
|
|
152
|
+
"3C:07:54": "Apple",
|
|
153
|
+
"3C:15:C2": "Apple",
|
|
154
|
+
"3C:D0:F8": "Apple",
|
|
155
|
+
"40:33:1A": "Apple",
|
|
156
|
+
"40:6C:8F": "Apple",
|
|
157
|
+
"40:A6:D9": "Apple",
|
|
158
|
+
"40:CB:C0": "Apple",
|
|
159
|
+
"44:4C:0C": "Apple",
|
|
160
|
+
"44:FB:42": "Apple",
|
|
161
|
+
"48:43:7C": "Apple",
|
|
162
|
+
"48:BF:6B": "Apple",
|
|
163
|
+
"4C:8D:79": "Apple",
|
|
164
|
+
"50:EA:D6": "Apple",
|
|
165
|
+
"54:26:96": "Apple",
|
|
166
|
+
"54:72:4F": "Apple",
|
|
167
|
+
"58:55:CA": "Apple",
|
|
168
|
+
"5C:59:48": "Apple",
|
|
169
|
+
"5C:95:AE": "Apple",
|
|
170
|
+
"60:33:4B": "Apple",
|
|
171
|
+
"60:C5:47": "Apple",
|
|
172
|
+
"64:A3:CB": "Apple",
|
|
173
|
+
"68:96:7B": "Apple",
|
|
174
|
+
"6C:40:08": "Apple",
|
|
175
|
+
"6C:72:20": "Apple",
|
|
176
|
+
"70:48:0F": "Apple",
|
|
177
|
+
"70:56:51": "Realtek",
|
|
178
|
+
"74:E2:F5": "Apple",
|
|
179
|
+
"78:31:C1": "Apple",
|
|
180
|
+
"78:4F:43": "Apple",
|
|
181
|
+
"7C:6D:62": "Apple",
|
|
182
|
+
"80:BE:05": "Apple",
|
|
183
|
+
"84:38:35": "Apple",
|
|
184
|
+
"84:FC:FE": "Apple",
|
|
185
|
+
"88:63:DF": "Apple",
|
|
186
|
+
"8C:85:90": "Apple",
|
|
187
|
+
"90:72:40": "Apple",
|
|
188
|
+
"94:E9:6A": "Apple",
|
|
189
|
+
"98:5F:D3": "Apple",
|
|
190
|
+
"9C:84:BF": "Apple",
|
|
191
|
+
"A0:99:9B": "Apple",
|
|
192
|
+
"A4:5E:60": "Apple",
|
|
193
|
+
"A4:C3:61": "Apple",
|
|
194
|
+
"A8:60:B6": "Apple",
|
|
195
|
+
"AC:1F:74": "Apple",
|
|
196
|
+
"AC:BC:32": "Apple",
|
|
197
|
+
"B0:65:BD": "Apple",
|
|
198
|
+
"B4:F0:AB": "Apple",
|
|
199
|
+
"B8:09:8A": "Apple",
|
|
200
|
+
"B8:53:AC": "Apple",
|
|
201
|
+
"BC:3B:AF": "Apple",
|
|
202
|
+
"BC:52:B7": "Apple",
|
|
203
|
+
"C0:25:E9": "Apple",
|
|
204
|
+
"C4:2C:03": "Apple",
|
|
205
|
+
"C8:1E:E7": "Apple",
|
|
206
|
+
"CC:08:E0": "Apple",
|
|
207
|
+
"D0:03:4B": "Apple",
|
|
208
|
+
"D4:9A:20": "Apple",
|
|
209
|
+
"D8:30:62": "Apple",
|
|
210
|
+
"D8:A2:5E": "Apple",
|
|
211
|
+
"DC:2B:61": "Apple",
|
|
212
|
+
"E0:AC:CB": "Apple",
|
|
213
|
+
"E4:CE:8F": "Apple",
|
|
214
|
+
"E8:40:40": "Apple",
|
|
215
|
+
"EC:35:86": "Apple",
|
|
216
|
+
"F0:18:98": "Apple",
|
|
217
|
+
"F0:DB:E2": "Apple",
|
|
218
|
+
"F4:F1:5A": "Apple",
|
|
219
|
+
"F8:1E:DF": "Apple",
|
|
220
|
+
"FC:25:3F": "Apple",
|
|
221
|
+
"00:1B:21": "Intel",
|
|
222
|
+
"00:1E:67": "Intel",
|
|
223
|
+
"00:21:70": "Intel",
|
|
224
|
+
"00:25:00": "Intel",
|
|
225
|
+
"00:26:B9": "Intel",
|
|
226
|
+
"00:1A:79": "Intel",
|
|
227
|
+
"00:15:17": "Intel",
|
|
228
|
+
"00:13:CE": "Intel",
|
|
229
|
+
"00:0D:3A": "Intel",
|
|
230
|
+
"00:0E:0C": "Intel",
|
|
231
|
+
"00:11:11": "Intel",
|
|
232
|
+
"00:14:4F": "Intel",
|
|
233
|
+
"00:16:76": "Intel",
|
|
234
|
+
"00:18:DE": "Intel",
|
|
235
|
+
"00:1B:77": "Intel",
|
|
236
|
+
"00:1C:C0": "Intel",
|
|
237
|
+
"00:1D:7E": "Intel",
|
|
238
|
+
"00:1E:67": "Intel",
|
|
239
|
+
"00:21:5C": "Intel",
|
|
240
|
+
"00:23:14": "Intel",
|
|
241
|
+
"00:23:DF": "Intel",
|
|
242
|
+
"00:24:D6": "Intel",
|
|
243
|
+
"00:25:00": "Intel",
|
|
244
|
+
"00:26:18": "Intel",
|
|
245
|
+
"00:26:4A": "Intel",
|
|
246
|
+
"00:26:B9": "Intel",
|
|
247
|
+
"00:26:C7": "Intel",
|
|
248
|
+
"00:27:13": "Intel",
|
|
249
|
+
"00:50:56": "VMware",
|
|
250
|
+
"00:0C:29": "VMware",
|
|
251
|
+
"00:05:69": "VMware",
|
|
252
|
+
"00:16:3E": "Xen",
|
|
253
|
+
"00:03:FF": "Microsoft Hyper-V",
|
|
254
|
+
"00:1C:42": "Parallels",
|
|
255
|
+
"00:0F:4B": "Virtual Iron",
|
|
256
|
+
"08:00:27": "VirtualBox",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
function lookupVendor(mac) {
|
|
260
|
+
if (!mac) return null;
|
|
261
|
+
|
|
262
|
+
// Normalize MAC address
|
|
263
|
+
const normalized = mac.replace(/[:-]/g, "").toUpperCase();
|
|
264
|
+
if (normalized.length < 6) return null;
|
|
265
|
+
|
|
266
|
+
// Get first 3 bytes (6 hex characters)
|
|
267
|
+
const prefix = normalized.substring(0, 6);
|
|
268
|
+
const formattedPrefix = `${prefix.substring(0, 2)}:${prefix.substring(2, 4)}:${prefix.substring(4, 6)}`;
|
|
269
|
+
|
|
270
|
+
return OUI_DB[formattedPrefix] || OUI_DB[prefix] || null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getVendorInfo(mac) {
|
|
274
|
+
const vendor = lookupVendor(mac);
|
|
275
|
+
return {
|
|
276
|
+
mac: mac,
|
|
277
|
+
vendor: vendor || "Unknown",
|
|
278
|
+
prefix: mac.substring(0, 8),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
lookupVendor,
|
|
284
|
+
getVendorInfo,
|
|
285
|
+
OUI_DB,
|
|
286
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spoof-d",
|
|
3
3
|
"description": "Cross-platform MAC address spoofing utility for macOS, Windows, and Linux",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"bin": {
|
|
6
|
-
"spoofy": "
|
|
6
|
+
"spoofy": "bin/cmd.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"index.js",
|
|
10
10
|
"bin/",
|
|
11
|
+
"lib/",
|
|
11
12
|
"LICENSE",
|
|
12
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
".spoofyrc.example",
|
|
15
|
+
"batch.example.json"
|
|
13
16
|
],
|
|
14
17
|
"bugs": {
|
|
15
18
|
"url": "https://github.com/TT5H/spoof-d/issues"
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
"chalk": "^4.1.0",
|
|
19
22
|
"common-tags": "^1.8.0",
|
|
20
23
|
"minimist": "^1.2.5",
|
|
24
|
+
"ora": "^9.0.0",
|
|
21
25
|
"shell-quote": "^1.7.2",
|
|
22
26
|
"zero-fill": "^2.2.4"
|
|
23
27
|
},
|