mirror of
https://github.com/Stone-Red-Code/ARP-Scanner.git
synced 2026-09-04 00:46:04 +02:00
Add command line arguments
- Save to JSON - Save to CSV - Retry - Concurrency - Silent - Help - Version
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ArpLookup" Version="2.0.3" />
|
<PackageReference Include="ArpLookup" Version="2.0.3" />
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="CuteUtils" Version="1.0.0" />
|
<PackageReference Include="CuteUtils" Version="1.0.0" />
|
||||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||||
<PackageReference Include="IPAddressRange" Version="6.0.0" />
|
<PackageReference Include="IPAddressRange" Version="6.0.0" />
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace ARP_Scanner;
|
||||||
|
internal class JsonResult
|
||||||
|
{
|
||||||
|
public required string Ip { get; set; }
|
||||||
|
public required string Mac { get; set; }
|
||||||
|
public required string VendorName { get; set; }
|
||||||
|
public required string BlockType { get; set; }
|
||||||
|
public required bool Private { get; set; }
|
||||||
|
public required string LastUpdate { get; set; }
|
||||||
|
|
||||||
|
public static IEnumerable<JsonResult> Parse(List<string[]> activeHosts)
|
||||||
|
{
|
||||||
|
return activeHosts.Select(row => new JsonResult
|
||||||
|
{
|
||||||
|
Ip = row[0],
|
||||||
|
Mac = row[1],
|
||||||
|
VendorName = row[2],
|
||||||
|
BlockType = row[3],
|
||||||
|
Private = bool.Parse(row[4]),
|
||||||
|
LastUpdate = row[5]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ internal partial class MacVendorLookup
|
|||||||
|
|
||||||
private MacDatabase macDatabase = new MacDatabase();
|
private MacDatabase macDatabase = new MacDatabase();
|
||||||
|
|
||||||
public async Task Initialize()
|
public async Task Initialize(bool silent)
|
||||||
{
|
{
|
||||||
string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "macDatabase.json");
|
string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "macDatabase.json");
|
||||||
|
|
||||||
@@ -32,18 +32,27 @@ internal partial class MacVendorLookup
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine($"Failed to read MAC database cache: {ex.Message}", ConsoleColor.Red);
|
if (!silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Failed to read MAC database cache: {ex.Message}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update MAC database if it's older than a week
|
// Update MAC database if it's older than a week
|
||||||
if (macDatabase.LastUpdate > DateTime.Now.AddDays(-7))
|
if (macDatabase.LastUpdate > DateTime.Now.AddDays(-7))
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine("Using cached MAC database...", ConsoleColor.DarkYellow);
|
if (!silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine("Using cached MAC database...", ConsoleColor.DarkYellow);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow);
|
if (!silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow);
|
||||||
|
}
|
||||||
|
|
||||||
List<MacInformation>? newMacInformation = null;
|
List<MacInformation>? newMacInformation = null;
|
||||||
|
|
||||||
@@ -53,11 +62,19 @@ internal partial class MacVendorLookup
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine($"Failed to download MAC database: {ex.Message}", ConsoleColor.Red);
|
if (!silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Failed to download MAC database: {ex.Message}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newMacInformation is null)
|
if (newMacInformation is null)
|
||||||
{
|
{
|
||||||
|
if (silent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (macDatabase.MacInformations.Count != 0)
|
if (macDatabase.MacInformations.Count != 0)
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine("Failed to download MAC database, using cache...", ConsoleColor.DarkYellow);
|
ConsoleExt.WriteLine("Failed to download MAC database, using cache...", ConsoleColor.DarkYellow);
|
||||||
@@ -69,7 +86,10 @@ internal partial class MacVendorLookup
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green);
|
if (!silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green);
|
||||||
|
}
|
||||||
|
|
||||||
_ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
|
_ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
|
||||||
|
|
||||||
|
|||||||
+133
-29
@@ -1,5 +1,7 @@
|
|||||||
using ArpLookup;
|
using ArpLookup;
|
||||||
|
|
||||||
|
using CommandLine;
|
||||||
|
|
||||||
using CuteUtils.Misc;
|
using CuteUtils.Misc;
|
||||||
|
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
@@ -9,58 +11,84 @@ using NetTools;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ARP_Scanner;
|
namespace ARP_Scanner;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
private static async Task Main(string[] args)
|
private static async Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
MacVendorLookup macVendorLookup = new MacVendorLookup();
|
|
||||||
|
|
||||||
if (!Arp.IsSupported)
|
if (!Arp.IsSupported)
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine("ARP is not supported on this platform!", ConsoleColor.Red);
|
ConsoleExt.WriteLine("ARP is not supported on this platform!", ConsoleColor.Red);
|
||||||
return;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IPAddressRange.TryParse(string.Join("", args), out IPAddressRange ipAddressRange))
|
return await Parser.Default.ParseArguments<ScanOptions>(args)
|
||||||
|
.MapResult(StartScan, HandleParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> StartScan(ScanOptions options)
|
||||||
|
{
|
||||||
|
if (!IPAddressRange.TryParse(options.IpRange, out IPAddressRange ipAddressRange))
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine("Invalid IP range!", ConsoleColor.Red);
|
ConsoleExt.WriteLine("Invalid IP range!", ConsoleColor.Red);
|
||||||
return;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
await macVendorLookup.Initialize();
|
MacVendorLookup macVendorLookup = new MacVendorLookup();
|
||||||
|
await macVendorLookup.Initialize(options.Silent);
|
||||||
|
|
||||||
long ipAddressesCount = ipAddressRange.Count();
|
long ipAddressesCount = ipAddressRange.Count();
|
||||||
long processedIpAddressesCount = 0;
|
long processedIpAddressesCount = 0;
|
||||||
int numberOfDigits = ipAddressesCount.ToString().Length;
|
int numberOfDigits = ipAddressesCount.ToString().Length;
|
||||||
|
int exitCode = 0;
|
||||||
|
|
||||||
|
|
||||||
List<string> header = ["IP", "MAC"];
|
|
||||||
ConcurrentBag<string[]> activeHosts = [];
|
ConcurrentBag<string[]> activeHosts = [];
|
||||||
|
|
||||||
header.AddRange([
|
// If you want to change the header, you also need to change the JsonResult class
|
||||||
|
List<string> header = [
|
||||||
|
"IP",
|
||||||
|
"MAC",
|
||||||
nameof(MacInformation.VendorName).Humanize(LetterCasing.Title),
|
nameof(MacInformation.VendorName).Humanize(LetterCasing.Title),
|
||||||
nameof(MacInformation.BlockType).Humanize(LetterCasing.Title),
|
nameof(MacInformation.BlockType).Humanize(LetterCasing.Title),
|
||||||
nameof(MacInformation.Private).Humanize(LetterCasing.Title),
|
nameof(MacInformation.Private).Humanize(LetterCasing.Title),
|
||||||
nameof(MacInformation.LastUpdate).Humanize(LetterCasing.Title)]);
|
nameof(MacInformation.LastUpdate).Humanize(LetterCasing.Title)];
|
||||||
|
|
||||||
ConsoleExt.WriteLine("Starting scan...", ConsoleColor.DarkYellow);
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine("Starting scan...", ConsoleColor.DarkYellow);
|
||||||
|
}
|
||||||
|
|
||||||
await Parallel.ForEachAsync(ipAddressRange, async (IPAddress ipAddress, CancellationToken _) =>
|
ParallelOptions parallelOptions = new()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = options.Concurrency
|
||||||
|
};
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(ipAddressRange, parallelOptions, async (IPAddress ipAddress, CancellationToken _) =>
|
||||||
{
|
{
|
||||||
PhysicalAddress? mac = null;
|
PhysicalAddress? mac = null;
|
||||||
bool fail = false;
|
bool fail = false;
|
||||||
|
int retry = options.Retry;
|
||||||
try
|
do
|
||||||
{
|
{
|
||||||
mac = await Arp.LookupAsync(ipAddress);
|
try
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
mac = await Arp.LookupAsync(ipAddress);
|
||||||
{
|
fail = false;
|
||||||
ConsoleExt.WriteLine($"Failed to lookup MAC address for {ipAddress}: {ex.Message}", ConsoleColor.Red);
|
}
|
||||||
fail = true;
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Failed to lookup MAC address for {ipAddress}: {ex.Message}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
|
fail = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
while (retry-- > 0 && (mac is null || fail));
|
||||||
|
|
||||||
long localProcessedIpAddressesCount = Interlocked.Increment(ref processedIpAddressesCount);
|
long localProcessedIpAddressesCount = Interlocked.Increment(ref processedIpAddressesCount);
|
||||||
if (mac is not null && Array.Exists(mac.GetAddressBytes(), b => b != 0))
|
if (mac is not null && Array.Exists(mac.GetAddressBytes(), b => b != 0))
|
||||||
@@ -70,34 +98,110 @@ internal static class Program
|
|||||||
MacInformation macInformation = macVendorLookup.GetInformation(formattedMac);
|
MacInformation macInformation = macVendorLookup.GetInformation(formattedMac);
|
||||||
|
|
||||||
List<string> info = [ipAddress.ToString(), formattedMac, macInformation.VendorName, macInformation.BlockType, macInformation.Private.ToString() ?? "Unknown", macInformation.LastUpdate];
|
List<string> info = [ipAddress.ToString(), formattedMac, macInformation.VendorName, macInformation.BlockType, macInformation.Private.ToString() ?? "Unknown", macInformation.LastUpdate];
|
||||||
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Active: {ipAddress}", ConsoleColor.Green);
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Active: {ipAddress}", ConsoleColor.Green);
|
||||||
|
}
|
||||||
activeHosts.Add([.. info]);
|
activeHosts.Add([.. info]);
|
||||||
}
|
}
|
||||||
else if (fail)
|
else if (fail)
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Failed: {ipAddress}", ConsoleColor.Red);
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Failed: {ipAddress}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Inactive: {ipAddress}", ConsoleColor.Red);
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Inactive: {ipAddress}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string[]>? activeHostsTable = [.. activeHosts];
|
||||||
|
activeHostsTable.Insert(0, [.. header]);
|
||||||
|
|
||||||
if (!activeHosts.IsEmpty)
|
if (!activeHosts.IsEmpty)
|
||||||
{
|
{
|
||||||
Console.WriteLine(Environment.NewLine + $"Active host{(activeHosts.Count == 1 ? "s" : "")}:");
|
Console.WriteLine($"Active host{(activeHosts.Count == 1 ? "s" : "")}:");
|
||||||
|
|
||||||
List<string[]>? activeHostsTable = [.. activeHosts];
|
|
||||||
|
|
||||||
activeHostsTable.Insert(0, [.. header]);
|
|
||||||
|
|
||||||
activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List);
|
activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List);
|
||||||
|
|
||||||
ConsoleExt.WriteLine($"{Environment.NewLine}Found {"active host".ToQuantity(activeHosts.Count)}", ConsoleColor.Green);
|
ConsoleExt.WriteLine($"{Environment.NewLine}Found {"active host".ToQuantity(activeHosts.Count)}", ConsoleColor.Green);
|
||||||
}
|
}
|
||||||
|
else if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"No active hosts found", ConsoleColor.Red);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.Silent && (options.JsonPath is not null || options.CsvPath is not null))
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.JsonPath is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(options.JsonPath, JsonSerializer.Serialize(JsonResult.Parse([.. activeHosts])));
|
||||||
|
|
||||||
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Saved JSON to '{options.JsonPath}'", ConsoleColor.Green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Failed to save JSON to '{options.JsonPath}': {ex.Message}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.CsvPath is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllLines(options.CsvPath, activeHostsTable.Select(row => string.Join(",", row)));
|
||||||
|
|
||||||
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Saved CSV to '{options.CsvPath}'", ConsoleColor.Green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!options.Silent)
|
||||||
|
{
|
||||||
|
ConsoleExt.WriteLine($"Failed to save CSV to '{options.CsvPath}': {ex.Message}", ConsoleColor.Red);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<int> HandleParseError(IEnumerable<Error> errors)
|
||||||
|
{
|
||||||
|
if (errors.IsHelp() || errors.IsVersion())
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ConsoleExt.WriteLine($"{Environment.NewLine}No active hosts found", ConsoleColor.Red);
|
return Task.FromResult(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace ARP_Scanner;
|
||||||
|
[Verb("scan")]
|
||||||
|
internal class ScanOptions
|
||||||
|
{
|
||||||
|
[Value(0, Required = true, MetaName = "IP range", HelpText = "The IP range to scan.")]
|
||||||
|
public required string IpRange { get; set; }
|
||||||
|
|
||||||
|
[Option('s', "silent", Required = false, HelpText = "Don't print anything to the console.")]
|
||||||
|
public bool Silent { get; set; }
|
||||||
|
|
||||||
|
[Option('v', "verbose", Required = false, HelpText = "Print verbose information.")]
|
||||||
|
public bool Verbose { get; set; }
|
||||||
|
|
||||||
|
[Option('r', "retry", Required = false, Default = 0, HelpText = "The number of retries for each ARP request.")]
|
||||||
|
public int Retry { get; set; }
|
||||||
|
|
||||||
|
[Option('c', "concurrency", Required = false, Default = -1, HelpText = "The number of concurrent ARP requests.")]
|
||||||
|
public int Concurrency { get; set; }
|
||||||
|
|
||||||
|
[Option("json", Required = false, HelpText = "The path to the JSON file to save the results.")]
|
||||||
|
public string? JsonPath { get; set; }
|
||||||
|
|
||||||
|
[Option("csv", Required = false, HelpText = "The path to the CSV file to save the results.")]
|
||||||
|
public string? CsvPath { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user