SEBWIN-313: Started implementing blacklist monitoring.

This commit is contained in:
dbuechel 2019-10-08 16:11:19 +02:00
parent 8d0d1832a9
commit d3d98c7df7
10 changed files with 136 additions and 102 deletions

View file

@ -94,7 +94,7 @@ namespace SafeExamBrowser.Client
taskbar = BuildTaskbar();
terminationActivator = new TerminationActivator(ModuleLogger(nameof(TerminationActivator)));
var applicationMonitor = new ApplicationMonitor(ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory))));
var applicationMonitor = new ApplicationMonitor(FIVE_SECONDS, ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory))));
var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo);
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);
var hashAlgorithm = new HashAlgorithm();

View file

@ -14,7 +14,6 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Applications;
namespace SafeExamBrowser.Client.Operations
@ -110,18 +109,18 @@ namespace SafeExamBrowser.Client.Operations
private void StartMonitor()
{
if (Context.Settings.KioskMode != KioskMode.None)
{
//TODO: if (Context.Settings.KioskMode != KioskMode.None)
//{
applicationMonitor.Start();
}
//}
}
private void StopMonitor()
{
if (Context.Settings.KioskMode != KioskMode.None)
{
//TODO: if (Context.Settings.KioskMode != KioskMode.None)
//{
applicationMonitor.Stop();
}
//}
}
private OperationResult TryTerminate(IEnumerable<RunningApplication> runningApplications)

View file

@ -9,8 +9,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Threading;
using System.Timers;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Applications.Events;
@ -24,21 +23,24 @@ namespace SafeExamBrowser.Monitoring.Applications
private IntPtr activeWindow;
private IList<BlacklistApplication> blacklist;
private Guid? captureHookId;
private ManagementEventWatcher explorerWatcher;
private Guid? foregroundHookId;
private ILogger logger;
private INativeMethods nativeMethods;
private IList<IProcess> processes;
private IProcessFactory processFactory;
private Timer timer;
private IList<WhitelistApplication> whitelist;
public event ExplorerStartedEventHandler ExplorerStarted;
public ApplicationMonitor(ILogger logger, INativeMethods nativeMethods, IProcessFactory processFactory)
public ApplicationMonitor(int interval_ms, ILogger logger, INativeMethods nativeMethods, IProcessFactory processFactory)
{
this.blacklist = new List<BlacklistApplication>();
this.logger = logger;
this.nativeMethods = nativeMethods;
this.processes = new List<IProcess>();
this.processFactory = processFactory;
this.timer = new Timer(interval_ms);
this.whitelist = new List<WhitelistApplication>();
}
@ -59,17 +61,19 @@ namespace SafeExamBrowser.Monitoring.Applications
logger.Debug($"Initialized blacklist with {blacklist.Count} applications{(blacklist.Any() ? $": {string.Join(", ", blacklist.Select(a => a.ExecutableName))}" : ".")}");
logger.Debug($"Initialized whitelist with {whitelist.Count} applications{(whitelist.Any() ? $": {string.Join(", ", whitelist.Select(a => a.ExecutableName))}" : ".")}");
foreach (var process in processFactory.GetAllRunning())
processes = processFactory.GetAllRunning();
foreach (var process in processes)
{
foreach (var application in blacklist)
{
var isMatch = BelongsToApplication(process, application);
var isBlacklisted = BelongsToApplication(process, application);
if (isMatch && !application.AutoTerminate)
if (isBlacklisted && !application.AutoTerminate)
{
AddForTermination(application.ExecutableName, process, result);
}
else if (isMatch && application.AutoTerminate && !TryTerminate(process))
else if (isBlacklisted && application.AutoTerminate && !TryTerminate(process))
{
AddFailed(application.ExecutableName, process, result);
}
@ -86,13 +90,10 @@ namespace SafeExamBrowser.Monitoring.Applications
public void Start()
{
// TODO: Start monitoring blacklist...
// TODO: Remove WMI event and use timer mechanism!
explorerWatcher = new ManagementEventWatcher(@"\\.\root\CIMV2", GetQueryFor("explorer.exe"));
explorerWatcher.EventArrived += new EventArrivedEventHandler(ExplorerWatcher_EventArrived);
explorerWatcher.Start();
logger.Info("Started monitoring process 'explorer.exe'.");
timer.AutoReset = false;
timer.Elapsed += Timer_Elapsed;
timer.Start();
logger.Info("Started monitoring applications.");
captureHookId = nativeMethods.RegisterSystemCaptureStartEvent(SystemEvent_WindowChanged);
logger.Info($"Registered system capture start event with ID = {captureHookId}.");
@ -103,8 +104,9 @@ namespace SafeExamBrowser.Monitoring.Applications
public void Stop()
{
explorerWatcher?.Stop();
logger.Info("Stopped monitoring 'explorer.exe'.");
timer.Stop();
timer.Elapsed -= Timer_Elapsed;
logger.Info("Stopped monitoring applications.");
if (captureHookId.HasValue)
{
@ -142,7 +144,7 @@ namespace SafeExamBrowser.Monitoring.Applications
}
application.Processes.Add(process);
logger.Error($"Process '{process.Name}' belongs to application '{application.Name}' and could not be terminated automatically!");
logger.Error($"Process {process} belongs to application '{application.Name}' and could not be terminated automatically!");
}
private void AddForTermination(string name, IProcess process, InitializationResult result)
@ -156,7 +158,7 @@ namespace SafeExamBrowser.Monitoring.Applications
}
application.Processes.Add(process);
logger.Debug($"Process '{process.Name}' belongs to application '{application.Name}' and needs to be terminated.");
logger.Debug($"Process {process} belongs to application '{application.Name}' and needs to be terminated.");
}
private bool BelongsToApplication(IProcess process, BlacklistApplication application)
@ -231,63 +233,73 @@ namespace SafeExamBrowser.Monitoring.Applications
private bool TryTerminate(IProcess process)
{
const int MAX_ATTEMPTS = 5;
const int TIMEOUT = 100;
const int TIMEOUT = 500;
try
for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++)
{
if (process.TryClose(TIMEOUT))
{
break;
}
}
if (!process.HasTerminated)
{
for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++)
{
if (process.TryClose())
if (process.TryKill(TIMEOUT))
{
break;
}
else
{
Thread.Sleep(TIMEOUT);
}
}
if (!process.HasTerminated)
{
for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++)
{
if (process.TryKill())
{
break;
}
else
{
Thread.Sleep(TIMEOUT);
}
}
}
if (process.HasTerminated)
{
logger.Info($"Successfully terminated process '{process.Name}'.");
}
else
{
logger.Warn($"Failed to terminate process '{process.Name}'!");
}
}
catch (Exception e)
if (process.HasTerminated)
{
logger.Error($"An error occurred while attempting to terminate process '{process.Name}'!", e);
logger.Info($"Successfully terminated process {process}.");
}
else
{
logger.Warn($"Failed to terminate process {process}!");
}
return process.HasTerminated;
}
private void ExplorerWatcher_EventArrived(object sender, EventArrivedEventArgs e)
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
var eventName = e.NewEvent.ClassPath.ClassName;
var running = processFactory.GetAllRunning();
var started = running.Where(r => processes.All(p => p.Id != r.Id)).ToList();
var terminated = processes.Where(p => running.All(r => r.Id != p.Id)).ToList();
if (eventName == "__InstanceCreationEvent")
foreach (var process in started)
{
logger.Warn("A new instance of Windows explorer has been started!");
ExplorerStarted?.Invoke();
logger.Debug($"Process {process} has been started.");
processes.Add(process);
foreach (var application in blacklist)
{
if (BelongsToApplication(process, application))
{
logger.Warn($"Process {process} belongs to blacklisted application '{application.ExecutableName}'! Attempting termination...");
var success = TryTerminate(process);
if (!success)
{
// TODO: Invoke event -> Show lock screen!
}
}
}
}
foreach (var process in terminated)
{
logger.Debug($"Process {process} has been terminated.");
processes.Remove(process);
}
timer.Start();
}
private void SystemEvent_WindowChanged(IntPtr window)
@ -299,15 +311,5 @@ namespace SafeExamBrowser.Monitoring.Applications
Check(window);
}
}
private string GetQueryFor(string processName)
{
return $@"
SELECT *
FROM __InstanceOperationEvent
WITHIN 2
WHERE TargetInstance ISA 'Win32_Process'
AND TargetInstance.Name = '{processName}'";
}
}
}

View file

@ -50,7 +50,6 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Windows.Forms" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="WindowsBase" />

View file

@ -172,7 +172,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Once);
proxy.Verify(p => p.Disconnect(), Times.Once);
process.Verify(p => p.TryKill(), Times.Never);
process.Verify(p => p.TryKill(default(int)), Times.Never);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);
@ -181,12 +181,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestMethod]
public void Revert_MustKillClientIfStoppingFailed()
{
process.Setup(p => p.TryKill()).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true));
process.Setup(p => p.TryKill(default(int))).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true));
PerformNormally();
sut.Revert();
process.Verify(p => p.TryKill(), Times.AtLeastOnce);
process.Verify(p => p.TryKill(default(int)), Times.AtLeastOnce);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);
@ -198,7 +198,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
PerformNormally();
sut.Revert();
process.Verify(p => p.TryKill(), Times.Exactly(5));
process.Verify(p => p.TryKill(default(int)), Times.Exactly(5));
Assert.IsNotNull(sessionContext.ClientProcess);
Assert.IsNotNull(sessionContext.ClientProxy);
@ -213,7 +213,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Never);
proxy.Verify(p => p.Disconnect(), Times.Never);
process.Verify(p => p.TryKill(), Times.Never);
process.Verify(p => p.TryKill(default(int)), Times.Never);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);

View file

@ -81,7 +81,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Once);
proxy.Verify(p => p.Disconnect(), Times.Once);
process.Verify(p => p.TryKill(), Times.Never);
process.Verify(p => p.TryKill(default(int)), Times.Never);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);

View file

@ -41,13 +41,16 @@ namespace SafeExamBrowser.WindowsApi.Contracts
event ProcessTerminatedEventHandler Terminated;
/// <summary>
/// Attempts to gracefully terminate the process by closing its main window. This will only work for interactive processes which have a main window.
/// Attempts to gracefully terminate the process by closing its main window. This will only work for interactive processes which have a main
/// window. Optionally waits the specified amount of time for the process to terminate. Returns <c>true</c> if the process has terminated,
/// otherwise <c>false</c>.
/// </summary>
bool TryClose();
bool TryClose(int timeout_ms = 0);
/// <summary>
/// Attempts to immediately kill the process.
/// Attempts to immediately kill the process. Optionally waits the specified amount of time for the process to terminate. Returns <c>true</c>
/// if the process has terminated, otherwise <c>false</c>.
/// </summary>
bool TryKill();
bool TryKill(int timeout_ms = 0);
}
}

View file

@ -24,7 +24,7 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// <summary>
/// Retrieves all currently running processes.
/// </summary>
IEnumerable<IProcess> GetAllRunning();
IList<IProcess> GetAllRunning();
/// <summary>
/// Starts a new process with the given command-line arguments.

View file

@ -62,18 +62,25 @@ namespace SafeExamBrowser.WindowsApi
this.originalNameInitialized = true;
}
public bool TryClose()
public bool TryClose(int timeout_ms = 0)
{
try
{
logger.Debug("Attempting to close process...");
process.Refresh();
if (!process.HasExited)
var success = process.CloseMainWindow();
if (success)
{
process.CloseMainWindow();
logger.Debug("Successfully sent close message to main window.");
}
else
{
logger.Warn("Failed to send close message to main window!");
}
return process.HasExited;
return success && WaitForTermination(timeout_ms);
}
catch (Exception e)
{
@ -83,18 +90,16 @@ namespace SafeExamBrowser.WindowsApi
return false;
}
public bool TryKill()
public bool TryKill(int timeout_ms = 0)
{
try
{
logger.Debug("Attempting to kill process...");
process.Refresh();
process.Kill();
if (!process.HasExited)
{
process.Kill();
}
return process.HasExited;
return WaitForTermination(timeout_ms);
}
catch (Exception e)
{
@ -104,6 +109,11 @@ namespace SafeExamBrowser.WindowsApi
return false;
}
public override string ToString()
{
return $"'{Name}' ({Id})";
}
private bool IsTerminated()
{
try
@ -127,6 +137,7 @@ namespace SafeExamBrowser.WindowsApi
eventInitialized = true;
process.Exited += Process_Exited;
process.EnableRaisingEvents = true;
logger.Debug("Initialized termination event.");
}
}
@ -165,9 +176,26 @@ namespace SafeExamBrowser.WindowsApi
return originalName;
}
private bool WaitForTermination(int timeout_ms)
{
var terminated = process.WaitForExit(timeout_ms);
if (terminated)
{
logger.Debug($"Process has terminated within {timeout_ms}ms.");
}
else
{
logger.Warn($"Process failed to terminate within {timeout_ms}ms!");
}
return terminated;
}
private void Process_Exited(object sender, EventArgs e)
{
TerminatedEvent?.Invoke(process.ExitCode);
logger.Debug("Process has terminated.");
}
}
}

View file

@ -32,17 +32,20 @@ namespace SafeExamBrowser.WindowsApi
this.logger = logger;
}
public IEnumerable<IProcess> GetAllRunning()
public IList<IProcess> GetAllRunning()
{
var processes = System.Diagnostics.Process.GetProcesses();
var processes = new List<IProcess>();
var running = System.Diagnostics.Process.GetProcesses();
var originalNames = LoadOriginalNames();
foreach (var process in processes)
foreach (var process in running)
{
var originalName = originalNames.FirstOrDefault(n => n.processId == process.Id).originalName;
yield return new Process(process, originalName, LoggerFor(process));
processes.Add(new Process(process, originalName, LoggerFor(process)));
}
return processes;
}
public IProcess StartNew(string path, params string[] args)