diff --git a/Configuration/AppSettings.cs b/Configuration/AppSettings.cs index 7a0552f..acbc830 100644 --- a/Configuration/AppSettings.cs +++ b/Configuration/AppSettings.cs @@ -12,6 +12,7 @@ namespace WebmrAPI.Configuration { public int ProcessScanInterval { get; set; } = 5; public int MemoryRegionScanTimeout { get; set; } = 30; + public bool AllowProcessReadAccess { get; set; } = true; } public class WebServerSettings diff --git a/Exceptions/GettingModuleInfoException.cs b/Exceptions/GettingModuleInfoException.cs new file mode 100644 index 0000000..95487f1 --- /dev/null +++ b/Exceptions/GettingModuleInfoException.cs @@ -0,0 +1,9 @@ +// File: Exceptions/GettingModuleInfoException.cs + +namespace WebmrAPI.Exceptions +{ + public class GettingModuleInfoException : ProcessMonitorException + { + public GettingModuleInfoException(string message) : base(message) { } + } +} diff --git a/Exceptions/MemoryRegionException.cs b/Exceptions/MemoryRegionException.cs new file mode 100644 index 0000000..108355a --- /dev/null +++ b/Exceptions/MemoryRegionException.cs @@ -0,0 +1,9 @@ +// File: Exceptions/MemoryRegionException.cs + +namespace WebmrAPI.Exceptions +{ + public class MemoryRegionException : ProcessMonitorException + { + public MemoryRegionException(string message) : base(message) { } + } +} diff --git a/Exceptions/ProcessAccessDeniedException.cs b/Exceptions/ProcessAccessDeniedException.cs new file mode 100644 index 0000000..dadf8b8 --- /dev/null +++ b/Exceptions/ProcessAccessDeniedException.cs @@ -0,0 +1,9 @@ +// File: Exceptions/ProcessAccessDeniedException.cs + +namespace WebmrAPI.Exceptions +{ + public class ProcessAccessDeniedException : ProcessMonitorException + { + public ProcessAccessDeniedException(string message) : base(message) { } + } +} diff --git a/Exceptions/ProcessMonitorException.cs b/Exceptions/ProcessMonitorException.cs index a2943f4..cc2ccf3 100644 --- a/Exceptions/ProcessMonitorException.cs +++ b/Exceptions/ProcessMonitorException.cs @@ -1,4 +1,6 @@ -namespace WebmrAPI.Exceptions +// File: Exceptions/ProcessMonitorException.cs + +namespace WebmrAPI.Exceptions { public class ProcessMonitorException : Exception { diff --git a/Models/MemoryRegion.cs b/Models/MemoryRegion.cs index 2a01a24..26b19cf 100644 --- a/Models/MemoryRegion.cs +++ b/Models/MemoryRegion.cs @@ -1,10 +1,11 @@ // File: Models/MemoryRegion.cs using System.Text.Json.Serialization; +using WebmrAPI.Utils; namespace WebmrAPI.Models { - public class MemoryRegion : ConcurrentDTO + public class MemoryRegion : ConcurrentObject { private long _addr = 0; private ulong _size = 0; diff --git a/Models/MemoryRegionInfo.cs b/Models/MemoryRegionInfo.cs index 733b8f2..5f48b61 100644 --- a/Models/MemoryRegionInfo.cs +++ b/Models/MemoryRegionInfo.cs @@ -1,26 +1,26 @@ // File: Models/MemoryRegionInfo.cs -using WebmrAPI.Services; +using WebmrAPI.Utils; namespace WebmrAPI.Models { public class MemoryRegionInfo : MemoryRegion { - private WinApi.MemoryState _state = 0; - private WinApi.MemoryPageProtectionState _protect = 0; - private WinApi.MemoryType _type = 0; + private WindowsProcess.MemoryState _state = 0; + private WindowsProcess.MemoryPageProtectionState _protect = 0; + private WindowsProcess.MemoryType _type = 0; - public WinApi.MemoryState MemoryState + public WindowsProcess.MemoryState MemoryState { get => LockedGet(ref _state); set => LockedSet(ref _state, value); } - public WinApi.MemoryPageProtectionState MemoryPageProtection + public WindowsProcess.MemoryPageProtectionState MemoryPageProtection { get => LockedGet(ref _protect); set => LockedSet(ref _protect, value); } - public WinApi.MemoryType MemoryType + public WindowsProcess.MemoryType MemoryType { get => LockedGet(ref _type); set => LockedSet(ref _type, value); diff --git a/Models/ProcessBaseInfo.cs b/Models/ProcessBaseInfo.cs index f624fe4..12c1edf 100644 --- a/Models/ProcessBaseInfo.cs +++ b/Models/ProcessBaseInfo.cs @@ -4,15 +4,15 @@ using System.Text.Json.Serialization; namespace WebmrAPI.Models { - public enum ProcessStatus - { - Undefined, - Running, - NotResponding - } - public class ProcessBaseInfo : MemoryRegion { + public enum ProcessStatus + { + Undefined, + Running, + NotResponding + } + private TimeSpan _lastPTime = TimeSpan.Zero; private TimeSpan _curPTime = TimeSpan.Zero; diff --git a/Models/ProcessInfo.cs b/Models/ProcessInfo.cs index 994a7ce..1b5f12a 100644 --- a/Models/ProcessInfo.cs +++ b/Models/ProcessInfo.cs @@ -2,25 +2,36 @@ using System.Runtime.Versioning; using System.Text.Json.Serialization; +using WebmrAPI.Utils; namespace WebmrAPI.Models { [SupportedOSPlatform("windows")] public class ProcessInfo : ProcessBaseInfo { - private DateTime _lastUpdate = DateTime.MinValue; - private List _regions = new(); - [JsonIgnore] - public DateTime LastUpdate + public LazyConcurrentContainer MemoryRegionsContainer { get; set; } = new(); + [JsonIgnore] + public LazyConcurrentContainer ModulesContainer { get; set; } = new(); + [JsonIgnore] + public LazyConcurrentContainer ThreadsContainer { get; set; } = new(); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? MemoryRegions { - get => LockedGet(ref _lastUpdate); - set => LockedSet(ref _lastUpdate, value); + get => MemoryRegionsContainer.Values; } - public List MemoryRegions + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? Modules { - get => LockedGet(ref _regions); - set => LockedSet(ref _regions, value); + get => ModulesContainer.Values; + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? Threads + { + get => ThreadsContainer.Values; } } } diff --git a/Models/ProcessModuleInfo.cs b/Models/ProcessModuleInfo.cs new file mode 100644 index 0000000..816f10f --- /dev/null +++ b/Models/ProcessModuleInfo.cs @@ -0,0 +1,37 @@ +// File: Models/ProcessModuleInfo.cs + +using System.Text.Json.Serialization; + +namespace WebmrAPI.Models +{ + public class ProcessModuleInfo : MemoryRegion + { + private string? _moduleName = null; + private string? _fileName = null; + private long _entrypointAddress = 0; + + public string? ModuleName + { + get => LockedGet(ref _moduleName); + set => LockedSet(ref _moduleName, value); + } + + public string? FileName + { + get => LockedGet(ref _fileName); + set => LockedSet(ref _fileName, value); + } + + public string? EntrypointAddress + { + get => (EntrypointRawAddress > 0) ? $"0x{EntrypointRawAddress:X16}" : null; + } + + [JsonIgnore] + public long EntrypointRawAddress + { + get => LockedGet(ref _entrypointAddress); + set => LockedSet(ref _entrypointAddress, value); + } + } +} diff --git a/Models/ProcessThreadInfo.cs b/Models/ProcessThreadInfo.cs new file mode 100644 index 0000000..2f9fd71 --- /dev/null +++ b/Models/ProcessThreadInfo.cs @@ -0,0 +1,52 @@ +// File: Models/ProcessThreadInfo.cs + +using System.Text.Json.Serialization; +using WebmrAPI.Utils; + +namespace WebmrAPI.Models +{ + public class ProcessThreadInfo : ConcurrentObject + { + private TimeSpan _lastPTime = TimeSpan.Zero; + private TimeSpan _curPTime = TimeSpan.Zero; + + private int _id = 0; + private int _currentPriority = 0; + private int _basePriority = 0; + private double _cpuUsage = 0; + + [JsonIgnore] + public TimeSpan TotalProcessorTime + { + set { lock (_lock) { _lastPTime = _curPTime; _curPTime = value; } } + } + [JsonIgnore] + public double ProcessorTime + { + get { lock (_lock) return (_curPTime - _lastPTime).TotalMilliseconds; } + } + + public int ID + { + get => LockedGet(ref _id); + set => LockedSet(ref _id, value); + } + + public int CurrentPriority + { + get => LockedGet(ref _currentPriority); + set => LockedSet(ref _currentPriority, value); + } + + public int BasePriority + { + get => LockedGet(ref _basePriority); + set => LockedSet(ref _basePriority, value); + } + public double CpuUsage + { + get => LockedGet(ref _cpuUsage); + set => LockedSet(ref _cpuUsage, value); + } + } +} diff --git a/Services/ProcessMonitor.cs b/Services/ProcessMonitor.cs index 6335ba1..3971d9f 100644 --- a/Services/ProcessMonitor.cs +++ b/Services/ProcessMonitor.cs @@ -1,65 +1,62 @@ // File: Services/ProcessMonitor.cs using Microsoft.Extensions.Options; -using System.Collections.Concurrent; -using System.Management; using System.Runtime.Versioning; using WebmrAPI.Configuration; +using WebmrAPI.Exceptions; using WebmrAPI.Models; +using WebmrAPI.Services.Scanners; +using WebmrAPI.Utils; namespace WebmrAPI.Services { [SupportedOSPlatform("windows")] public class ProcessMonitor : IHostedService, IDisposable { - private DateTime _lastScanTime = DateTime.MinValue; - private DateTime _currentScanTime = DateTime.MinValue; - private Timer? _timer; private readonly ILogger _logger; - private readonly MonitoringSettings _config; + private readonly MonitoringSettings _config; + private LazyConcurrentContainer _processes = new(); + private ScanProvider _provider; - private readonly object _lock = new object(); - private ConcurrentDictionary _processesBuffer = new(); + public ILogger Logger { get => _logger; } + public MonitoringSettings Config { get => _config; } + public LazyConcurrentContainer Processes { get => _processes; } - private DateTime ScanTime + public IEnumerable? GetBufferedProcesses() { - set - { - _lastScanTime = _currentScanTime; - _currentScanTime = value; - } + return _processes.Values; } - private TimeSpan Elapsed + public ProcessInfo? GetProcessDetails(int pid, ScanTarget target = ScanTarget.ProcessDetails) { - get => _currentScanTime - _lastScanTime; - } - - public IEnumerable GetBufferedProcesses() - { - lock (_lock) + if (_processes.Container != null && _processes.Container.TryGetValue(pid, out var info)) { - return _processesBuffer.Values.ToList(); - } - } - - public ProcessInfo? GetProcessDetails(int pid) - { - lock (_lock) - { - if (_processesBuffer.TryGetValue(pid, out ProcessInfo? data)) + try { - return PopulateProcessInfo(data); + _provider.CreateScanTask(info, target).Scan(); + _logger.LogInformation($"Scan details of the process {info.Name} (PID: {info.PID}) was completed."); + return info; + } + catch (ProcessMonitorException ex) + { + _logger.LogWarning(ex.Message); + } + catch (Exception ex) + { + _logger.LogError($"Unhandled error during scanning of the process {info.Name} (PID: {info.PID}): {ex.Message}"); } - return null; } + + return null; } + public ProcessMonitor(ILogger logger, IOptions settings) { _logger = logger; _config = settings.Value.Monitoring; + _provider = new(this); } public Task StartAsync(CancellationToken cancellationToken) @@ -76,236 +73,24 @@ namespace WebmrAPI.Services return Task.CompletedTask; } - private bool OpenProcess(ProcessInfo process, out IntPtr ctx) - { - ctx = WinApi.OpenProcess(WinApi.PROCESS_QUERY_INFORMATION | WinApi.PROCESS_VM_OPERATION, false, (uint)process.PID); - if (ctx == IntPtr.Zero) - { - var errno = WinApi.LastError; - - if (errno == 5) - { - _logger.LogWarning($"Access denied to process {process.Name} (PID: {process.PID})"); - } - else - { - _logger.LogError($"WinAPI call 'OpenProcess for PID {process.PID}' failed", errno); - } - return false; - } - - return true; - } - - private ProcessInfo PopulateProcessInfo(ProcessInfo dst, List regions) - { - dst.MemoryRegions = regions; - dst.LastUpdate = DateTime.UtcNow; - return dst; - } - - private ProcessInfo PopulateProcessInfo(ProcessInfo dst) - { - if ((DateTime.UtcNow - dst.LastUpdate).TotalSeconds < _config.MemoryRegionScanTimeout) return dst; - - IntPtr cur = IntPtr.Zero; - IntPtr ctx = IntPtr.Zero; - long next = 0; - var regions = new List(); - - try - { - if (OpenProcess(dst, out ctx)) - while ((next > cur.ToInt64() && next < 0x7fffffffffff) || next == 0) - { - WinApi.MEMORY_BASIC_INFORMATION mbi; - - cur = new IntPtr(next); - - if (WinApi.VirtualQueryEx(ctx, cur, out mbi, WinApi.MBISize) == 0) - { - _logger.LogWarning($"VirtualQueryEx for PID {dst.PID} at address 0x{cur.ToInt64():X12} failed"); - break; - } - - if (mbi.State == WinApi.MemoryState.Commit || mbi.State == WinApi.MemoryState.Reserve) - { - regions.Add(new MemoryRegionInfo - { - MemoryAddress = mbi.BaseAddress.ToInt64(), - MemorySize = mbi.RegionSize.ToUInt64(), - MemoryState = mbi.State, - MemoryPageProtection = mbi.PageProtection, - MemoryType = mbi.Type - }); - } - - next = mbi.BaseAddress.ToInt64() + (long)mbi.RegionSize.ToUInt64(); - } - } - finally - { - if (ctx != IntPtr.Zero) WinApi.CloseHandle(ctx); - } - - return PopulateProcessInfo(dst, regions); - } - private void PopulateBaseProcessInfo(ProcessInfo dst, System.Diagnostics.Process process, Dictionary wmiData) - { - dst.PID = process.Id; - dst.MemorySize = (ulong)process.WorkingSet64; - dst.ThreadCount = process.Threads.Count; - dst.TotalProcessorTime = process.TotalProcessorTime; - dst.MemoryAddress = 0x000000000000; - - if (wmiData.TryGetValue(process.Id, out var entry)) - { - dst.Name = entry.Name; - dst.CommandLine = entry.CommandLine; - dst.ParentPID = entry.ParentPID; - } - else - { - dst.Name = process.ProcessName; - _logger.LogDebug("No WMI data was found for PID {pid}.", process.Id); - } - - try - { - if (process.MainModule != null) - { - dst.FileName = process.MainModule.FileName; - dst.MemoryAddress = process.MainModule.BaseAddress.ToInt64(); - } - else - { - _logger.LogDebug($"Process {process.ProcessName} (PID: {process.Id}) process has no MainModule"); - } - } - catch (System.ComponentModel.Win32Exception ex) - { - _logger.LogDebug($"Process {process.ProcessName} (PID: {process.Id}), getting process address ends with message: {ex.Message}"); - } - catch (Exception ex) - { - _logger.LogError($"Process {process.ProcessName} (PID: {process.Id}), an error occurred while getting process address: {ex.Message}"); - } - - try - { - dst.StartTime = process.StartTime; - } - catch (Exception ex) - { - _logger.LogDebug("Could not get StartTime for PID {Pid}: {Message}", process.Id, ex.Message); - } - - try - { - process.Refresh(); - dst.Status = process.Responding ? ProcessStatus.Running : ProcessStatus.NotResponding; - } - catch (Exception ex) - { - dst.Status = ProcessStatus.Undefined; - _logger.LogDebug("Could not get Status for PID {Pid}: {Message}", process.Id, ex.Message); - } - } - private void DoWork(object? state) { _logger.LogDebug("Initiating process scan..."); try { - var cur = new Dictionary(); - var wmiData = GetWmiProcessData(); - ScanTime = DateTime.UtcNow; - - foreach (var process in System.Diagnostics.Process.GetProcesses()) - { - ProcessInfo? processInfo; - - if (process.Id == 0 || process.Id == 4) - { - process.Dispose(); - continue; - } - - if (_processesBuffer.TryGetValue(process.Id, out var existingProcessInfo)) - { - processInfo = existingProcessInfo; - } - else - { - processInfo = new ProcessInfo(); - } - - try - { - PopulateBaseProcessInfo(processInfo, process, wmiData); - - if (_lastScanTime != DateTime.MinValue && Elapsed.TotalMilliseconds > 0) - { - processInfo.CpuUsage = (processInfo.ProcessorTime / Elapsed.TotalMilliseconds) / Environment.ProcessorCount * 100.0; - if (processInfo.CpuUsage > 100.0) processInfo.CpuUsage = 100.0; - } - else - { - processInfo.CpuUsage = 0.0; - } - - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to get full information for process {ProcessId} ({ProcessName}).", - process.Id, process.ProcessName); - } - finally - { - cur.TryAdd(process.Id, processInfo); - process?.Dispose(); - } - - } - - lock (_lock) - { - _processesBuffer = new ConcurrentDictionary(cur); - } - _logger.LogInformation("Process buffer updated, contains {Count} processes.", _processesBuffer.Count); + _provider.CreateScanTask().Scan(); + _logger.LogInformation($"Process buffer updated, contains {Processes.Container?.Count} processes."); + } + catch (ProcessMonitorException ex) + { + _logger.LogWarning(ex.Message); } catch (Exception ex) { - _logger.LogError(ex, "Unhandled error during process monitoring cycle."); + _logger.LogError($"Unhandled error during process monitoring cycle: {ex.Message}"); } } - private Dictionary GetWmiProcessData() - { - var data = new Dictionary(); - try - { - using (var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, CommandLine, ParentProcessId FROM Win32_Process")) - using (ManagementObjectCollection processes = searcher.Get()) - { - foreach (ManagementObject obj in processes) - { - int pid = Convert.ToInt32(obj["ProcessId"]); - int ppid = Convert.ToInt32(obj["ParentProcessId"]); - string? name = obj["Name"]?.ToString(); - string? cmd = obj["CommandLine"]?.ToString(); - - data[pid] = (name, cmd, ppid); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "The data of the WMI process could not be retrieved."); - } - return data; - } - public void Dispose() { _timer?.Dispose(); diff --git a/Services/Scanners/AbstractCpuScanner.cs b/Services/Scanners/AbstractCpuScanner.cs new file mode 100644 index 0000000..5ce6b9e --- /dev/null +++ b/Services/Scanners/AbstractCpuScanner.cs @@ -0,0 +1,23 @@ +// File: Services/Scanners/AbstractCpuScanner.cs + +using WebmrAPI.Utils; + +namespace WebmrAPI.Services.Scanners +{ + public abstract class AbstractCpuScanner : AbstractScanner where T : new() + { + public static double CalcCpuUsage(double pTime, double elapsed) + { + double cpuUsage = 0; + if (elapsed > 0) + { + cpuUsage = (pTime / elapsed) / Environment.ProcessorCount * 100.0; + if (cpuUsage > 100.0) cpuUsage = 100.0; + } + return cpuUsage; + } + + public AbstractCpuScanner(IScanProvider scanner, LazyConcurrentContainer container) + : base(scanner, container) { } + } +} diff --git a/Services/Scanners/AbstractScanner.cs b/Services/Scanners/AbstractScanner.cs new file mode 100644 index 0000000..a1614e5 --- /dev/null +++ b/Services/Scanners/AbstractScanner.cs @@ -0,0 +1,66 @@ +// File: Services/Scanners/AbstractScanner.cs + +using WebmrAPI.Utils; + +namespace WebmrAPI.Services.Scanners +{ + public abstract class AbstractScanner : IScannable where T : new() + { + internal IScanProvider _provider; + LazyConcurrentContainer _container; + + abstract public ScanTarget Target { get; } + + internal IScanProvider Provider { get => _provider; } + internal LazyConcurrentContainer Container { get => _container; } + internal int Timeout { get => _provider.GetTimeout(Target); } + + internal bool IsActual() + { + return (DateTime.UtcNow - Container.LastUpdate).TotalSeconds < Timeout; + } + internal bool GetFromCacheOrNew(long key, out T res) + { + var cache = Container.Container; + + bool result = true; + T? found; + if (cache == null || !cache.TryGetValue(key, out found)) + { + result = false; + found = new(); + } + res = found; + return result; + } + + abstract internal bool Scan(out Dictionary? data); + + public void Scan() + { + bool success = false; + if (Container.Container != null && IsActual()) return; + + try + { + if (success = Scan(out var data)) + { + Container.SetContainer(data); + } + } + finally + { + if (!success) + { + Container.CleanContainer(); + } + } + } + + public AbstractScanner(IScanProvider scanProvider, LazyConcurrentContainer container) + { + _provider = scanProvider; + _container = container; + } + } +} diff --git a/Services/Scanners/IScanProvider.cs b/Services/Scanners/IScanProvider.cs new file mode 100644 index 0000000..848ed11 --- /dev/null +++ b/Services/Scanners/IScanProvider.cs @@ -0,0 +1,11 @@ +// File: Services/Scanners/IScanProvider.cs + +namespace WebmrAPI.Services.Scanners +{ + public interface IScanProvider + { + public bool ProcessReadAccess { get; } + public ILogger Logger { get; } + public int GetTimeout(ScanTarget target); + } +} diff --git a/Services/Scanners/IScannable.cs b/Services/Scanners/IScannable.cs new file mode 100644 index 0000000..df6d3fc --- /dev/null +++ b/Services/Scanners/IScannable.cs @@ -0,0 +1,10 @@ +// File: Services/Scanners/IScannable.cs + +namespace WebmrAPI.Services.Scanners +{ + public interface IScannable + { + public ScanTarget Target { get; } + public void Scan(); + } +} diff --git a/Services/Scanners/MemoryRegionScanner.cs b/Services/Scanners/MemoryRegionScanner.cs new file mode 100644 index 0000000..2c77b29 --- /dev/null +++ b/Services/Scanners/MemoryRegionScanner.cs @@ -0,0 +1,61 @@ +// File: Services/Scanners/MemoryRegionScanner.cs + +using WebmrAPI.Exceptions; +using WebmrAPI.Models; +using WebmrAPI.Utils; + +namespace WebmrAPI.Services.Scanners +{ + public class MemoryRegionScanner : AbstractScanner + { + override public ScanTarget Target { get => ScanTarget.MemoryRegions; } + internal int PID { get; private set; } + + override internal bool Scan(out Dictionary? data) + { + data = new(); + + try + { + using (var process = new WindowsProcess(PID)) + { + var regions = data; + process.ForeachMemoryRegion(info => + { + long addr = info.BaseAddress.ToInt64(); + regions.Add(addr, new MemoryRegionInfo + { + MemoryAddress = addr, + MemorySize = info.RegionSize.ToUInt64(), + MemoryState = info.State, + MemoryPageProtection = info.PageProtection, + MemoryType = info.Type + }); + }); + } + + return true; + } + catch (ProcessAccessDeniedException ex) + { + Provider.Logger.LogWarning($"Access denied to process {PID} for memory region scanning. Error: {ex.Message}"); + } + catch (MemoryRegionException ex) + { + Provider.Logger.LogWarning($"Error scanning memory regions for PID {PID}. Error: {ex.Message}"); + return true; + } + catch (Exception ex) + { + Provider.Logger.LogError($"An unexpected error occurred while scanning memory regions for PID {PID}. Error: {ex.Message}"); + } + return false; + } + + public MemoryRegionScanner(IScanProvider scanner, LazyConcurrentContainer container, int pid) + : base(scanner, container) + { + PID = pid; + } + } +} diff --git a/Services/Scanners/ModuleScanner.cs b/Services/Scanners/ModuleScanner.cs new file mode 100644 index 0000000..bb97cb2 --- /dev/null +++ b/Services/Scanners/ModuleScanner.cs @@ -0,0 +1,98 @@ +// File: Services/Scanners/ModuleScanner.cs + +using System.Diagnostics; +using WebmrAPI.Exceptions; +using WebmrAPI.Models; +using WebmrAPI.Utils; + +namespace WebmrAPI.Services.Scanners +{ + public class ModuleScanner : AbstractScanner + { + override public ScanTarget Target { get => ScanTarget.Modules; } + internal int PID { get; private set; } + + override internal bool Scan(out Dictionary? data) + { + data = new(); + + try + { + using (var process = Process.GetProcessById(PID)) + { + foreach (ProcessModule module in process.Modules) + { + long addr = module.BaseAddress.ToInt64(); + data.Add(addr, new ProcessModuleInfo + { + MemoryAddress = addr, + ModuleName = module.ModuleName, + FileName = module.FileName, + MemorySize = (ulong)module.ModuleMemorySize, + EntrypointRawAddress = module.EntryPointAddress.ToInt64() + }); + } + } + + return true; + } + catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 5) + { + Provider.Logger.LogWarning($"Access denied to Process.Modules for PID {PID}. Attempting P/Invoke. Error: {ex.Message}"); + if (!Provider.ProcessReadAccess) return false; + } + catch (InvalidOperationException ex) + { + Provider.Logger.LogWarning($"Process with PID {PID} might have exited before module scanning. Error: {ex.Message}"); + return false; + } + catch (Exception ex) + { + Provider.Logger.LogError(ex, $"An unexpected error occurred while getting modules for PID {PID} using managed API."); + return false; + } + + try + { + using (var process = new WindowsProcess(PID, true)) + { + var modules = data; + process.ForeachLoadedModule(info => + { + long addr = info.BaseAddress.ToInt64(); + modules.Add(addr, new ProcessModuleInfo + { + ModuleName = info.Name.ToString(), + MemoryAddress = addr, + FileName = info.FileName.ToString(), + MemorySize = info.MemorySize, + EntrypointRawAddress = info.EntryPointAddress.ToInt64() + }); + }); + } + + return true; + } + catch (GettingModuleInfoException ex) + { + Provider.Logger.LogDebug(ex.Message); + } + catch (ProcessMonitorException ex) + { + Provider.Logger.LogError(ex.Message); + } + catch (Exception ex) + { + Provider.Logger.LogError(ex, $"An error occurred while getting modules for PID {PID} using P/Invoke."); + } + + return false; + } + + public ModuleScanner(IScanProvider scanner, LazyConcurrentContainer container, int pid) + : base(scanner, container) + { + PID = pid; + } + } +} diff --git a/Services/Scanners/ProcessScanner.cs b/Services/Scanners/ProcessScanner.cs new file mode 100644 index 0000000..c1bc973 --- /dev/null +++ b/Services/Scanners/ProcessScanner.cs @@ -0,0 +1,148 @@ +// File: Services/Scanners/ProcessScanner.cs + +using System.Diagnostics; +using System.Management; +using WebmrAPI.Models; +using WebmrAPI.Utils; + +namespace WebmrAPI.Services.Scanners +{ + public class ProcessScanner : AbstractCpuScanner + { + override public ScanTarget Target { get => ScanTarget.Processes; } + + private Task> _wmi; + + private Dictionary WmiData + { + get => _wmi.Result; + } + + async private Task> GetWmiDataAsync() + { + var wmi = new Dictionary(); + + try + { + using (var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, CommandLine, ParentProcessId FROM Win32_Process")) + using (var processes = await Task.Run(() => { return searcher.Get(); })) + await Task.Run(() => + { + foreach (var obj in processes) + { + int pid = Convert.ToInt32(obj["ProcessId"]); + int ppid = Convert.ToInt32(obj["ParentProcessId"]); + string? name = obj["Name"]?.ToString(); + string? cmd = obj["CommandLine"]?.ToString(); + + wmi[pid] = (name, cmd, ppid); + } + }); + } + catch (Exception ex) + { + Provider.Logger.LogError(ex, "Failed to retrieve WMI process data for all processes."); + } + + return wmi; + } + + private bool Populate(ProcessInfo info, Process process) + { + info.TotalProcessorTime = process.TotalProcessorTime; + info.MemorySize = (ulong)process.WorkingSet64; + info.ThreadCount = process.Threads.Count; + + try + { + if (info.MemoryAddress == 0 && process.MainModule != null) + { + info.FileName = process.MainModule.FileName; + info.MemoryAddress = process.MainModule.BaseAddress.ToInt64(); + } + } + catch (Exception ex) + { + Provider.Logger.LogWarning($"Process {process.ProcessName} (PID: {process.Id}), an error occurred while getting process address: {ex.Message}"); + } + + if (info.MemoryAddress != 0) + { + try + { + if (WmiData.TryGetValue(process.Id, out var entry)) + { + info.Name = entry.Name; + info.CommandLine = entry.CommandLine; + info.ParentPID = entry.ParentPID; + } + else + { + info.Name = process.ProcessName; + Provider.Logger.LogDebug($"WMI data not found for PID {process.Id}. Falling back to Process.ProcessName."); + } + + if (info.StartTime == null) + { + try { info.StartTime = process.StartTime; } + catch (Exception ex) + { + Provider.Logger.LogDebug($"Could not get StartTime for PID {process.Id}: {ex.Message}"); + } + } + process.Refresh(); + info.Status = process.Responding ? ProcessInfo.ProcessStatus.Running : ProcessInfo.ProcessStatus.NotResponding; + } + catch (Exception ex) + { + info.Status = ProcessInfo.ProcessStatus.Undefined; + Provider.Logger.LogDebug($"Could not get Status for PID {process.Id}: {ex.Message}"); + } + return true; + } + return false; + } + + override internal bool Scan(out Dictionary? data) + { + data = new(); + + foreach (var process in Process.GetProcesses()) + using (process) + { + ProcessInfo? info; + + if (process.Id == 0 || process.Id == 4) + { + process.Dispose(); + continue; + } + + if (GetFromCacheOrNew(process.Id, out info)) + { + info.CpuUsage = CalcCpuUsage(info.ProcessorTime, Container.Elapsed.TotalMilliseconds); + } + else + { + info.PID = process.Id; + } + + try + { + if (Populate(info, process)) + { + data.Add(info.PID, info); + } + } catch (Exception) { } + } + + return true; + } + + public ProcessScanner(IScanProvider scanner, LazyConcurrentContainer container) + : base(scanner, container) + { + _wmi = GetWmiDataAsync(); + } + } +} diff --git a/Services/Scanners/ScanProvider.cs b/Services/Scanners/ScanProvider.cs new file mode 100644 index 0000000..5af8ec9 --- /dev/null +++ b/Services/Scanners/ScanProvider.cs @@ -0,0 +1,78 @@ +// File: Services/Scanners/ScanProvider.cs + +using WebmrAPI.Models; + +namespace WebmrAPI.Services.Scanners +{ + public class ScanProvider : IScanProvider + { + private ProcessMonitor _monitor; + public bool ProcessReadAccess { get => _monitor.Config.AllowProcessReadAccess; } + public ILogger Logger { get => _monitor.Logger; } + + public int GetTimeout(ScanTarget target) + { + switch (target) + { + case ScanTarget.Processes: return _monitor.Config.MemoryRegionScanTimeout; + case ScanTarget.Modules: return _monitor.Config.MemoryRegionScanTimeout; + case ScanTarget.Threads: return _monitor.Config.MemoryRegionScanTimeout; + default: return _monitor.Config.ProcessScanInterval; + } + } + + public IScannable CreateScanTask() + { + return new ProcessScanner(this, _monitor.Processes); + } + + private IScannable CreateScanTask(ScanTarget target) + { + var scanner = new ScanQueue(); + + CreateScanTask().Scan(); + var data = _monitor.Processes.Values; + + if (data != null) + { + foreach (var process in data) + { + scanner.Add(CreateScanTask(process, target)); + } + } + return scanner; + } + + public IScannable CreateScanTask(ProcessInfo? process, ScanTarget target = ScanTarget.ProcessDetails) + { + var scanner = new ScanQueue(); + + if (target.HasFlag(ScanTarget.Processes)) + { + return CreateScanTask(target^ScanTarget.Processes); + } + + if (target.HasFlag(ScanTarget.MemoryRegions) && process != null) + { + scanner.Add(new MemoryRegionScanner(this, process.MemoryRegionsContainer, process.PID)); + } + + if (target.HasFlag(ScanTarget.Modules) && process != null) + { + scanner.Add(new ModuleScanner(this, process.ModulesContainer, process.PID)); + } + + if (target.HasFlag(ScanTarget.Threads) && process != null) + { + scanner.Add(new ThreadScanner(this, process.ThreadsContainer, process.PID)); + } + + return scanner; + } + + public ScanProvider(ProcessMonitor monitoringService) + { + _monitor = monitoringService; + } + } +} diff --git a/Services/Scanners/ScanQueue.cs b/Services/Scanners/ScanQueue.cs new file mode 100644 index 0000000..336c61c --- /dev/null +++ b/Services/Scanners/ScanQueue.cs @@ -0,0 +1,27 @@ +// File: Services/Scanners/ScanQueue.cs + +namespace WebmrAPI.Services.Scanners +{ + public class ScanQueue : IScannable + { + private ScanTarget _target = 0; + public ScanTarget Target { get => _target; } + + private Queue _queue = new(); + + public ScanQueue Add(IScannable item) + { + _queue.Enqueue(item); + _target |= item.Target; + return this; + } + + public void Scan() + { + while (_queue.Count > 0) + { + _queue.Dequeue().Scan(); + } + } + } +} diff --git a/Services/Scanners/ScanTarget.cs b/Services/Scanners/ScanTarget.cs new file mode 100644 index 0000000..c9e8f4d --- /dev/null +++ b/Services/Scanners/ScanTarget.cs @@ -0,0 +1,16 @@ +// File: Services/Scanners/ScanTarget.cs + +namespace WebmrAPI.Services.Scanners +{ + [Flags] + public enum ScanTarget : byte + { + Processes = 0x01, + MemoryRegions = 0x02, + Modules = 0x04, + Threads = 0x08, + + ProcessDetails = MemoryRegions | Modules | Threads, + All = Processes | ProcessDetails + } +} diff --git a/Services/Scanners/ThreadScanner.cs b/Services/Scanners/ThreadScanner.cs new file mode 100644 index 0000000..15721ef --- /dev/null +++ b/Services/Scanners/ThreadScanner.cs @@ -0,0 +1,65 @@ +// File: Services/Scanners/ThreadScanner.cs + +using System.Diagnostics; +using WebmrAPI.Models; +using WebmrAPI.Utils; + +namespace WebmrAPI.Services.Scanners +{ + public class ThreadScanner : AbstractCpuScanner + { + override public ScanTarget Target { get => ScanTarget.Threads; } + internal int PID { get; private set; } + + override internal bool Scan(out Dictionary? data) + { + data = new(); + + try + { + using (var process = Process.GetProcessById(PID)) + foreach (ProcessThread thread in process.Threads) + { + ProcessThreadInfo info; + + if (GetFromCacheOrNew(thread.Id, out info)) + { + info.CpuUsage = CalcCpuUsage(info.ProcessorTime, Container.Elapsed.TotalMilliseconds); + } + else + { + info.ID = thread.Id; + } + + info.BasePriority = thread.BasePriority; + info.CurrentPriority = thread.CurrentPriority; + info.TotalProcessorTime = thread.TotalProcessorTime; + + data.Add(info.ID, info); + } + } + catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 5) + { + Provider.Logger.LogWarning($"Access denied to process {PID} for thread scanning. Error: {ex.Message}"); + return false; + } + catch (InvalidOperationException ex) + { + Provider.Logger.LogWarning($"Process {PID} might have exited during thread enumeration. Error: {ex.Message}"); + return false; + } + catch (Exception ex) + { + Provider.Logger.LogError($"An unexpected error occurred while scanning threads for PID {PID}. Error: {ex.Message}"); + return false; + } + return true; + } + + public ThreadScanner(IScanProvider scanner, LazyConcurrentContainer container, int pid) + : base(scanner, container) + { + PID = pid; + } + } +} diff --git a/Services/WinApi.cs b/Services/WinApi.cs deleted file mode 100644 index 48830b1..0000000 --- a/Services/WinApi.cs +++ /dev/null @@ -1,251 +0,0 @@ -// File: Services/WinApi.cs - -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text.Json.Serialization; -using WebmrAPI.Utils; - -namespace WebmrAPI.Services -{ - [SupportedOSPlatform("windows")] - public static class WinApi - { - public const uint PROCESS_QUERY_INFORMATION = 0x00000400; - public const uint PROCESS_VM_READ = 0x00000010; - public const uint PROCESS_VM_OPERATION = 0x00000008; - - public static int LastError { get => Marshal.GetLastWin32Error(); } - public static uint MBISize { get => (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)); } - - [JsonConverter(typeof(JsonEnumConverter))] - public enum MemoryState : uint - { - Undefined = 0, - Commit = 0x00001000, - Reserve = 0x00002000, - Free = 0x00010000, - } - - [JsonConverter(typeof(JsonEnumConverter))] - public enum MemoryType : uint - { - Undefined = 0, - Image = 0x01000000, - Mapped = 0x00040000, - Private = 0x00020000, - } - - [JsonConverter(typeof(JsonEnumConverter))] - public enum MemoryPageProtectionState : uint - { - Undefined = 0, - NoAccess = 0x0001, - ReadOnly = 0x0002, - ReadWrite = 0x0004, - WriteCopy = 0x0008, - Execute = 0x0010, - ExecuteRead = 0x0020, - ExecuteReadWrite = 0x0040, - ExecuteWriteCopy = 0x0080, - Guard = 0x0100, - NoCache = 0x0200, - WriteCombine = 0x0400, - } - - // --- P/Invoke Declarations --- - - // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information - [StructLayout(LayoutKind.Sequential)] - public struct MEMORY_BASIC_INFORMATION - { - public IntPtr BaseAddress; - public IntPtr AllocationBase; - public uint AllocationProtect; - public ushort PartitionId; - public UIntPtr RegionSize; - public MemoryState State; - public MemoryPageProtectionState PageProtection; - public MemoryType Type; - } - - - // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr OpenProcess( - uint dwDesiredAccess, - [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, - uint dwProcessId - ); - - // https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CloseHandle(IntPtr hObject); - - // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-virtualqueryex - [DllImport("kernel32.dll", SetLastError = true)] - public static extern int VirtualQueryEx( - IntPtr hProcess, - IntPtr lpAddress, - out MEMORY_BASIC_INFORMATION lpBuffer, - uint dwLength - ); - - // --- Module-related P/Invokes (from psapi.dll and kernel32.dll) --- - - // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodulesex - [DllImport("psapi.dll", SetLastError = true)] - public static extern bool EnumProcessModulesEx( - IntPtr hProcess, - [Out] IntPtr[] lphModule, // Array to receive module handles - uint cb, // Size of the lphModule array, in bytes - out uint lpcbNeeded, // Number of bytes required to store all module handles - uint dwFilterFlag // Filter for modules (e.g., LIST_MODULES_ALL) - ); - - // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw - [DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern uint GetModuleFileNameEx( - IntPtr hProcess, - IntPtr hModule, - [Out] System.Text.StringBuilder lpFilename, - uint nSize - ); - - // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulebasenamew - [DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern uint GetModuleBaseName( - IntPtr hProcess, - IntPtr hModule, - [Out] System.Text.StringBuilder lpBaseName, - uint nSize - ); - - // https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-_moduleinfo - [StructLayout(LayoutKind.Sequential)] - public struct MODULEINFO - { - public IntPtr lpBaseOfDll; - public uint SizeOfImage; - public IntPtr EntryPoint; - } - - // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmoduleinformation - [DllImport("psapi.dll", SetLastError = true)] - public static extern bool GetModuleInformation( - IntPtr hProcess, - IntPtr hModule, - out MODULEINFO lpmodinfo, - uint cb - ); - - // --- Thread-related P/Invokes (from kernel32.dll and ntdll.dll) --- - - // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openthread - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr OpenThread( - uint dwDesiredAccess, - [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, - uint dwThreadId - ); - - - // https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationthread - [DllImport("ntdll.dll", SetLastError = true)] - public static extern int NtQueryInformationThread( - IntPtr ThreadHandle, - THREAD_INFO_CLASS ThreadInformationClass, - IntPtr ThreadInformation, // Pointer to buffer - uint ThreadInformationLength, - out uint ReturnLength - ); - - // Enum for ThreadInformationClass in NtQueryInformationThread - // https://ntdoc.m417z.com/threadinfoclass - public enum THREAD_INFO_CLASS : int - { - ThreadBasicInformation, // q: THREAD_BASIC_INFORMATION - ThreadTimes, // q: KERNEL_USER_TIMES - ThreadPriority, // s: KPRIORITY (requires SeIncreaseBasePriorityPrivilege) - ThreadBasePriority, // s: KPRIORITY - ThreadAffinityMask, // s: KAFFINITY - ThreadImpersonationToken, // s: HANDLE - ThreadDescriptorTableEntry, // q: DESCRIPTOR_TABLE_ENTRY (or WOW64_DESCRIPTOR_TABLE_ENTRY) - ThreadEnableAlignmentFaultFixup, // s: BOOLEAN - ThreadEventPair, // Obsolete - ThreadQuerySetWin32StartAddress, // qs: PVOID (requires THREAD_Set_LIMITED_INFORMATION) - ThreadZeroTlsCell, // s: ULONG // TlsIndex // 10 - ThreadPerformanceCount, // q: LARGE_INTEGER - ThreadAmILastThread, // q: ULONG - ThreadIdealProcessor, // s: ULONG - ThreadPriorityBoost, // qs: ULONG - ThreadSetTlsArrayAddress, // s: ULONG_PTR - ThreadIsIoPending, // q: ULONG - ThreadHideFromDebugger, // q: BOOLEAN; s: void - ThreadBreakOnTermination, // qs: ULONG - ThreadSwitchLegacyState, // s: void // NtCurrentThread // NPX/FPU - ThreadIsTerminated, // q: ULONG // 20 - ThreadLastSystemCall, // q: THREAD_LAST_SYSCALL_INFORMATION - ThreadIoPriority, // qs: IO_PRIORITY_HINT (requires SeIncreaseBasePriorityPrivilege) - ThreadCycleTime, // q: THREAD_CYCLE_TIME_INFORMATION (requires THREAD_QUERY_LIMITED_INFORMATION) - ThreadPagePriority, // qs: PAGE_PRIORITY_INFORMATION - ThreadActualBasePriority, // s: LONG (requires SeIncreaseBasePriorityPrivilege) - ThreadTebInformation, // q: THREAD_TEB_INFORMATION (requires THREAD_GET_CONTEXT + THREAD_SET_CONTEXT) - ThreadCSwitchMon, // Obsolete - ThreadCSwitchPmu, // Obsolete - ThreadWow64Context, // qs: WOW64_CONTEXT, ARM_NT_CONTEXT since 20H1 - ThreadGroupInformation, // qs: GROUP_AFFINITY // 30 - ThreadUmsInformation, // q: THREAD_UMS_INFORMATION // Obsolete - ThreadCounterProfiling, // q: BOOLEAN; s: THREAD_PROFILING_INFORMATION? - ThreadIdealProcessorEx, // qs: PROCESSOR_NUMBER; s: previous PROCESSOR_NUMBER on return - ThreadCpuAccountingInformation, // q: BOOLEAN; s: HANDLE (NtOpenSession) // NtCurrentThread // since WIN8 - ThreadSuspendCount, // q: ULONG // since WINBLUE - ThreadHeterogeneousCpuPolicy, // q: KHETERO_CPU_POLICY // since THRESHOLD - ThreadContainerId, // q: GUID - ThreadNameInformation, // qs: THREAD_NAME_INFORMATION (requires THREAD_SET_LIMITED_INFORMATION) - ThreadSelectedCpuSets, // q: ULONG[] - ThreadSystemThreadInformation, // q: SYSTEM_THREAD_INFORMATION // 40 - ThreadActualGroupAffinity, // q: GROUP_AFFINITY // since THRESHOLD2 - ThreadDynamicCodePolicyInfo, // q: ULONG; s: ULONG (NtCurrentThread) - ThreadExplicitCaseSensitivity, // qs: ULONG; s: 0 disables, otherwise enables // (requires SeDebugPrivilege and PsProtectedSignerAntimalware) - ThreadWorkOnBehalfTicket, // ALPC_WORK_ON_BEHALF_TICKET // RTL_WORK_ON_BEHALF_TICKET_EX // NtCurrentThread - ThreadSubsystemInformation, // q: SUBSYSTEM_INFORMATION_TYPE // since REDSTONE2 - ThreadDbgkWerReportActive, // s: ULONG; s: 0 disables, otherwise enables - ThreadAttachContainer, // s: HANDLE (job object) // NtCurrentThread - ThreadManageWritesToExecutableMemory, // MANAGE_WRITES_TO_EXECUTABLE_MEMORY // since REDSTONE3 - ThreadPowerThrottlingState, // qs: POWER_THROTTLING_THREAD_STATE // since REDSTONE3 (set), WIN11 22H2 (query) - ThreadWorkloadClass, // THREAD_WORKLOAD_CLASS // since REDSTONE5 // 50 - ThreadCreateStateChange, // since WIN11 - ThreadApplyStateChange, - ThreadStrongerBadHandleChecks, // s: ULONG // NtCurrentThread // since 22H1 - ThreadEffectiveIoPriority, // q: IO_PRIORITY_HINT - ThreadEffectivePagePriority, // q: ULONG - ThreadUpdateLockOwnership, // THREAD_LOCK_OWNERSHIP // since 24H2 - ThreadSchedulerSharedDataSlot, // SCHEDULER_SHARED_DATA_SLOT_INFORMATION - ThreadTebInformationAtomic, // q: THREAD_TEB_INFORMATION (requires THREAD_GET_CONTEXT + THREAD_QUERY_INFORMATION) - ThreadIndexInformation, // THREAD_INDEX_INFORMATION - MaxThreadInfoClass - } - - // Structure for ThreadBasicInformation (if needed, though ThreadQuerySetWin32StartAddress directly gives address) - // https://ntdoc.m417z.com/thread_basic_information - [StructLayout(LayoutKind.Sequential)] - public struct THREAD_BASIC_INFORMATION - { - public int ExitStatus; - public IntPtr TebBaseAddress; - public CLIENT_ID ClientId; - public IntPtr AffinityMask; - public int Priority; - public int BasePriority; - } - - // https://ntdoc.m417z.com/client_id - [StructLayout(LayoutKind.Sequential)] - public struct CLIENT_ID - { - public IntPtr UniqueProcess; - public IntPtr UniqueThread; - } - } -} diff --git a/Models/ConcurrentDTO.cs b/Utils/ConcurrentObject.cs similarity index 74% rename from Models/ConcurrentDTO.cs rename to Utils/ConcurrentObject.cs index e4a4758..ae7ba8a 100644 --- a/Models/ConcurrentDTO.cs +++ b/Utils/ConcurrentObject.cs @@ -1,8 +1,8 @@ -// File: Models/ConcurrentDTO.cs +// File: Utils/ConcurrentObject.cs -namespace WebmrAPI.Models +namespace WebmrAPI.Utils { - public class ConcurrentDTO + public class ConcurrentObject { internal readonly object _lock = new object(); diff --git a/Utils/LazyConcurrentContainer.cs b/Utils/LazyConcurrentContainer.cs new file mode 100644 index 0000000..25fdb1b --- /dev/null +++ b/Utils/LazyConcurrentContainer.cs @@ -0,0 +1,55 @@ +// File: Utils/LazyConcurrentContainer.cs + +using System.Collections.Concurrent; + +namespace WebmrAPI.Utils +{ + public class LazyConcurrentContainer : ConcurrentObject + { + private DateTime _lastScan = DateTime.MinValue; + private DateTime _currentScan = DateTime.MinValue; + private ConcurrentDictionary? _container; + + public DateTime LastUpdate + { + get => LockedGet(ref _lastScan); + } + + public TimeSpan Elapsed + { + get { lock (_lock) return _currentScan - _lastScan; } + } + + public ConcurrentDictionary? Container + { + get => LockedGet(ref _container); + set + { + lock (_lock) + { + _lastScan = _currentScan; + _currentScan = DateTime.UtcNow; + _container = value; + } + } + } + + public IEnumerable? Values + { + get => _container?.Values.ToList(); + } + + public void SetContainer(Dictionary? container) + { + Container = container != null ? new ConcurrentDictionary(container) : null; + } + public void SetContainer(ConcurrentDictionary container) + { + Container = container != null ? new ConcurrentDictionary(container) : null; + } + public void CleanContainer() + { + Container = null; + } + } +} diff --git a/Utils/WindowsProcess.cs b/Utils/WindowsProcess.cs new file mode 100644 index 0000000..66fb975 --- /dev/null +++ b/Utils/WindowsProcess.cs @@ -0,0 +1,248 @@ +// File: Utils/WindowsProcess.cs + +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.Json.Serialization; +using WebmrAPI.Exceptions; +using WebmrAPI.Models; + +namespace WebmrAPI.Utils +{ + [SupportedOSPlatform("windows")] + public class WindowsProcess : IDisposable + { + private IntPtr _ptr = IntPtr.Zero; + private int _pid = 0; + private bool _readAccess = false; + private static int LastError { get => Marshal.GetLastWin32Error(); } + private static uint MBISize { get => (uint)Marshal.SizeOf(typeof(MemoryBasicInformation)); } + + [Flags] + [JsonConverter(typeof(JsonEnumConverter))] + public enum MemoryState : uint + { + Commit = 0x00001000, + Reserve = 0x00002000, + Free = 0x00010000, + } + + [Flags] + [JsonConverter(typeof(JsonEnumConverter))] + public enum MemoryType : uint + { + Image = 0x01000000, + Mapped = 0x00040000, + Private = 0x00020000, + } + + [Flags] + [JsonConverter(typeof(JsonEnumConverter))] + public enum MemoryPageProtectionState : uint + { + NoAccess = 0x0001, + ReadOnly = 0x0002, + ReadWrite = 0x0004, + WriteCopy = 0x0008, + Execute = 0x0010, + ExecuteRead = 0x0020, + ExecuteReadWrite = 0x0040, + ExecuteWriteCopy = 0x0080, + Guard = 0x0100, + NoCache = 0x0200, + WriteCombine = 0x0400, + } + + [Flags] + public enum ModuleFilterFlags : uint + { + Default = 0x00, + Modules32Bit = 0x01, + Modules64Bit = 0x02, + ModulesAll = Modules32Bit | Modules64Bit, + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information + [StructLayout(LayoutKind.Sequential)] + public struct MemoryBasicInformation + { + public IntPtr BaseAddress; + public IntPtr AllocationBase; + public uint AllocationProtect; + public ushort PartitionId; + public UIntPtr RegionSize; + public MemoryState State; + public MemoryPageProtectionState PageProtection; + public MemoryType Type; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ModuleInformation + { + public IntPtr BaseAddress; + public uint MemorySize; + public IntPtr EntryPointAddress; + public System.Text.StringBuilder Name; + public System.Text.StringBuilder FileName; + } + + // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess( + uint dwDesiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, + int dwProcessId + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); + + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-virtualqueryex + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int VirtualQueryEx( + IntPtr hProcess, + IntPtr lpAddress, + out MemoryBasicInformation lpBuffer, + uint dwLength + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodulesex + [DllImport("psapi.dll", SetLastError = true)] + private static extern bool EnumProcessModulesEx( + IntPtr hProcess, + [Out] IntPtr[] lphModule, + uint cb, + out uint lpcbNeeded, + ModuleFilterFlags dwFilterFlag + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw + [DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern uint GetModuleFileNameEx( + IntPtr hProcess, + IntPtr hModule, + [Out] System.Text.StringBuilder lpFilename, + uint nSize + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulebasenamew + [DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern uint GetModuleBaseName( + IntPtr hProcess, + IntPtr hModule, + [Out] System.Text.StringBuilder lpBaseName, + uint nSize + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmoduleinformation + [DllImport("psapi.dll", SetLastError = true)] + private static extern bool GetModuleInformation( + IntPtr hProcess, + IntPtr hModule, + out ModuleInformation lpmodinfo, + uint cb + ); + + public void ForeachMemoryRegion(Action callback, MemoryState filter = MemoryState.Commit | MemoryState.Reserve) + { + IntPtr cur = IntPtr.Zero; + long next = 0; + var regions = new Dictionary(); + + while ((next > cur.ToInt64() && next < 0x7fffffffffff) || next == 0) + { + MemoryBasicInformation info; + + cur = new IntPtr(next); + + if (VirtualQueryEx(_ptr, cur, out info, MBISize) == 0) + { + throw new MemoryRegionException($"WinAPI call 'VirtualQueryEx for PID {_pid} at address 0x{cur.ToInt64():X16}' failed."); + } + + if ((filter & info.State) != 0) + { + callback(info); + } + + next = info.BaseAddress.ToInt64() + (long)info.RegionSize.ToUInt64(); + } + } + + public void ForeachLoadedModule(Action callback, ModuleFilterFlags filter = ModuleFilterFlags.ModulesAll, bool force = false, bool ignoreErrors = false) + { + if (!_readAccess && force) + { + Dispose(); + _readAccess = true; + Open(); + } + + IntPtr[] modules = new IntPtr[1024]; + uint nmemb; + + if (EnumProcessModulesEx(_ptr, modules, (uint)(modules.Length * IntPtr.Size), out nmemb, filter)) + { + uint count = nmemb / (uint)IntPtr.Size; + for (int i = 0; i < count; i++) + { + ModuleInformation info; + + info.Name = new System.Text.StringBuilder(256); + info.FileName = new System.Text.StringBuilder(1024); + + if ((!GetModuleInformation(_ptr, modules[i], out info, (uint)(2 * IntPtr.Size + sizeof(uint))) || + GetModuleBaseName(_ptr, modules[i], info.Name, (uint)info.Name.Capacity) <= 0 || + GetModuleFileNameEx(_ptr, modules[i], info.FileName, (uint)info.FileName.Capacity) <= 0) && !ignoreErrors) + { + throw new GettingModuleInfoException($"Failed to get module info for module handle {modules[i]} in PID {_pid}. Error: {LastError}"); + } + + callback(info); + } + } + else + { + throw new ProcessMonitorException($"EnumProcessModulesEx for PID {_pid} failed. Error: {LastError}"); + } + } + + private void Open() + { + var access = (uint)(_readAccess ? 0x00000010 : 0) | 0x00000400; + _ptr = OpenProcess(access, false, _pid); + + if (_ptr == IntPtr.Zero) + { + var errno = LastError; + + if (errno == 5) + { + throw new ProcessAccessDeniedException($"Access denied to process {_pid}"); + } + else + { + throw new ProcessMonitorException($"WinAPI call 'OpenProcess for PID {_pid}' failed. Error: {errno}"); + } + } + } + + public WindowsProcess(int pid, bool readAccess = false) + { + _readAccess = readAccess; + _pid = pid; + Open(); + } + + public void Dispose() + { + if (_ptr != IntPtr.Zero) CloseHandle(_ptr); + } + + ~WindowsProcess() + { + Dispose(); + } + } +} diff --git a/webmr-api.csproj b/webmr-api.csproj index 53df987..f0a7c38 100644 --- a/webmr-api.csproj +++ b/webmr-api.csproj @@ -14,7 +14,7 @@ 1.0.0.0 1.0.0.0 - 0.1.1 + 0.1.2 OpenSource Process Monitoring Agent A service for detailed monitoring processes and memory regions.