From b89eba69e1e02a4cec965fa3956d88b928473dce Mon Sep 17 00:00:00 2001 From: venizelou andreas Date: Sun, 30 Jun 2024 17:57:24 +0300 Subject: [PATCH] SNMP, live stats --- Protest/Protocols/Snmp.Oid.cs | 10 +++ Protest/Protocols/Snmp.Polling.cs | 51 +++++++++++++++ Protest/Tasks/Fetch.cs | 56 ++-------------- Protest/Tools/LiveStats.cs | 102 +++++++++++++++++++++--------- 4 files changed, 139 insertions(+), 80 deletions(-) diff --git a/Protest/Protocols/Snmp.Oid.cs b/Protest/Protocols/Snmp.Oid.cs index 39b7718b..12a72871 100644 --- a/Protest/Protocols/Snmp.Oid.cs +++ b/Protest/Protocols/Snmp.Oid.cs @@ -16,6 +16,16 @@ public static class Oid { INTERFACE_TOTAL }; + public static string[] LIVESTATS_OID = new string[] { + SYSTEM_UPTIME, + SYSTEM_TEMPERATURE, + PRINTER_STATUS, + PRINTER_MESSAGE, + PRINTER_JOBS + }; + + + public const string SYSTEM_DESCRIPTOR = "1.3.6.1.2.1.1.1.0"; public const string SYSTEM_OBJECT_ID = "1.3.6.1.2.1.1.2.0"; public const string SYSTEM_UPTIME = "1.3.6.1.2.1.1.3.0"; diff --git a/Protest/Protocols/Snmp.Polling.cs b/Protest/Protocols/Snmp.Polling.cs index 36ef0fa6..b016180f 100644 --- a/Protest/Protocols/Snmp.Polling.cs +++ b/Protest/Protocols/Snmp.Polling.cs @@ -6,6 +6,7 @@ using Lextm.SharpSnmpLib; using Lextm.SharpSnmpLib.Messaging; using Lextm.SharpSnmpLib.Security; +using Protest.Tools; namespace Protest.Protocols.Snmp; @@ -280,4 +281,54 @@ private static byte[] ParseResponse(IList result) { builder.Append(']'); return Encoding.UTF8.GetBytes(builder.ToString()); } + + public static (IList, SnmpProfiles.Profile) SnmpQueryTrialAndError(IPAddress target, SnmpProfiles.Profile[] snmpProfiles, string[] oids) { + for (int i = 0; i < snmpProfiles.Length; i++) { + IList result = SnmpQuery(target, snmpProfiles[i], oids); + + if (result is not null) { + return (result, snmpProfiles[i]); + } + } + + return (null, null); + } + + public static IList SnmpQuery(IPAddress target, SnmpProfiles.Profile profile, string[] oids) { + IList result = null; + + if (profile.version == 3) { + try { + result = Protocols.Snmp.Polling.SnmpRequestV3( + target, + 3000, + profile, + Protocols.Snmp.Oid.GENERIC_OID, + Protocols.Snmp.Polling.SnmpOperation.Get + ); + } + catch { } + } + else { + VersionCode version = profile.version switch + { + 1 => VersionCode.V1, + _ => VersionCode.V2 + }; + + try { + result = Protocols.Snmp.Polling.SnmpRequestV1V2( + target, + version, + 3000, + profile.community, + Protocols.Snmp.Oid.GENERIC_OID, + Protocols.Snmp.Polling.SnmpOperation.Get + ); + } + catch { } + } + + return result; + } } \ No newline at end of file diff --git a/Protest/Tasks/Fetch.cs b/Protest/Tasks/Fetch.cs index f9de8fdc..78266c89 100644 --- a/Protest/Tasks/Fetch.cs +++ b/Protest/Tasks/Fetch.cs @@ -14,6 +14,7 @@ using Protest.Http; using Protest.Tools; using Lextm.SharpSnmpLib; +using Protest.Protocols.Snmp; namespace Protest.Tasks; @@ -392,11 +393,11 @@ public static ConcurrentDictionary SingleDevice(string target, profile = null; } else if (snmpProfiles.Length == 1) { - result = SnmpQuery(ipAddress, snmpProfiles[0], Protocols.Snmp.Oid.GENERIC_OID); + result = Protocols.Snmp.Polling.SnmpQuery(ipAddress, snmpProfiles[0], Protocols.Snmp.Oid.GENERIC_OID); profile = snmpProfiles[0]; } else { - (result, profile) = SnmpQueryTrialAndError(ipAddress, snmpProfiles, Protocols.Snmp.Oid.GENERIC_OID); + (result, profile) = Protocols.Snmp.Polling.SnmpQueryTrialAndError(ipAddress, snmpProfiles, Protocols.Snmp.Oid.GENERIC_OID); } for (int i = 0; i < result?.Count; i++) { @@ -419,7 +420,7 @@ public static ConcurrentDictionary SingleDevice(string target, case "multiprinter": case "ticket printer": case "print": - IList printerResult = SnmpQuery(ipAddress, profile, Protocols.Snmp.Oid.PRINTERS_OID); + IList printerResult = Protocols.Snmp.Polling.SnmpQuery(ipAddress, profile, Protocols.Snmp.Oid.PRINTERS_OID); for (int i = 0; i < printerResult?.Count; i++) { string dataString = printerResult[i].Data.ToString(); if (String.IsNullOrEmpty(dataString)) { continue; } @@ -432,7 +433,7 @@ public static ConcurrentDictionary SingleDevice(string target, case "firewall": case "router": case "switch": - IList switchResult = SnmpQuery(ipAddress, profile, Protocols.Snmp.Oid.SWITCH_OID); + IList switchResult = Protocols.Snmp.Polling.SnmpQuery(ipAddress, profile, Protocols.Snmp.Oid.SWITCH_OID); for (int i = 0; i < switchResult?.Count; i++) { string dataString = switchResult[i].Data.ToString(); if (String.IsNullOrEmpty(dataString)) { continue; } @@ -452,54 +453,7 @@ public static ConcurrentDictionary SingleDevice(string target, return data; } - private static (IList, SnmpProfiles.Profile) SnmpQueryTrialAndError(IPAddress target, SnmpProfiles.Profile[] snmpProfiles, string[] oids) { - for (int i = 0; i < snmpProfiles.Length; i++) { - IList result = SnmpQuery(target, snmpProfiles[i], oids); - if (result is not null) { - return (result, snmpProfiles[i]); - } - } - - return (null, null); - } - - private static IList SnmpQuery(IPAddress target, SnmpProfiles.Profile profile, string[] oids) { - IList result = null; - - if (profile.version == 3) { - try { - result = Protocols.Snmp.Polling.SnmpRequestV3( - target, - 3000, - profile, - Protocols.Snmp.Oid.GENERIC_OID, - Protocols.Snmp.Polling.SnmpOperation.Get - ); - } - catch { } - } - else { - VersionCode version = profile.version switch { - 1 => VersionCode.V1, - _ => VersionCode.V2 - }; - - try { - result = Protocols.Snmp.Polling.SnmpRequestV1V2( - target, - version, - 3000, - profile.community, - Protocols.Snmp.Oid.GENERIC_OID, - Protocols.Snmp.Polling.SnmpOperation.Get - ); - } - catch { } - } - - return result; - } public static byte[] SingleUserSerialize(Dictionary parameters) { parameters.TryGetValue("target", out string target); diff --git a/Protest/Tools/LiveStats.cs b/Protest/Tools/LiveStats.cs index da06668f..6d8f96be 100644 --- a/Protest/Tools/LiveStats.cs +++ b/Protest/Tools/LiveStats.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; using System.DirectoryServices; using System.Management; using System.Net; @@ -8,17 +9,22 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Lextm.SharpSnmpLib; using Protest.Protocols; using Protest.Tasks; namespace Protest.Tools; internal static class LiveStats { - private static async Task WsWriteText(WebSocket ws, [StringSyntax(StringSyntaxAttribute.Json)] string text) { - await WsWriteText(ws, Encoding.UTF8.GetBytes(text)); + private static void WsWriteText(WebSocket ws, [StringSyntax(StringSyntaxAttribute.Json)] string text, object mutex) { + lock (mutex) { + WsWriteText(ws, Encoding.UTF8.GetBytes(text), mutex); + } } - private static async Task WsWriteText(WebSocket ws, byte[] bytes) { - await ws.SendAsync(new ArraySegment(bytes, 0, bytes.Length), WebSocketMessageType.Text, true, CancellationToken.None); + private static void WsWriteText(WebSocket ws, byte[] bytes, object mutex) { + lock (mutex) { + ws.SendAsync(new ArraySegment(bytes, 0, bytes.Length), WebSocketMessageType.Text, true, CancellationToken.None); + } } public static async void DeviceStats(HttpListenerContext ctx) { @@ -47,9 +53,11 @@ public static async void DeviceStats(HttpListenerContext ctx) { string firstAlive = null; PingReply firstReply = null; + entry.attributes.TryGetValue("type", out Database.Attribute _type); entry.attributes.TryGetValue("ip", out Database.Attribute _ip); entry.attributes.TryGetValue("hostname", out Database.Attribute _hostname); entry.attributes.TryGetValue("operating system", out Database.Attribute _os); + entry.attributes.TryGetValue("snmp profile", out Database.Attribute _snmpProfile); if (_ip?.value?.Length > 0) { pingArray = _ip.value.Split(';').Select(o => o.Trim()).ToArray(); @@ -58,6 +66,8 @@ public static async void DeviceStats(HttpListenerContext ctx) { pingArray = _hostname.value.Split(';').Select(o => o.Trim()).ToArray(); } + object mutex = new object(); + if (pingArray.Length > 0) { var pingTasks = new List(); @@ -72,19 +82,19 @@ public static async void DeviceStats(HttpListenerContext ctx) { firstAlive = pingArray[index]; firstReply = reply; } - await WsWriteText(ws, $"{{\"echoReply\":\"{reply.RoundtripTime}\",\"for\":\"{pingArray[index]}\",\"source\":\"ICMP\"}}"); - await WsWriteText(ws, $"{{\"info\":\"Last seen {pingArray[index]}: Just now\",\"source\":\"ICMP\"}}"); + WsWriteText(ws, $"{{\"echoReply\":\"{reply.RoundtripTime}\",\"for\":\"{pingArray[index]}\",\"source\":\"ICMP\"}}", mutex); + WsWriteText(ws, $"{{\"info\":\"Last seen {pingArray[index]}: Just now\",\"source\":\"ICMP\"}}", mutex); LastSeen.Seen(pingArray[index]); } else if (reply.Status == IPStatus.TimedOut) { - await WsWriteText(ws, $"{{\"echoReply\":\"Timed out\",\"for\":\"{pingArray[index]}\",\"source\":\"ICMP\"}}"); + WsWriteText(ws, $"{{\"echoReply\":\"Timed out\",\"for\":\"{pingArray[index]}\",\"source\":\"ICMP\"}}", mutex); } else { - await WsWriteText(ws, $"{{\"echoReply\":\"{Data.EscapeJsonText(reply.Status.ToString())}\",\"for\":\"{pingArray[index]}\",\"source\":\"ICMP\"}}"); + WsWriteText(ws, $"{{\"echoReply\":\"{Data.EscapeJsonText(reply.Status.ToString())}\",\"for\":\"{pingArray[index]}\",\"source\":\"ICMP\"}}", mutex); } } catch { - await WsWriteText(ws, $"{{\"echoReply\":\"Error\",\"for\":\"{Data.EscapeJsonText(pingArray[index])}\",\"source\":\"ICMP\"}}"); + WsWriteText(ws, $"{{\"echoReply\":\"Error\",\"for\":\"{Data.EscapeJsonText(pingArray[index])}\",\"source\":\"ICMP\"}}", mutex); } })); } @@ -94,10 +104,10 @@ public static async void DeviceStats(HttpListenerContext ctx) { if (firstAlive is null) { for (int i = 0; i < pingArray.Length; i++) { string lastSeen = LastSeen.HasBeenSeen(pingArray[i], true); - await WsWriteText(ws, $"{{\"info\":\"Last seen {pingArray[i]}: {lastSeen}\",\"source\":\"ICMP\"}}"); + WsWriteText(ws, $"{{\"info\":\"Last seen {pingArray[i]}: {lastSeen}\",\"source\":\"ICMP\"}}", mutex); } } - } + } string wmiHostname = null, adHostname = null, netbios = null, dns = null; @@ -125,10 +135,10 @@ firstAlive is not null && if (nSize == 0) continue; double percent = Math.Round(100.0 * nFree / nSize, 1); - await WsWriteText(ws, $"{{\"drive\":\"{caption}\",\"total\":{nSize},\"used\":{nSize - nFree},\"path\":\"{Data.EscapeJsonText($"\\\\{firstAlive}\\{caption.Replace(":", String.Empty)}$")}\",\"source\":\"WMI\"}}"); + WsWriteText(ws, $"{{\"drive\":\"{caption}\",\"total\":{nSize},\"used\":{nSize - nFree},\"path\":\"{Data.EscapeJsonText($"\\\\{firstAlive}\\{caption.Replace(":", String.Empty)}$")}\",\"source\":\"WMI\"}}", mutex); if (percent < 15) { - await WsWriteText(ws, $"{{\"warning\":\"{percent}% free space on disk {Data.EscapeJsonText(caption)}\",\"source\":\"WMI\"}}"); + WsWriteText(ws, $"{{\"warning\":\"{percent}% free space on disk {Data.EscapeJsonText(caption)}\",\"source\":\"WMI\"}}", mutex); } } @@ -144,23 +154,23 @@ firstAlive is not null && DateTime current = new DateTime(year, month, day, hour, minute, second); DateTime now = DateTime.UtcNow; if (Math.Abs(current.Ticks - now.Ticks) > 600_000_000L) { - await WsWriteText(ws, "{\"warning\":\"System time is off by more then 5 minutes\",\"source\":\"WMI\"}"u8.ToArray()); + WsWriteText(ws, "{\"warning\":\"System time is off by more then 5 minutes\",\"source\":\"WMI\"}"u8.ToArray(), mutex); } } if (scope is not null) { string startTime = Wmi.WmiGet(scope, "Win32_LogonSession", "StartTime", false, new Wmi.FormatMethodPtr(Wmi.DateTimeToString)); if (startTime.Length > 0) { - await WsWriteText(ws, $"{{\"info\":\"Start time: {Data.EscapeJsonText(startTime)}\",\"source\":\"WMI\"}}"); + WsWriteText(ws, $"{{\"info\":\"Start time: {Data.EscapeJsonText(startTime)}\",\"source\":\"WMI\"}}", mutex); } string username = Wmi.WmiGet(scope, "Win32_ComputerSystem", "UserName", false, null); if (username.Length > 0) { - await WsWriteText(ws, $"{{\"info\":\"Logged in user: {Data.EscapeJsonText(username)}\",\"source\":\"WMI\"}}"); + WsWriteText(ws, $"{{\"info\":\"Logged in user: {Data.EscapeJsonText(username)}\",\"source\":\"WMI\"}}", mutex); } wmiHostname = Wmi.WmiGet(scope, "Win32_ComputerSystem", "DNSHostName", false, null); - await WsWriteText(ws, $"{{\"activeUser\":\"{Data.EscapeJsonText(username)}\",\"source\":\"WMI\"}}"); + WsWriteText(ws, $"{{\"activeUser\":\"{Data.EscapeJsonText(username)}\",\"source\":\"WMI\"}}", mutex); } } } @@ -168,6 +178,38 @@ firstAlive is not null && catch { } } + if (_snmpProfile is not null) { + SnmpProfiles.Profile profile = null; + if (String.IsNullOrEmpty(_snmpProfile.value) && Guid.TryParse(_snmpProfile.value, out Guid guid)) { + SnmpProfiles.Profile[] profiles = SnmpProfiles.Load(); + for (int i = 0; i < profiles.Length; i++) { + if (profiles[i].guid == guid) { + profile = profiles[i]; + break; + } + } + } + + if (profile is not null + && firstAlive is not null + && firstReply.Status == IPStatus.Success + && IPAddress.TryParse(firstAlive, out IPAddress ipAddress)) { + IList result = Protocols.Snmp.Polling.SnmpQuery(ipAddress, profile, Protocols.Snmp.Oid.LIVESTATS_OID); + + for (int i = 0; i < result?.Count; i++) { + string dataString = result[i].Data.ToString(); + if (String.IsNullOrEmpty(dataString)) { continue; } + switch (result[i].Id.ToString()) { + case Protocols.Snmp.Oid.SYSTEM_UPTIME : WsWriteText(ws, $"{{\"info\":\"Uptime: {Data.EscapeJsonText(dataString)}\",\"source\":\"SNMP\"}}", mutex); break; + case Protocols.Snmp.Oid.SYSTEM_TEMPERATURE : WsWriteText(ws, $"{{\"info\":\"Temperature: {Data.EscapeJsonText(dataString)}\",\"source\":\"SNMP\"}}", mutex); break; + case Protocols.Snmp.Oid.PRINTER_STATUS : WsWriteText(ws, $"{{\"info\":\"Printer status: {Data.EscapeJsonText(dataString)}\",\"source\":\"SNMP\"}}", mutex); break; + case Protocols.Snmp.Oid.PRINTER_MESSAGE : WsWriteText(ws, $"{{\"info\":\"Printer message: {Data.EscapeJsonText(dataString)}\",\"source\":\"SNMP\"}}", mutex); break; + case Protocols.Snmp.Oid.PRINTER_JOBS : WsWriteText(ws, $"{{\"info\":\"Printer jobs: {Data.EscapeJsonText(dataString)}\",\"source\":\"SNMP\"}}", mutex); break; + } + } + } + } + if (OperatingSystem.IsWindows() && _hostname?.value?.Length > 0) { try { string hostname = _hostname.value; @@ -177,14 +219,14 @@ firstAlive is not null && if (result.Properties["lastLogonTimestamp"].Count > 0) { string time = Kerberos.FileTimeString(result.Properties["lastLogonTimestamp"][0].ToString()); if (time.Length > 0) { - await WsWriteText(ws, $"{{\"info\":\"Last logon: {Data.EscapeJsonText(time)}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"info\":\"Last logon: {Data.EscapeJsonText(time)}\",\"source\":\"Kerberos\"}}", mutex); } } if (result.Properties["lastLogoff"].Count > 0) { string time = Kerberos.FileTimeString(result.Properties["lastLogoff"][0].ToString()); if (time.Length > 0) { - await WsWriteText(ws, $"{{\"info\":\"Last logoff: {Data.EscapeJsonText(time)}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"info\":\"Last logoff: {Data.EscapeJsonText(time)}\",\"source\":\"Kerberos\"}}", mutex); } } @@ -209,7 +251,7 @@ firstAlive is not null && if (!mismatch && !String.IsNullOrEmpty(wmiHostname)) { wmiHostname = wmiHostname?.Split('.')[0].ToUpper(); if (wmiHostname != dns) { - await WsWriteText(ws, $"{{\"warning\":\"DNS mismatch: {Data.EscapeJsonText(wmiHostname)}\",\"source\":\"WMI\"}}"); + WsWriteText(ws, $"{{\"warning\":\"DNS mismatch: {Data.EscapeJsonText(wmiHostname)}\",\"source\":\"WMI\"}}", mutex); mismatch = true; } } @@ -217,7 +259,7 @@ firstAlive is not null && if (!mismatch && !String.IsNullOrEmpty(adHostname)) { adHostname = adHostname?.Split('.')[0].ToUpper(); if (adHostname != dns) { - await WsWriteText(ws, $"{{\"warning\":\"DNS mismatch: {Data.EscapeJsonText(adHostname)}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"warning\":\"DNS mismatch: {Data.EscapeJsonText(adHostname)}\",\"source\":\"Kerberos\"}}", mutex); mismatch = true; } } @@ -228,7 +270,7 @@ firstAlive is not null && if (!mismatch && !String.IsNullOrEmpty(netbios)) { netbios = netbios?.Split('.')[0].ToUpper(); if (netbios != dns) { - await WsWriteText(ws, $"{{\"warning\":\"DNS mismatch: {Data.EscapeJsonText(netbios)}\",\"source\":\"NetBIOS\"}}"); + WsWriteText(ws, $"{{\"warning\":\"DNS mismatch: {Data.EscapeJsonText(netbios)}\",\"source\":\"NetBIOS\"}}", mutex); mismatch = true; } } @@ -247,7 +289,7 @@ firstAlive is not null && IPAddress[] reversed = System.Net.Dns.GetHostAddresses(hostnames[i]); for (int j = 0; j < reversed.Length; j++) { if (!ips.Contains(reversed[j].ToString())) { - await WsWriteText(ws, $"{{\"warning\":\"Revese DNS mismatch: {Data.EscapeJsonText(reversed[j].ToString())}\",\"source\":\"DNS\"}}"); + WsWriteText(ws, $"{{\"warning\":\"Revese DNS mismatch: {Data.EscapeJsonText(reversed[j].ToString())}\",\"source\":\"DNS\"}}", mutex); break; } } @@ -259,7 +301,7 @@ firstAlive is not null && if (entry.attributes.TryGetValue("password", out Database.Attribute password)) { string value = password.value; if (value.Length > 0 && PasswordStrength.Entropy(value) < 28) { - await WsWriteText(ws, "{\"warnings\":\"Weak password\"}"u8.ToArray()); + WsWriteText(ws, "{\"warnings\":\"Weak password\"}"u8.ToArray(), mutex); } } } @@ -303,31 +345,33 @@ public static async void UserStats(HttpListenerContext ctx) { return; } + object mutex = new object(); + if (OperatingSystem.IsWindows() && entry.attributes.TryGetValue("username", out Database.Attribute username)) { try { SearchResult result = Kerberos.GetUser(username.value); if (result != null) { if (result.Properties["lastLogonTimestamp"].Count > 0) { if (Int64.TryParse(result.Properties["lastLogonTimestamp"][0].ToString(), out long time) && time > 0) { - await WsWriteText(ws, $"{{\"info\":\"Last logon: {DateTime.FromFileTime(time)}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"info\":\"Last logon: {DateTime.FromFileTime(time)}\",\"source\":\"Kerberos\"}}", mutex); } } if (result.Properties["lastLogoff"].Count > 0) { if (Int64.TryParse(result.Properties["lastLogoff"][0].ToString(), out long time) && time > 0) { - await WsWriteText(ws, $"{{\"info\":\"Last logoff: {DateTime.FromFileTime(time)}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"info\":\"Last logoff: {DateTime.FromFileTime(time)}\",\"source\":\"Kerberos\"}}", mutex); } } if (result.Properties["badPasswordTime"].Count > 0) { if (Int64.TryParse(result.Properties["badPasswordTime"][0].ToString(), out long time) && time > 0) { - await WsWriteText(ws, $"{{\"info\":\"Bad password time: {(DateTime.FromFileTime(time))}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"info\":\"Bad password time: {(DateTime.FromFileTime(time))}\",\"source\":\"Kerberos\"}}", mutex); } } if (result.Properties["lockoutTime"].Count > 0) { if (Int64.TryParse(result.Properties["lockoutTime"][0].ToString(), out long time) && time > 0) { - await WsWriteText(ws, $"{{\"lockedOut\":\"{DateTime.FromFileTime(time)}\",\"source\":\"Kerberos\"}}"); + WsWriteText(ws, $"{{\"lockedOut\":\"{DateTime.FromFileTime(time)}\",\"source\":\"Kerberos\"}}", mutex); } } } @@ -338,7 +382,7 @@ public static async void UserStats(HttpListenerContext ctx) { if (entry.attributes.TryGetValue("password", out Database.Attribute password)) { string value = password.value; if (value.Length > 0 && PasswordStrength.Entropy(value) < 28) { - await WsWriteText(ws, "{\"warnings\":\"Weak password\"}"u8.ToArray()); + WsWriteText(ws, "{\"warnings\":\"Weak password\"}"u8.ToArray(), mutex); } } }