From 5ceee1092fc368ed52e4535601e709a59cb06696 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 1 May 2024 02:00:12 +0200 Subject: [PATCH 1/7] Set `Private` to `null` instead of `false` --- src/ARP-Scanner/MacVendorLookup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ARP-Scanner/MacVendorLookup.cs b/src/ARP-Scanner/MacVendorLookup.cs index 04a96b6..8a46a81 100644 --- a/src/ARP-Scanner/MacVendorLookup.cs +++ b/src/ARP-Scanner/MacVendorLookup.cs @@ -88,7 +88,7 @@ internal partial class MacVendorLookup MacPrefix = macAdress, VendorName = "Unknown", BlockType = "Unknown", - Private = false, + Private = null, LastUpdate = "Unknown" }; } From 553fd97e2a9307a8339324c6a1da843e69e5e90b Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 1 May 2024 14:48:11 +0200 Subject: [PATCH 2/7] Update arp-scanner.nuspec --- choco/arp-scanner.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/choco/arp-scanner.nuspec b/choco/arp-scanner.nuspec index ffa16f7..4f78304 100644 --- a/choco/arp-scanner.nuspec +++ b/choco/arp-scanner.nuspec @@ -74,7 +74,7 @@ enclosed in quotation marks, you should use an editor that supports UTF-8, not t **Usage** 1. Download the latest release - - Chocolatey (Windows): `choco install arp-scanner --version=0.2.0` + - Chocolatey (Windows): `choco install arp-scanner` - Snapcraft (Linux): `snap install arp-scanner`\ and `snap connect arp-scanner:network-control` to give it permission to send arp requests - GitHub (Windows/Linux): https://github.com/Stone-Red-Code/ARP-Scanner/releases From 3c5d165ebb0ef5a017e66349eb5645ee10d5a255 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 1 May 2024 14:58:45 +0200 Subject: [PATCH 3/7] Fix pluralization & alignment of output --- src/ARP-Scanner/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ARP-Scanner/Program.cs b/src/ARP-Scanner/Program.cs index a10b09a..8c1839a 100644 --- a/src/ARP-Scanner/Program.cs +++ b/src/ARP-Scanner/Program.cs @@ -64,12 +64,12 @@ internal static class Program MacInformation macInformation = macVendorLookup.GetInformation(formattedMac); List 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); + ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Active: {ipAddress}", ConsoleColor.Green); activeHosts.Add([.. info]); } else if (fail) { - 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 { @@ -79,7 +79,7 @@ internal static class Program if (!activeHosts.IsEmpty) { - Console.WriteLine(Environment.NewLine + "Active hosts:"); + Console.WriteLine(Environment.NewLine + $"Active host{(activeHosts.Count == 1 ? "s" : "")}:"); List? activeHostsTable = [.. activeHosts]; @@ -87,7 +87,7 @@ internal static class Program activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List); - ConsoleExt.WriteLine($"{Environment.NewLine}Found {activeHosts.Count} active hosts", ConsoleColor.Green); + ConsoleExt.WriteLine($"{Environment.NewLine}Found {activeHosts.Count} active host{(activeHosts.Count == 1 ? "s" : "")}", ConsoleColor.Green); } else { From ae8055fac87fe5d4822da0245e681ad234765905 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 25 Oct 2024 00:16:11 +0200 Subject: [PATCH 4/7] Add humanizer library --- src/ARP-Scanner/ARP-Scanner.csproj | 1 + src/ARP-Scanner/MacVendorLookup.cs | 16 ++++++++-------- src/ARP-Scanner/Program.cs | 10 ++++++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/ARP-Scanner/ARP-Scanner.csproj b/src/ARP-Scanner/ARP-Scanner.csproj index a740f6c..7ad33bc 100644 --- a/src/ARP-Scanner/ARP-Scanner.csproj +++ b/src/ARP-Scanner/ARP-Scanner.csproj @@ -11,6 +11,7 @@ + diff --git a/src/ARP-Scanner/MacVendorLookup.cs b/src/ARP-Scanner/MacVendorLookup.cs index 8a46a81..946291d 100644 --- a/src/ARP-Scanner/MacVendorLookup.cs +++ b/src/ARP-Scanner/MacVendorLookup.cs @@ -45,18 +45,18 @@ internal partial class MacVendorLookup ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow); - List? newMacInformations = null; + List? newMacInformation = null; try { - newMacInformations = await httpClient.GetFromJsonAsync>(macLookupUrl); + newMacInformation = await httpClient.GetFromJsonAsync>(macLookupUrl); } catch (Exception ex) { ConsoleExt.WriteLine($"Failed to download MAC database: {ex.Message}", ConsoleColor.Red); } - if (newMacInformations is null) + if (newMacInformation is null) { if (macDatabase.MacInformations.Count != 0) { @@ -73,19 +73,19 @@ internal partial class MacVendorLookup _ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); - macDatabase.MacInformations = newMacInformations; + macDatabase.MacInformations = newMacInformation; macDatabase.LastUpdate = DateTime.Now; File.WriteAllText(cachePath, JsonSerializer.Serialize(macDatabase)); } } - public MacInformation GetInformation(string macAdress) + public MacInformation GetInformation(string macAddress) { - macAdress = macAdress.Replace("-", ":")[..8].ToUpper(); + macAddress = macAddress.Replace("-", ":")[..8].ToUpper(); - return macDatabase.MacInformations.Find(m => m.MacPrefix == macAdress) ?? new MacInformation() + return macDatabase.MacInformations.Find(m => m.MacPrefix == macAddress) ?? new MacInformation() { - MacPrefix = macAdress, + MacPrefix = macAddress, VendorName = "Unknown", BlockType = "Unknown", Private = null, diff --git a/src/ARP-Scanner/Program.cs b/src/ARP-Scanner/Program.cs index 8c1839a..75747c5 100644 --- a/src/ARP-Scanner/Program.cs +++ b/src/ARP-Scanner/Program.cs @@ -2,6 +2,8 @@ using CuteUtils.Misc; +using Humanizer; + using NetTools; using System.Collections.Concurrent; @@ -37,7 +39,11 @@ internal static class Program List header = ["IP", "MAC"]; ConcurrentBag activeHosts = []; - header.AddRange([nameof(MacInformation.VendorName), nameof(MacInformation.BlockType), nameof(MacInformation.Private), nameof(MacInformation.LastUpdate)]); + header.AddRange([ + nameof(MacInformation.VendorName).Humanize(LetterCasing.Title), + nameof(MacInformation.BlockType).Humanize(LetterCasing.Title), + nameof(MacInformation.Private).Humanize(LetterCasing.Title), + nameof(MacInformation.LastUpdate).Humanize(LetterCasing.Title)]); ConsoleExt.WriteLine("Starting scan...", ConsoleColor.DarkYellow); @@ -87,7 +93,7 @@ internal static class Program activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List); - ConsoleExt.WriteLine($"{Environment.NewLine}Found {activeHosts.Count} active host{(activeHosts.Count == 1 ? "s" : "")}", ConsoleColor.Green); + ConsoleExt.WriteLine($"{Environment.NewLine}Found {"active host".ToQuantity(activeHosts.Count)}", ConsoleColor.Green); } else { From 701cd8e3fc4838a34781856216474b6f8a793fcc Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:45:26 +0200 Subject: [PATCH 5/7] Add command line arguments - Save to JSON - Save to CSV - Retry - Concurrency - Silent - Help - Version --- src/ARP-Scanner/ARP-Scanner.csproj | 1 + src/ARP-Scanner/JsonResult.cs | 23 ++++ src/ARP-Scanner/MacVendorLookup.cs | 32 ++++-- src/ARP-Scanner/Program.cs | 162 +++++++++++++++++++++++------ src/ARP-Scanner/ScanOptions.cs | 27 +++++ 5 files changed, 210 insertions(+), 35 deletions(-) create mode 100644 src/ARP-Scanner/JsonResult.cs create mode 100644 src/ARP-Scanner/ScanOptions.cs diff --git a/src/ARP-Scanner/ARP-Scanner.csproj b/src/ARP-Scanner/ARP-Scanner.csproj index 7ad33bc..13b889f 100644 --- a/src/ARP-Scanner/ARP-Scanner.csproj +++ b/src/ARP-Scanner/ARP-Scanner.csproj @@ -10,6 +10,7 @@ + diff --git a/src/ARP-Scanner/JsonResult.cs b/src/ARP-Scanner/JsonResult.cs new file mode 100644 index 0000000..85d7f5f --- /dev/null +++ b/src/ARP-Scanner/JsonResult.cs @@ -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 Parse(List 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] + }); + } +} diff --git a/src/ARP-Scanner/MacVendorLookup.cs b/src/ARP-Scanner/MacVendorLookup.cs index 946291d..293c0cf 100644 --- a/src/ARP-Scanner/MacVendorLookup.cs +++ b/src/ARP-Scanner/MacVendorLookup.cs @@ -12,7 +12,7 @@ internal partial class MacVendorLookup 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"); @@ -32,18 +32,27 @@ internal partial class MacVendorLookup } 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 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; } - ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow); + if (!silent) + { + ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow); + } List? newMacInformation = null; @@ -53,11 +62,19 @@ internal partial class MacVendorLookup } 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 (silent) + { + return; + } + if (macDatabase.MacInformations.Count != 0) { ConsoleExt.WriteLine("Failed to download MAC database, using cache...", ConsoleColor.DarkYellow); @@ -69,7 +86,10 @@ internal partial class MacVendorLookup } else { - ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green); + if (!silent) + { + ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green); + } _ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); diff --git a/src/ARP-Scanner/Program.cs b/src/ARP-Scanner/Program.cs index 75747c5..710897a 100644 --- a/src/ARP-Scanner/Program.cs +++ b/src/ARP-Scanner/Program.cs @@ -1,5 +1,7 @@ using ArpLookup; +using CommandLine; + using CuteUtils.Misc; using Humanizer; @@ -9,58 +11,84 @@ using NetTools; using System.Collections.Concurrent; using System.Net; using System.Net.NetworkInformation; +using System.Text.Json; namespace ARP_Scanner; internal static class Program { - private static async Task Main(string[] args) + private static async Task Main(string[] args) { - MacVendorLookup macVendorLookup = new MacVendorLookup(); - if (!Arp.IsSupported) { 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(args) + .MapResult(StartScan, HandleParseError); + } + + private static async Task StartScan(ScanOptions options) + { + if (!IPAddressRange.TryParse(options.IpRange, out IPAddressRange ipAddressRange)) { 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 processedIpAddressesCount = 0; int numberOfDigits = ipAddressesCount.ToString().Length; + int exitCode = 0; + - List header = ["IP", "MAC"]; ConcurrentBag activeHosts = []; - header.AddRange([ + // If you want to change the header, you also need to change the JsonResult class + List header = [ + "IP", + "MAC", nameof(MacInformation.VendorName).Humanize(LetterCasing.Title), nameof(MacInformation.BlockType).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; bool fail = false; - - try + int retry = options.Retry; + do { - mac = await Arp.LookupAsync(ipAddress); - } - catch (Exception ex) - { - ConsoleExt.WriteLine($"Failed to lookup MAC address for {ipAddress}: {ex.Message}", ConsoleColor.Red); - fail = true; + try + { + mac = await Arp.LookupAsync(ipAddress); + fail = false; + } + 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); 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); List 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]); } 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 { - 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? activeHostsTable = [.. activeHosts]; + activeHostsTable.Insert(0, [.. header]); + if (!activeHosts.IsEmpty) { - Console.WriteLine(Environment.NewLine + $"Active host{(activeHosts.Count == 1 ? "s" : "")}:"); - - List? activeHostsTable = [.. activeHosts]; - - activeHostsTable.Insert(0, [.. header]); + Console.WriteLine($"Active host{(activeHosts.Count == 1 ? "s" : "")}:"); activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List); 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 HandleParseError(IEnumerable errors) + { + if (errors.IsHelp() || errors.IsVersion()) + { + return Task.FromResult(0); + } else { - ConsoleExt.WriteLine($"{Environment.NewLine}No active hosts found", ConsoleColor.Red); + return Task.FromResult(1); } } } \ No newline at end of file diff --git a/src/ARP-Scanner/ScanOptions.cs b/src/ARP-Scanner/ScanOptions.cs new file mode 100644 index 0000000..d087a96 --- /dev/null +++ b/src/ARP-Scanner/ScanOptions.cs @@ -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; } +} From 1b5278bf09ab264996e79a43c2df53fc33f87a20 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Sun, 27 Oct 2024 01:17:03 +0200 Subject: [PATCH 6/7] Add monitoring mode and improve json output format --- src/ARP-Scanner/ARP-Scanner.csproj | 1 + src/ARP-Scanner/JsonResult.cs | 62 ++++-- src/ARP-Scanner/MacVendorLookup.cs | 6 + src/ARP-Scanner/MonitorOptions.cs | 10 + src/ARP-Scanner/Program.cs | 187 +++++++++++++++--- .../Properties/launchSettings.json | 2 +- src/ARP-Scanner/ScanOptions.cs | 8 +- 7 files changed, 228 insertions(+), 48 deletions(-) create mode 100644 src/ARP-Scanner/MonitorOptions.cs diff --git a/src/ARP-Scanner/ARP-Scanner.csproj b/src/ARP-Scanner/ARP-Scanner.csproj index 13b889f..6c81d75 100644 --- a/src/ARP-Scanner/ARP-Scanner.csproj +++ b/src/ARP-Scanner/ARP-Scanner.csproj @@ -6,6 +6,7 @@ ARP_Scanner enable enable + false diff --git a/src/ARP-Scanner/JsonResult.cs b/src/ARP-Scanner/JsonResult.cs index 85d7f5f..4beeea9 100644 --- a/src/ARP-Scanner/JsonResult.cs +++ b/src/ARP-Scanner/JsonResult.cs @@ -1,23 +1,53 @@ 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 IEnumerable Hosts { get; set; } = []; - public static IEnumerable Parse(List activeHosts) + public IEnumerable NewHosts { get; set; } = []; + + public IEnumerable RemovedHosts { get; set; } = []; + + public static JsonResult Parse(IEnumerable activeHosts, List previousHosts) { - return activeHosts.Select(row => new JsonResult + JsonResult result = new JsonResult { - Ip = row[0], - Mac = row[1], - VendorName = row[2], - BlockType = row[3], - Private = bool.Parse(row[4]), - LastUpdate = row[5] - }); + Hosts = activeHosts.Select(HostInformation.Parse) + }; + + if (previousHosts.Count == 0) + { + return result; + } + + result.NewHosts = result.Hosts.Where(host => !previousHosts.Exists(previousHost => previousHost[1] == host.Mac)); + result.RemovedHosts = previousHosts.Where(previousHost => !result.Hosts.Any(host => host.Mac == previousHost[1])).Select(HostInformation.Parse); + + return result; } -} + + public class HostInformation + { + 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 bool? Private { get; set; } + public required string LastUpdate { get; set; } + + public static HostInformation Parse(string[] row) + { + bool success = bool.TryParse(row[4], out bool isPrivate); + + return new HostInformation + { + Ip = row[0], + Mac = row[1], + VendorName = row[2], + BlockType = row[3], + Private = success ? isPrivate : null, + LastUpdate = row[5] + }; + } + } +} \ No newline at end of file diff --git a/src/ARP-Scanner/MacVendorLookup.cs b/src/ARP-Scanner/MacVendorLookup.cs index 293c0cf..672a04c 100644 --- a/src/ARP-Scanner/MacVendorLookup.cs +++ b/src/ARP-Scanner/MacVendorLookup.cs @@ -14,6 +14,12 @@ internal partial class MacVendorLookup public async Task Initialize(bool silent) { + if (macDatabase.MacInformations.Count != 0) + { + // Already initialized + return; + } + string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "macDatabase.json"); // Snap support diff --git a/src/ARP-Scanner/MonitorOptions.cs b/src/ARP-Scanner/MonitorOptions.cs new file mode 100644 index 0000000..4e9cd22 --- /dev/null +++ b/src/ARP-Scanner/MonitorOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace ARP_Scanner; + +[Verb("monitor", HelpText = "Continuously monitor the specified IP range.")] +internal class MonitorOptions : ScanOptions +{ + [Option('d', "delay", Required = false, Default = 60, HelpText = "The delay between each scan in seconds.")] + public int Delay { get; set; } +} \ No newline at end of file diff --git a/src/ARP-Scanner/Program.cs b/src/ARP-Scanner/Program.cs index 710897a..8f0fe1b 100644 --- a/src/ARP-Scanner/Program.cs +++ b/src/ARP-Scanner/Program.cs @@ -5,18 +5,30 @@ using CommandLine; using CuteUtils.Misc; using Humanizer; +using Humanizer.Localisation; using NetTools; using System.Collections.Concurrent; +using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Text.Json; +using System.Text.Json.Serialization; namespace ARP_Scanner; internal static class Program { + private static readonly MacVendorLookup macVendorLookup = new(); + private static readonly List previouslyActiveHosts = []; + + private static readonly JsonSerializerOptions jsonSerializerOptions = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + private static async Task Main(string[] args) { if (!Arp.IsSupported) @@ -25,8 +37,45 @@ internal static class Program return 1; } - return await Parser.Default.ParseArguments(args) - .MapResult(StartScan, HandleParseError); + return await Parser.Default.ParseArguments(args) + .MapResult( + (MonitorOptions monitorOptions) => StartMonitor(monitorOptions), + (ScanOptions scanOptions) => StartScan(scanOptions), + HandleParseError); + } + + private static async Task StartMonitor(MonitorOptions options) + { + ConsoleExt.WriteLine($"Monitoring IP range '{options.IpRange}' every {options.Delay} seconds...", ConsoleColor.DarkYellow); + + while (true) + { + int exitCode = await StartScan(options); + + if (exitCode != 0) + { + return exitCode; + } + + Console.WriteLine(); + + Stopwatch stopwatch = Stopwatch.StartNew(); + + bool printed = false; + + while (stopwatch.Elapsed < TimeSpan.FromSeconds(options.Delay)) + { + TimeSpan remainingTime = TimeSpan.FromSeconds(options.Delay) - stopwatch.Elapsed; + Console.Write($"\rNext scan in: {remainingTime.Humanize(3, minUnit: TimeUnit.Second)} "); + await Task.Delay(1000); + printed = true; + } + + if (printed) + { + Console.Write($"\r{new string(' ', Console.WindowWidth)}\r"); + } + } } private static async Task StartScan(ScanOptions options) @@ -37,7 +86,6 @@ internal static class Program return 2; } - MacVendorLookup macVendorLookup = new MacVendorLookup(); await macVendorLookup.Initialize(options.Silent); long ipAddressesCount = ipAddressRange.Count(); @@ -45,7 +93,6 @@ internal static class Program int numberOfDigits = ipAddressesCount.ToString().Length; int exitCode = 0; - ConcurrentBag activeHosts = []; // If you want to change the header, you also need to change the JsonResult class @@ -97,7 +144,7 @@ internal static class Program MacInformation macInformation = macVendorLookup.GetInformation(formattedMac); - List info = [ipAddress.ToString(), formattedMac, macInformation.VendorName, macInformation.BlockType, macInformation.Private.ToString() ?? "Unknown", macInformation.LastUpdate]; + List 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); @@ -120,25 +167,13 @@ internal static class Program } }); - if (!options.Silent) + if (previouslyActiveHosts.Count > 0 && !options.Silent) { - Console.WriteLine(); - } - - List? activeHostsTable = [.. activeHosts]; - activeHostsTable.Insert(0, [.. header]); - - if (!activeHosts.IsEmpty) - { - Console.WriteLine($"Active host{(activeHosts.Count == 1 ? "s" : "")}:"); - - activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List); - - ConsoleExt.WriteLine($"{Environment.NewLine}Found {"active host".ToQuantity(activeHosts.Count)}", ConsoleColor.Green); + PrintDifference(activeHosts, header); } else if (!options.Silent) { - ConsoleExt.WriteLine($"No active hosts found", ConsoleColor.Red); + PrintActiveHosts(activeHosts, header); } if (!options.Silent && (options.JsonPath is not null || options.CsvPath is not null)) @@ -148,20 +183,25 @@ internal static class Program if (options.JsonPath is not null) { + string jsonPath = GetOutputPath(options.JsonPath, ".json"); + try { - File.WriteAllText(options.JsonPath, JsonSerializer.Serialize(JsonResult.Parse([.. activeHosts]))); + _ = Directory.CreateDirectory(Path.GetDirectoryName(jsonPath) ?? string.Empty); + + string json = JsonSerializer.Serialize(JsonResult.Parse([.. activeHosts], previouslyActiveHosts), jsonSerializerOptions); + File.WriteAllText(jsonPath, json); if (!options.Silent) { - ConsoleExt.WriteLine($"Saved JSON to '{options.JsonPath}'", ConsoleColor.Green); + ConsoleExt.WriteLine($"Saved JSON to '{jsonPath}'", ConsoleColor.Green); } } catch (Exception ex) { if (!options.Silent) { - ConsoleExt.WriteLine($"Failed to save JSON to '{options.JsonPath}': {ex.Message}", ConsoleColor.Red); + ConsoleExt.WriteLine($"Failed to save JSON to '{jsonPath}': {ex.Message}", ConsoleColor.Red); } exitCode = 3; @@ -170,26 +210,37 @@ internal static class Program if (options.CsvPath is not null) { + string csvPath = GetOutputPath(options.CsvPath, ".csv"); + List? activeHostsTable = [.. activeHosts]; + activeHostsTable.Insert(0, [.. header]); + try { - File.WriteAllLines(options.CsvPath, activeHostsTable.Select(row => string.Join(",", row))); + _ = Directory.CreateDirectory(Path.GetDirectoryName(csvPath) ?? string.Empty); + File.WriteAllLines(csvPath, activeHostsTable.Select(row => string.Join(",", row))); if (!options.Silent) { - ConsoleExt.WriteLine($"Saved CSV to '{options.CsvPath}'", ConsoleColor.Green); + ConsoleExt.WriteLine($"Saved CSV to '{csvPath}'", ConsoleColor.Green); } } catch (Exception ex) { if (!options.Silent) { - ConsoleExt.WriteLine($"Failed to save CSV to '{options.CsvPath}': {ex.Message}", ConsoleColor.Red); + ConsoleExt.WriteLine($"Failed to save CSV to '{csvPath}': {ex.Message}", ConsoleColor.Red); } exitCode = 3; } } + previouslyActiveHosts.Clear(); + foreach (string[] activeHost in activeHosts) + { + previouslyActiveHosts.Add(activeHost); + } + return exitCode; } @@ -204,4 +255,88 @@ internal static class Program return Task.FromResult(1); } } + + private static string GetOutputPath(string path, string extension) + { + string directory = Path.GetDirectoryName(path) ?? string.Empty; + string fileName = Path.GetFileNameWithoutExtension(path); + string fileExtension = Path.GetExtension(path); + fileExtension = string.IsNullOrEmpty(fileExtension) ? extension : fileExtension; + + string dateTime = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + + string newPath = Path.Combine(directory, $"{fileName}_{dateTime}{fileExtension}"); + + int counter = 0; + while (File.Exists(newPath)) + { + newPath = Path.Combine(directory, $"{fileName}_{dateTime}_{++counter}{fileExtension}"); + } + + return newPath; + } + + private static void PrintActiveHosts(ConcurrentBag activeHosts, List header) + { + List? activeHostsTable = [.. activeHosts]; + activeHostsTable.Insert(0, [.. header]); + + Console.WriteLine(); + + if (!activeHosts.IsEmpty) + { + Console.WriteLine($"Active hosts:"); + + activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List); + + ConsoleExt.WriteLine($"{Environment.NewLine}Found {"active host".ToQuantity(activeHosts.Count)}", ConsoleColor.Green); + } + else + { + ConsoleExt.WriteLine($"No active hosts found", ConsoleColor.Red); + } + } + + private static void PrintDifference(ConcurrentBag activeHosts, List header) + { + List newHosts = activeHosts.Where(activeHost => !previouslyActiveHosts.Exists(previousHost => previousHost[1] == activeHost[1])).ToList(); + List removedHosts = previouslyActiveHosts.Where(previousHost => !activeHosts.Any(activeHost => activeHost[1] == previousHost[1])).ToList(); + + List? newHostsTable = [.. newHosts]; + newHostsTable.Insert(0, [.. header]); + + List? removedHostsTable = [.. removedHosts]; + removedHostsTable.Insert(0, [.. header]); + + Console.WriteLine(); + + if (newHosts.Count > 0) + { + Console.WriteLine($"New hosts:"); + + newHostsTable.ToArray().To2D().PrintTable(TableStyle.List); + + ConsoleExt.WriteLine($"{Environment.NewLine}Found {"active host".ToQuantity(activeHosts.Count)}", ConsoleColor.Green); + } + else + { + ConsoleExt.WriteLine($"No new hosts found", ConsoleColor.Blue); + } + + Console.WriteLine(); + + if (removedHosts.Count > 0) + { + // Better term for removed host? + Console.WriteLine($"Previously active hosts:"); + + removedHostsTable.ToArray().To2D().PrintTable(TableStyle.List); + + ConsoleExt.WriteLine($"{Environment.NewLine}Found {"previously active host".ToQuantity(removedHosts.Count)}", ConsoleColor.Red); + } + else + { + ConsoleExt.WriteLine($"No previously active hosts are currently inactive", ConsoleColor.Blue); + } + } } \ No newline at end of file diff --git a/src/ARP-Scanner/Properties/launchSettings.json b/src/ARP-Scanner/Properties/launchSettings.json index 50b3f07..264fb0f 100644 --- a/src/ARP-Scanner/Properties/launchSettings.json +++ b/src/ARP-Scanner/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "ARP-Scanner": { "commandName": "Project", - "commandLineArgs": "192.168.1.0 - 192.168.1.255" + "commandLineArgs": "scan 192.168.1.239" } } } \ No newline at end of file diff --git a/src/ARP-Scanner/ScanOptions.cs b/src/ARP-Scanner/ScanOptions.cs index d087a96..aedb303 100644 --- a/src/ARP-Scanner/ScanOptions.cs +++ b/src/ARP-Scanner/ScanOptions.cs @@ -1,7 +1,8 @@ using CommandLine; namespace ARP_Scanner; -[Verb("scan")] + +[Verb("scan", HelpText = "Scan the specified IP range.")] internal class ScanOptions { [Value(0, Required = true, MetaName = "IP range", HelpText = "The IP range to scan.")] @@ -10,9 +11,6 @@ internal class ScanOptions [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; } @@ -24,4 +22,4 @@ internal class ScanOptions [Option("csv", Required = false, HelpText = "The path to the CSV file to save the results.")] public string? CsvPath { get; set; } -} +} \ No newline at end of file From 6e86058862825a980bf149c7c8ba8201163279e0 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:17:14 +0100 Subject: [PATCH 7/7] Set "scan" as default --- src/ARP-Scanner/ScanOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ARP-Scanner/ScanOptions.cs b/src/ARP-Scanner/ScanOptions.cs index aedb303..cf8b9ba 100644 --- a/src/ARP-Scanner/ScanOptions.cs +++ b/src/ARP-Scanner/ScanOptions.cs @@ -2,7 +2,7 @@ namespace ARP_Scanner; -[Verb("scan", HelpText = "Scan the specified IP range.")] +[Verb("scan", true, HelpText = "Scan the specified IP range.")] internal class ScanOptions { [Value(0, Required = true, MetaName = "IP range", HelpText = "The IP range to scan.")]