diff --git a/Protest/Front/issues.js b/Protest/Front/issues.js index d73da4c4..d0bc8fde 100644 --- a/Protest/Front/issues.js +++ b/Protest/Front/issues.js @@ -7,6 +7,7 @@ class Issues extends List { }; static CATEGORY_ICON = { + "Database" : "url(mono/database.svg)", "Directory" : "url(mono/directory.svg)", "Password" : "url(mono/lock.svg)", "Printer component" : "url(mono/printer.svg)", diff --git a/Protest/Tools/LiveStats.cs b/Protest/Tools/LiveStats.cs index be61021c..4a95b690 100644 --- a/Protest/Tools/LiveStats.cs +++ b/Protest/Tools/LiveStats.cs @@ -180,14 +180,14 @@ public static async void DeviceStats(HttpListenerContext ctx) { if (OperatingSystem.IsWindows() && _os?.value?.Contains("windows", StringComparison.OrdinalIgnoreCase) == true && firstAlive is not null && firstReply.Status == IPStatus.Success) { - WmiQuery(ws, mutex, entry.filename, firstAlive, ref wmiHostname); + WmiQuery(ws, mutex, firstAlive, ref wmiHostname); } if (firstAlive is not null && firstReply.Status == IPStatus.Success && entry.attributes.TryGetValue("type", out Database.Attribute _type) && entry.attributes.TryGetValue("snmp profile", out Database.Attribute _snmpProfile)) { - SnmpQuery(ws, mutex, entry.filename, firstAlive, _type?.value.ToLower(), _snmpProfile.value); + SnmpQuery(ws, mutex, firstAlive, _type?.value.ToLower(), _snmpProfile.value); } if (OperatingSystem.IsWindows() && _hostname?.value?.Length > 0) { @@ -302,7 +302,7 @@ public static async void DeviceStats(HttpListenerContext ctx) { } [SupportedOSPlatform("windows")] - private static void WmiQuery(WebSocket ws, object mutex, string file, string firstAlive, ref string wmiHostname) { + private static void WmiQuery(WebSocket ws, object mutex, string firstAlive, ref string wmiHostname) { try { ManagementScope scope = Protocols.Wmi.Scope(firstAlive, 3_000); if (scope is not null && scope.IsConnected) { @@ -324,7 +324,7 @@ private static void WmiQuery(WebSocket ws, object mutex, string file, string fir WsWriteText(ws, $"{{\"drive\":\"{caption}\",\"total\":{nSize},\"used\":{nSize - nFree},\"path\":\"{Data.EscapeJsonText($"\\\\{firstAlive}\\{caption.Replace(":", String.Empty)}$")}\",\"source\":\"WMI\"}}", mutex); - if (Issues.CheckDiskCapacity(file, firstAlive, percent, caption, out Issues.Issue? diskIssue)) { + if (Issues.CheckDiskCapacity(null, firstAlive, percent, caption, out Issues.Issue? diskIssue)) { WsWriteText(ws, diskIssue?.ToLiveStatsJsonBytes(), mutex); } } @@ -365,7 +365,7 @@ private static void WmiQuery(WebSocket ws, object mutex, string file, string fir catch { } } - private static void SnmpQuery(WebSocket ws, object mutex, string file, string firstAlive, string type, string snmpProfileGuid) { + private static void SnmpQuery(WebSocket ws, object mutex, string firstAlive, string type, string snmpProfileGuid) { if (!SnmpProfiles.FromGuid(snmpProfileGuid, out SnmpProfiles.Profile profile)) { return; } @@ -412,7 +412,7 @@ private static void SnmpQuery(WebSocket ws, object mutex, string file, string fi WsWriteText(ws, $"{{\"info\":\"Total jobs: {Data.EscapeJsonText(snmpPrinterJobs)}\",\"source\":\"SNMP\"}}", mutex); } - if (Issues.CheckPrinterComponent(file, ipAddress, profile, out Issues.Issue[] issues)) { + if (Issues.CheckPrinterComponent(null, ipAddress, profile, out Issues.Issue[] issues) && issues is not null) { for (int i = 0; i < issues.Length; i++) { WsWriteText(ws, issues[i].ToLiveStatsJsonBytes(), mutex); } diff --git a/Protest/Workers/Issues.cs b/Protest/Workers/Issues.cs index ad278355..c85044f3 100644 --- a/Protest/Workers/Issues.cs +++ b/Protest/Workers/Issues.cs @@ -20,7 +20,7 @@ namespace Protest.Workers; internal static class Issues { - private const int WEAK_PASSWORD_ENTROPY_THRESHOLD = 28; + private const double WEAK_PASSWORD_ENTROPY_THRESHOLD = 36.0; public enum SeverityLevel { info = 1, @@ -35,13 +35,13 @@ public struct Issue { public string target; public string category; public string source; - public bool isUser; public string file; + public bool isUser; public long timestamp; } private static TaskWrapper task; - private static ConcurrentBag issues = new ConcurrentBag(); + private static ConcurrentBag issues; public static byte[] ToLiveStatsJsonBytes(this Issue issue) => JsonSerializer.SerializeToUtf8Bytes(new Dictionary { { issue.severity.ToString(), issue.message }, @@ -57,6 +57,9 @@ public static byte[] List() { public static byte[] Start(string origin) { if (task is not null) return Data.CODE_OTHER_TASK_IN_PROGRESS.Array; + issues?.Clear(); + issues = new ConcurrentBag(); + Thread thread = new Thread(() => Scan()); task = new TaskWrapper("Issues") { @@ -141,8 +144,8 @@ public static async void WebSocketHandler(HttpListenerContext ctx) { target = o.target, category = o.category, source = o.source, - isUser = o.isUser, file = o.file, + isUser = o.isUser, })); await WsWriteText(ws, bytes); @@ -194,15 +197,40 @@ public static void ScanUser(Database.Entry user) { } private static void ScanDevices() { + Dictionary ipAddresses = new Dictionary(); + foreach (KeyValuePair device in DatabaseInstances.devices.dictionary) { + if (device.Value.attributes.TryGetValue("ip", out Database.Attribute ipAttribute)) { + + string[] ips = ipAttribute.value.Split(',').Select(o=>o.Trim()).ToArray(); + for (int i = 0; i < ips.Length; i++) { + if (ipAddresses.ContainsKey(ips[i])) { + issues.Add(new Issue { + severity = SeverityLevel.info, + message = "IP address is duplicated in various records", + target = ips[i], + category = "Database", + source = "Internal check", + file = device.Value.filename, + isUser = false, + timestamp = DateTime.UtcNow.Ticks + }); + + continue; + } + + ipAddresses.Add(ips[i], device.Value); + } + } + ScanDevice(device.Value); } + + ipAddresses.Clear(); } public static void ScanDevice(Database.Entry device) { device.attributes.TryGetValue("type", out Database.Attribute typeAttribute); - //device.Value.attributes.TryGetValue("ip", out Database.Attribute ipAttribute); - //device.Value.attributes.TryGetValue("hostname", out Database.Attribute hostnameAttribute); device.attributes.TryGetValue("operating system", out Database.Attribute osAttribute); if (CheckPasswordStrength(device, false, out Issue? issue) && issue.HasValue) { @@ -210,7 +238,7 @@ public static void ScanDevice(Database.Entry device) { } if (osAttribute?.value.Contains("windows", StringComparison.OrdinalIgnoreCase) == true) { - + //TODO: } else if (Data.PRINTER_TYPES.Contains(typeAttribute?.value, StringComparer.OrdinalIgnoreCase)) { if (CheckPrinterComponent(device, out Issue[] printerIssues) && printerIssues is not null) { @@ -220,48 +248,53 @@ public static void ScanDevice(Database.Entry device) { } } else if (Data.SWITCH_TYPES.Contains(typeAttribute?.value, StringComparer.OrdinalIgnoreCase)) { - + //TODO: } } + public static bool CheckLatency(Database.Entry entry, out Issue? issue) { + issue = null; + return false; + } + [SupportedOSPlatform("windows")] - public static bool CheckDomainUser(Database.Entry user, out Issue[] issues, SeverityLevel severityThreshhold, SearchResult result = null) { + public static bool CheckDomainUser(Database.Entry user, out Issue[] issues, SeverityLevel severityThreshold) { if (!user.attributes.TryGetValue("username", out Database.Attribute username)) { issues = null; return false; } try { - result ??= Kerberos.GetUser(username.value); + SearchResult result = Kerberos.GetUser(username.value); List list = new List(); long lockedTime = 0; - if (result is null && severityThreshhold <= SeverityLevel.warning) { + if (result is null && severityThreshold <= SeverityLevel.warning) { list.Add(new Issue { - severity = SeverityLevel.warning, - message = $"{username.value} is not a domain user", - target = username.value, - category = "Directory", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.warning, + message = $"{username.value} is not a domain user", + target = username.value, + category = "Directory", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); } else { bool isDisabled = false; - if (severityThreshhold <= SeverityLevel.info + if (severityThreshold <= SeverityLevel.info && result.Properties["userAccountControl"].Count > 0 && Int32.TryParse(result.Properties["userAccountControl"][0].ToString(), out int userControl) && (userControl & 0x0002) != 0) { list.Add(new Issue { - severity = SeverityLevel.info, - message = $"User {username.value} is disabled", - target = username.value, - category = "Directory", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.info, + message = $"User {username.value} is disabled", + target = username.value, + category = "Directory", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); isDisabled = true; @@ -273,62 +306,62 @@ public static bool CheckDomainUser(Database.Entry user, out Issue[] issues, Seve DateTime oneYearAgo = DateTime.UtcNow.AddYears(-1); DateTime sixMonthsAgo = DateTime.UtcNow.AddMonths(-6); - if (severityThreshhold <= SeverityLevel.error && lastPasswordChange < oneYearAgo) { + if (severityThreshold <= SeverityLevel.error && lastPasswordChange < oneYearAgo) { list.Add(new Issue { - severity = SeverityLevel.error, - message = $"Password has not been changed since {lastPasswordChange.ToString(Data.DATE_FORMAT_LONG)}", - target = username.value, - category = "Password", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.error, + message = $"Password has not been changed since {lastPasswordChange.ToString(Data.DATE_FORMAT_LONG)}", + target = username.value, + category = "Password", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); } - else if (severityThreshhold <= SeverityLevel.warning && lastPasswordChange < sixMonthsAgo) { + else if (severityThreshold <= SeverityLevel.warning && lastPasswordChange < sixMonthsAgo) { list.Add(new Issue { - severity = SeverityLevel.warning, - message = $"Password has not been changed since {lastPasswordChange.ToString(Data.DATE_FORMAT_LONG)}", - target = username.value, - category = "Password", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.warning, + message = $"Password has not been changed since {lastPasswordChange.ToString(Data.DATE_FORMAT_LONG)}", + target = username.value, + category = "Password", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); } } - if (severityThreshhold <= SeverityLevel.warning + if (severityThreshold <= SeverityLevel.warning && result.Properties["lockoutTime"].Count > 0 && Int64.TryParse(result.Properties["lockoutTime"][0].ToString(), out lockedTime) && lockedTime > 0 && DateTime.UtcNow < DateTime.FromFileTime(lockedTime).AddHours(1)) { list.Add(new Issue { - severity = SeverityLevel.warning, - message = $"User {username.value} is locked out", - target = username.value, - category = "Directory", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.warning, + message = $"User {username.value} is locked out", + target = username.value, + category = "Directory", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); } - if (severityThreshhold <= SeverityLevel.info + if (severityThreshold <= SeverityLevel.info && result.Properties["lastLogonTimestamp"].Count > 0 && Int64.TryParse(result.Properties["lastLogonTimestamp"][0].ToString(), out long lastLogonTimestamp) && lastLogonTimestamp > 0) { list.Add(new Issue { - severity = SeverityLevel.info, - message = $"Last logon: {DateTime.FromFileTime(lastLogonTimestamp)}", - target = username.value, - category = "Directory", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.info, + message = $"Last logon: {DateTime.FromFileTime(lastLogonTimestamp)}", + target = username.value, + category = "Directory", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); } @@ -337,30 +370,30 @@ public static bool CheckDomainUser(Database.Entry user, out Issue[] issues, Seve && Int64.TryParse(result.Properties["lastLogoff"][0].ToString(), out long lastLogOffTime) && lastLogOffTime > 0) { list.Add(new Issue { - severity = SeverityLevel.info, - message = $"Last logoff: {DateTime.FromFileTime(lastLogOffTime)}", - target = username.value, - category = "Directory", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.info, + message = $"Last logoff: {DateTime.FromFileTime(lastLogOffTime)}", + target = username.value, + category = "Directory", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); }*/ - if (severityThreshhold <= SeverityLevel.info + if (severityThreshold <= SeverityLevel.info && lockedTime > 0 && result.Properties["badPasswordTime"].Count > 0 && Int64.TryParse(result.Properties["badPasswordTime"][0].ToString(), out long badPasswordTime) && badPasswordTime > 0) { list.Add(new Issue { - severity = SeverityLevel.info, - message = $"Bad password time: {(DateTime.FromFileTime(badPasswordTime))}", - target = username.value, - category = "Directory", - source = "Kerberos", - isUser = true, - file = user.filename, + severity = SeverityLevel.info, + message = $"Bad password time: {(DateTime.FromFileTime(badPasswordTime))}", + target = username.value, + category = "Directory", + source = "Kerberos", + file = user.filename, + isUser = true, timestamp = DateTime.UtcNow.Ticks }); } @@ -384,7 +417,8 @@ public static bool CheckDomainUser(Database.Entry user, out Issue[] issues, Seve public static bool CheckPasswordStrength(Database.Entry entry, bool isUser, out Issue? issue) { if (entry.attributes.TryGetValue("password", out Database.Attribute password)) { string value = password.value; - if (value.Length > 0 && PasswordStrength.Entropy(value) < WEAK_PASSWORD_ENTROPY_THRESHOLD) { + double entropy = PasswordStrength.Entropy(value); + if (value.Length > 0 && entropy < WEAK_PASSWORD_ENTROPY_THRESHOLD) { string target; if (isUser) { if (entry.attributes.TryGetValue("username", out Database.Attribute usernameAttr)) { @@ -410,14 +444,14 @@ public static bool CheckPasswordStrength(Database.Entry entry, bool isUser, out } issue = new Issue { - severity = Issues.SeverityLevel.critical, - target = target, - message = "Weak password", - category = "Password", - source = "Internal check", - isUser = isUser, - file = entry.filename, - timestamp = DateTime.UtcNow.Ticks + severity = Issues.SeverityLevel.critical, + target = target, + message = $"Weak password with {Math.Round(entropy, 2)} bits of entropy", + category = "Password", + source = "Internal check", + file = entry.filename, + isUser = isUser, + timestamp = DateTime.UtcNow.Ticks }; return true; } @@ -437,8 +471,8 @@ public static bool CheckDiskCapacity(string file, string target, double percent, message = message, category = "Disk drive", source = "WMI", - isUser = false, file = file, + isUser = false, timestamp = DateTime.UtcNow.Ticks, }; return true; @@ -451,8 +485,8 @@ public static bool CheckDiskCapacity(string file, string target, double percent, message = message, category = "Disk drive", source = "WMI", - isUser = false, file = file, + isUser = false, timestamp = DateTime.UtcNow.Ticks, }; return true; @@ -465,8 +499,8 @@ public static bool CheckDiskCapacity(string file, string target, double percent, message = message, category = "Disk drive", source = "WMI", - isUser = false, file = file, + isUser = false, timestamp = DateTime.UtcNow.Ticks, }; return true; @@ -547,8 +581,8 @@ public static bool CheckPrinterComponent(string file, IPAddress ipAddress, SnmpP target = ipAddress.ToString(), category = "Printer component", source = "SNMP", - isUser = false, file = file, + isUser = false, timestamp = DateTime.UtcNow.Ticks }); } diff --git a/Protest/Workers/LastSeen.cs b/Protest/Workers/LastSeen.cs index 3f1c6bbb..3f0ecdd5 100644 --- a/Protest/Workers/LastSeen.cs +++ b/Protest/Workers/LastSeen.cs @@ -12,7 +12,6 @@ public static void Seen(in string ip) { string filename = $"{Data.DIR_LASTSEEN}\\{ip}.txt"; try { object mutex = mutexes.GetOrAdd(ip, new object()); - lock (mutex) { File.WriteAllText(filename, DateTime.Now.ToString(Data.DATETIME_FORMAT_LONG)); //File.WriteAllText(filename, DateTime.UtcNow.ToString()); @@ -23,17 +22,6 @@ public static void Seen(in string ip) { } } - public static string HasBeenSeen(in string[] para, bool recordOnly = false) { - string ip = null; - for (int i = 1; i < para.Length; i++) { - if (para[i].StartsWith("ip=")) { - ip = para[i][3..]; - } - } - - return HasBeenSeen(ip, recordOnly); - } - public static string HasBeenSeen(string ip, bool recordOnly = false) { if (ip is null) return null; @@ -53,7 +41,6 @@ public static string HasBeenSeen(string ip, bool recordOnly = false) { try { if (File.Exists(filename)) { object mutex = mutexes.GetOrAdd(ip, new object()); - lock (mutex) { return File.ReadAllText(filename); }