Add monitoring mode and improve json output format

This commit is contained in:
Stone_Red
2024-10-27 01:17:10 +02:00
parent 701cd8e3fc
commit 1b5278bf09
7 changed files with 228 additions and 48 deletions
+1
View File
@@ -6,6 +6,7 @@
<RootNamespace>ARP_Scanner</RootNamespace> <RootNamespace>ARP_Scanner</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+46 -16
View File
@@ -1,23 +1,53 @@
namespace ARP_Scanner; namespace ARP_Scanner;
internal class JsonResult internal class JsonResult
{ {
public required string Ip { get; set; } public IEnumerable<HostInformation> Hosts { 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) public IEnumerable<HostInformation> NewHosts { get; set; } = [];
public IEnumerable<HostInformation> RemovedHosts { get; set; } = [];
public static JsonResult Parse(IEnumerable<string[]> activeHosts, List<string[]> previousHosts)
{ {
return activeHosts.Select(row => new JsonResult JsonResult result = new JsonResult
{ {
Ip = row[0], Hosts = activeHosts.Select(HostInformation.Parse)
Mac = row[1], };
VendorName = row[2],
BlockType = row[3], if (previousHosts.Count == 0)
Private = bool.Parse(row[4]), {
LastUpdate = row[5] 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]
};
}
}
}
+6
View File
@@ -14,6 +14,12 @@ internal partial class MacVendorLookup
public async Task Initialize(bool silent) 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"); string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "macDatabase.json");
// Snap support // Snap support
+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; }
}
+161 -26
View File
@@ -5,18 +5,30 @@ using CommandLine;
using CuteUtils.Misc; using CuteUtils.Misc;
using Humanizer; using Humanizer;
using Humanizer.Localisation;
using NetTools; using NetTools;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
namespace ARP_Scanner; namespace ARP_Scanner;
internal static class Program internal static class Program
{ {
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) private static async Task<int> Main(string[] args)
{ {
if (!Arp.IsSupported) if (!Arp.IsSupported)
@@ -25,8 +37,45 @@ internal static class Program
return 1; return 1;
} }
return await Parser.Default.ParseArguments<ScanOptions>(args) return await Parser.Default.ParseArguments<ScanOptions, MonitorOptions>(args)
.MapResult(StartScan, HandleParseError); .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) private static async Task<int> StartScan(ScanOptions options)
@@ -37,7 +86,6 @@ internal static class Program
return 2; return 2;
} }
MacVendorLookup macVendorLookup = new MacVendorLookup();
await macVendorLookup.Initialize(options.Silent); await macVendorLookup.Initialize(options.Silent);
long ipAddressesCount = ipAddressRange.Count(); long ipAddressesCount = ipAddressRange.Count();
@@ -45,7 +93,6 @@ internal static class Program
int numberOfDigits = ipAddressesCount.ToString().Length; int numberOfDigits = ipAddressesCount.ToString().Length;
int exitCode = 0; int exitCode = 0;
ConcurrentBag<string[]> activeHosts = []; ConcurrentBag<string[]> activeHosts = [];
// If you want to change the header, you also need to change the JsonResult class // 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); 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) 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);
@@ -120,25 +167,13 @@ internal static class Program
} }
}); });
if (!options.Silent) if (previouslyActiveHosts.Count > 0 && !options.Silent)
{ {
Console.WriteLine(); PrintDifference(activeHosts, header);
}
List<string[]>? 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);
} }
else if (!options.Silent) 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)) 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) if (options.JsonPath is not null)
{ {
string jsonPath = GetOutputPath(options.JsonPath, ".json");
try 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) if (!options.Silent)
{ {
ConsoleExt.WriteLine($"Saved JSON to '{options.JsonPath}'", ConsoleColor.Green); ConsoleExt.WriteLine($"Saved JSON to '{jsonPath}'", ConsoleColor.Green);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
if (!options.Silent) 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; exitCode = 3;
@@ -170,26 +210,37 @@ internal static class Program
if (options.CsvPath is not null) if (options.CsvPath is not null)
{ {
string csvPath = GetOutputPath(options.CsvPath, ".csv");
List<string[]>? activeHostsTable = [.. activeHosts];
activeHostsTable.Insert(0, [.. header]);
try 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) if (!options.Silent)
{ {
ConsoleExt.WriteLine($"Saved CSV to '{options.CsvPath}'", ConsoleColor.Green); ConsoleExt.WriteLine($"Saved CSV to '{csvPath}'", ConsoleColor.Green);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
if (!options.Silent) 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; exitCode = 3;
} }
} }
previouslyActiveHosts.Clear();
foreach (string[] activeHost in activeHosts)
{
previouslyActiveHosts.Add(activeHost);
}
return exitCode; return exitCode;
} }
@@ -204,4 +255,88 @@ internal static class Program
return Task.FromResult(1); 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": { "profiles": {
"ARP-Scanner": { "ARP-Scanner": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "192.168.1.0 - 192.168.1.255" "commandLineArgs": "scan 192.168.1.239"
} }
} }
} }
+3 -5
View File
@@ -1,7 +1,8 @@
using CommandLine; using CommandLine;
namespace ARP_Scanner; namespace ARP_Scanner;
[Verb("scan")]
[Verb("scan", HelpText = "Scan the specified IP range.")]
internal class ScanOptions internal class ScanOptions
{ {
[Value(0, Required = true, MetaName = "IP range", HelpText = "The IP range to scan.")] [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.")] [Option('s', "silent", Required = false, HelpText = "Don't print anything to the console.")]
public bool Silent { get; set; } 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.")] [Option('r', "retry", Required = false, Default = 0, HelpText = "The number of retries for each ARP request.")]
public int Retry { get; set; } 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.")] [Option("csv", Required = false, HelpText = "The path to the CSV file to save the results.")]
public string? CsvPath { get; set; } public string? CsvPath { get; set; }
} }