Merge branch 'develop' into main

This commit is contained in:
Stone_Red
2024-10-29 12:21:27 +01:00
committed by GitHub
7 changed files with 407 additions and 45 deletions
+3
View File
@@ -6,11 +6,14 @@
<RootNamespace>ARP_Scanner</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ArpLookup" Version="2.0.3" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CuteUtils" Version="1.0.0" />
<PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="IPAddressRange" Version="6.0.0" />
</ItemGroup>
+53
View File
@@ -0,0 +1,53 @@
namespace ARP_Scanner;
internal class JsonResult
{
public IEnumerable<HostInformation> Hosts { get; set; } = [];
public IEnumerable<HostInformation> NewHosts { get; set; } = [];
public IEnumerable<HostInformation> RemovedHosts { get; set; } = [];
public static JsonResult Parse(IEnumerable<string[]> activeHosts, List<string[]> previousHosts)
{
JsonResult result = new JsonResult
{
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]
};
}
}
}
+41 -15
View File
@@ -12,8 +12,14 @@ internal partial class MacVendorLookup
private MacDatabase macDatabase = new MacDatabase();
public async Task Initialize()
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
@@ -32,32 +38,49 @@ 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<MacInformation>? newMacInformations = null;
List<MacInformation>? newMacInformation = null;
try
{
newMacInformations = await httpClient.GetFromJsonAsync<List<MacInformation>>(macLookupUrl);
newMacInformation = await httpClient.GetFromJsonAsync<List<MacInformation>>(macLookupUrl);
}
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 (newMacInformations is null)
if (newMacInformation is null)
{
if (silent)
{
return;
}
if (macDatabase.MacInformations.Count != 0)
{
ConsoleExt.WriteLine("Failed to download MAC database, using cache...", ConsoleColor.DarkYellow);
@@ -69,26 +92,29 @@ 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)!);
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 = false,
Private = null,
LastUpdate = "Unknown"
};
}
+10
View File
@@ -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; }
}
+274 -29
View File
@@ -1,60 +1,141 @@
using ArpLookup;
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 async Task Main(string[] args)
{
MacVendorLookup macVendorLookup = new MacVendorLookup();
private static readonly MacVendorLookup macVendorLookup = new();
private static readonly List<string[]> previouslyActiveHosts = [];
private static readonly JsonSerializerOptions jsonSerializerOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private static async Task<int> Main(string[] args)
{
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<ScanOptions, MonitorOptions>(args)
.MapResult(
(MonitorOptions monitorOptions) => StartMonitor(monitorOptions),
(ScanOptions scanOptions) => StartScan(scanOptions),
HandleParseError);
}
private static async Task<int> 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<int> StartScan(ScanOptions options)
{
if (!IPAddressRange.TryParse(options.IpRange, out IPAddressRange ipAddressRange))
{
ConsoleExt.WriteLine("Invalid IP range!", ConsoleColor.Red);
return;
return 2;
}
await macVendorLookup.Initialize();
await macVendorLookup.Initialize(options.Silent);
long ipAddressesCount = ipAddressRange.Count();
long processedIpAddressesCount = 0;
int numberOfDigits = ipAddressesCount.ToString().Length;
int exitCode = 0;
List<string> header = ["IP", "MAC"];
ConcurrentBag<string[]> activeHosts = [];
header.AddRange([nameof(MacInformation.VendorName), nameof(MacInformation.BlockType), nameof(MacInformation.Private), nameof(MacInformation.LastUpdate)]);
// 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.BlockType).Humanize(LetterCasing.Title),
nameof(MacInformation.Private).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))
@@ -63,35 +144,199 @@ internal static class Program
MacInformation macInformation = macVendorLookup.GetInformation(formattedMac);
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);
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);
}
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 (!activeHosts.IsEmpty)
if (previouslyActiveHosts.Count > 0 && !options.Silent)
{
Console.WriteLine(Environment.NewLine + "Active hosts:");
PrintDifference(activeHosts, header);
}
else if (!options.Silent)
{
PrintActiveHosts(activeHosts, header);
}
if (!options.Silent && (options.JsonPath is not null || options.CsvPath is not null))
{
Console.WriteLine();
}
if (options.JsonPath is not null)
{
string jsonPath = GetOutputPath(options.JsonPath, ".json");
try
{
_ = 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 '{jsonPath}'", ConsoleColor.Green);
}
}
catch (Exception ex)
{
if (!options.Silent)
{
ConsoleExt.WriteLine($"Failed to save JSON to '{jsonPath}': {ex.Message}", ConsoleColor.Red);
}
exitCode = 3;
}
}
if (options.CsvPath is not null)
{
string csvPath = GetOutputPath(options.CsvPath, ".csv");
List<string[]>? activeHostsTable = [.. activeHosts];
activeHostsTable.Insert(0, [.. header]);
activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List);
try
{
_ = Directory.CreateDirectory(Path.GetDirectoryName(csvPath) ?? string.Empty);
File.WriteAllLines(csvPath, activeHostsTable.Select(row => string.Join(",", row)));
ConsoleExt.WriteLine($"{Environment.NewLine}Found {activeHosts.Count} active hosts", ConsoleColor.Green);
if (!options.Silent)
{
ConsoleExt.WriteLine($"Saved CSV to '{csvPath}'", ConsoleColor.Green);
}
}
catch (Exception ex)
{
if (!options.Silent)
{
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;
}
private static Task<int> HandleParseError(IEnumerable<Error> 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);
}
}
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<string[]> activeHosts, List<string> header)
{
List<string[]>? 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<string[]> activeHosts, List<string> header)
{
List<string[]> newHosts = activeHosts.Where(activeHost => !previouslyActiveHosts.Exists(previousHost => previousHost[1] == activeHost[1])).ToList();
List<string[]> removedHosts = previouslyActiveHosts.Where(previousHost => !activeHosts.Any(activeHost => activeHost[1] == previousHost[1])).ToList();
List<string[]>? newHostsTable = [.. newHosts];
newHostsTable.Insert(0, [.. header]);
List<string[]>? 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);
}
}
}
@@ -2,7 +2,7 @@
"profiles": {
"ARP-Scanner": {
"commandName": "Project",
"commandLineArgs": "192.168.1.0 - 192.168.1.255"
"commandLineArgs": "scan 192.168.1.239"
}
}
}
+25
View File
@@ -0,0 +1,25 @@
using CommandLine;
namespace ARP_Scanner;
[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.")]
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('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; }
}