Add command line arguments

- Save to JSON
- Save to CSV
- Retry
- Concurrency
- Silent
- Help
- Version
This commit is contained in:
Stone_Red
2024-10-25 14:45:26 +02:00
parent ae8055fac8
commit 701cd8e3fc
5 changed files with 210 additions and 35 deletions
+1
View File
@@ -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" />
+23
View File
@@ -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]
});
}
}
+21 -1
View File
@@ -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");
@@ -31,19 +31,28 @@ internal partial class MacVendorLookup
macDatabase = JsonSerializer.Deserialize<MacDatabase>(File.ReadAllText(cachePath)) ?? new MacDatabase(); macDatabase = JsonSerializer.Deserialize<MacDatabase>(File.ReadAllText(cachePath)) ?? new MacDatabase();
} }
catch (Exception ex) catch (Exception ex)
{
if (!silent)
{ {
ConsoleExt.WriteLine($"Failed to read MAC database cache: {ex.Message}", ConsoleColor.Red); 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))
{
if (!silent)
{ {
ConsoleExt.WriteLine("Using cached MAC database...", ConsoleColor.DarkYellow); ConsoleExt.WriteLine("Using cached MAC database...", ConsoleColor.DarkYellow);
}
return; return;
} }
if (!silent)
{
ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow); ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow);
}
List<MacInformation>? newMacInformation = null; List<MacInformation>? newMacInformation = null;
@@ -52,12 +61,20 @@ internal partial class MacVendorLookup
newMacInformation = await httpClient.GetFromJsonAsync<List<MacInformation>>(macLookupUrl); newMacInformation = await httpClient.GetFromJsonAsync<List<MacInformation>>(macLookupUrl);
} }
catch (Exception ex) catch (Exception ex)
{
if (!silent)
{ {
ConsoleExt.WriteLine($"Failed to download MAC database: {ex.Message}", ConsoleColor.Red); 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);
@@ -68,8 +85,11 @@ internal partial class MacVendorLookup
} }
} }
else else
{
if (!silent)
{ {
ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green); ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green);
}
_ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); _ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+120 -16
View File
@@ -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)];
if (!options.Silent)
{
ConsoleExt.WriteLine("Starting scan...", ConsoleColor.DarkYellow); 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;
do
{
try try
{ {
mac = await Arp.LookupAsync(ipAddress); mac = await Arp.LookupAsync(ipAddress);
fail = false;
} }
catch (Exception ex) catch (Exception ex)
{
if (!options.Silent)
{ {
ConsoleExt.WriteLine($"Failed to lookup MAC address for {ipAddress}: {ex.Message}", ConsoleColor.Red); ConsoleExt.WriteLine($"Failed to lookup MAC address for {ipAddress}: {ex.Message}", ConsoleColor.Red);
}
fail = true; 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];
if (!options.Silent)
{
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Active: {ipAddress}", ConsoleColor.Green); 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)
{
if (!options.Silent)
{ {
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Failed: {ipAddress}", ConsoleColor.Red); ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Failed: {ipAddress}", ConsoleColor.Red);
} }
}
else else
{
if (!options.Silent)
{ {
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Inactive: {ipAddress}", ConsoleColor.Red); ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Inactive: {ipAddress}", ConsoleColor.Red);
} }
}
}); });
if (!activeHosts.IsEmpty) if (!options.Silent)
{ {
Console.WriteLine(Environment.NewLine + $"Active host{(activeHosts.Count == 1 ? "s" : "")}:"); Console.WriteLine();
}
List<string[]>? activeHostsTable = [.. activeHosts]; List<string[]>? activeHostsTable = [.. activeHosts];
activeHostsTable.Insert(0, [.. header]); activeHostsTable.Insert(0, [.. header]);
if (!activeHosts.IsEmpty)
{
Console.WriteLine($"Active host{(activeHosts.Count == 1 ? "s" : "")}:");
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);
} }
} }
} }
+27
View File
@@ -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; }
}