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] 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