SEBWIN-313: Started implementing application blacklist mechanism.

This commit is contained in:
dbuechel 2019-10-04 16:36:12 +02:00
parent 51570ecc91
commit b72c37273e
23 changed files with 620 additions and 128 deletions

View file

@ -8,9 +8,11 @@
using System;
using System.IO;
using System.Linq;
using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Client.Operations.Events;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
@ -343,7 +345,15 @@ namespace SafeExamBrowser.Client
private void Operations_ActionRequired(ActionRequiredEventArgs args)
{
// TODO
switch (args)
{
case ApplicationTerminationEventArgs a:
AskForAutomaticApplicationTermination(a);
break;
case ApplicationTerminationFailedEventArgs a:
InformAboutFailedApplicationTermination(a);
break;
}
}
private void Operations_ProgressChanged(ProgressChangedEventArgs args)
@ -401,6 +411,27 @@ namespace SafeExamBrowser.Client
terminationActivator.Resume();
}
private void AskForAutomaticApplicationTermination(ApplicationTerminationEventArgs args)
{
var nl = Environment.NewLine;
var applicationList = string.Join(Environment.NewLine, args.RunningApplications.Select(a => a.Name));
var warning = text.Get(TextKey.MessageBox_ApplicationAutoTerminationDataLossWarning);
var message = $"{text.Get(TextKey.MessageBox_ApplicationAutoTerminationQuestion)}{nl}{nl}{warning}{nl}{nl}{applicationList}";
var title = text.Get(TextKey.MessageBox_ApplicationAutoTerminationQuestionTitle);
var result = messageBox.Show(message, title, MessageBoxAction.YesNo, MessageBoxIcon.Question, parent: splashScreen);
args.TerminateProcesses = result == MessageBoxResult.Yes;
}
private void InformAboutFailedApplicationTermination(ApplicationTerminationFailedEventArgs args)
{
var applicationList = string.Join(Environment.NewLine, args.Applications.Select(a => a.Name));
var message = $"{text.Get(TextKey.MessageBox_ApplicationTerminationFailure)}{Environment.NewLine}{Environment.NewLine}{applicationList}";
var title = text.Get(TextKey.MessageBox_ApplicationTerminationFailureTitle);
messageBox.Show(message, title, icon: MessageBoxIcon.Error, parent: splashScreen);
}
private bool TryInitiateShutdown()
{
var hasQuitPassword = !String.IsNullOrEmpty(Settings.QuitPasswordHash);

View file

@ -29,7 +29,6 @@ using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Applications;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Display;
using SafeExamBrowser.Monitoring.Keyboard;
using SafeExamBrowser.Monitoring.Mouse;
@ -63,7 +62,6 @@ namespace SafeExamBrowser.Client
private UserInterfaceMode uiMode;
private IActionCenter actionCenter;
private IApplicationMonitor applicationMonitor;
private ILogger logger;
private IMessageBox messageBox;
private INativeMethods nativeMethods;
@ -89,16 +87,16 @@ namespace SafeExamBrowser.Client
InitializeText();
actionCenter = BuildActionCenter();
applicationMonitor = new ApplicationMonitor(new ModuleLogger(logger, nameof(ApplicationMonitor)), nativeMethods);
context = new ClientContext();
messageBox = BuildMessageBox();
uiFactory = BuildUserInterfaceFactory();
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy)), Interlocutor.Client);
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), ModuleLogger(nameof(RuntimeProxy)), Interlocutor.Client);
taskbar = BuildTaskbar();
terminationActivator = new TerminationActivator(new ModuleLogger(logger, nameof(TerminationActivator)));
terminationActivator = new TerminationActivator(ModuleLogger(nameof(TerminationActivator)));
var displayMonitor = new DisplayMonitor(new ModuleLogger(logger, nameof(DisplayMonitor)), nativeMethods, systemInfo);
var explorerShell = new ExplorerShell(new ModuleLogger(logger, nameof(ExplorerShell)), nativeMethods);
var applicationMonitor = new ApplicationMonitor(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();
var operations = new Queue<IOperation>();
@ -195,7 +193,7 @@ namespace SafeExamBrowser.Client
private IOperation BuildBrowserOperation()
{
var moduleLogger = new ModuleLogger(logger, nameof(BrowserApplication));
var moduleLogger = ModuleLogger(nameof(BrowserApplication));
var browser = new BrowserApplication(context.AppConfig, context.Settings.Browser, messageBox, moduleLogger, text, uiFactory);
var browserInfo = new BrowserApplicationInfo();
var operation = new BrowserOperation(actionCenter, context, logger, taskbar, uiFactory);
@ -209,7 +207,7 @@ namespace SafeExamBrowser.Client
{
var processId = Process.GetCurrentProcess().Id;
var factory = new HostObjectFactory();
var clientHost = new ClientHost(context.AppConfig.ClientAddress, factory, new ModuleLogger(logger, nameof(ClientHost)), processId, FIVE_SECONDS);
var clientHost = new ClientHost(context.AppConfig.ClientAddress, factory, ModuleLogger(nameof(ClientHost)), processId, FIVE_SECONDS);
var operation = new CommunicationHostOperation(clientHost, logger);
context.ClientHost = clientHost;
@ -220,7 +218,7 @@ namespace SafeExamBrowser.Client
private IOperation BuildKeyboardInterceptorOperation()
{
var keyboardInterceptor = new KeyboardInterceptor(new ModuleLogger(logger, nameof(KeyboardInterceptor)), nativeMethods, context.Settings.Keyboard);
var keyboardInterceptor = new KeyboardInterceptor(ModuleLogger(nameof(KeyboardInterceptor)), nativeMethods, context.Settings.Keyboard);
var operation = new KeyboardInterceptorOperation(context, keyboardInterceptor, logger);
return operation;
@ -228,7 +226,7 @@ namespace SafeExamBrowser.Client
private IOperation BuildMouseInterceptorOperation()
{
var mouseInterceptor = new MouseInterceptor(new ModuleLogger(logger, nameof(MouseInterceptor)), nativeMethods, context.Settings.Mouse);
var mouseInterceptor = new MouseInterceptor(ModuleLogger(nameof(MouseInterceptor)), nativeMethods, context.Settings.Mouse);
var operation = new MouseInterceptorOperation(context, logger, mouseInterceptor);
return operation;
@ -238,16 +236,16 @@ namespace SafeExamBrowser.Client
{
var aboutInfo = new AboutNotificationInfo(text);
var aboutController = new AboutNotificationController(context.AppConfig, uiFactory);
var audio = new Audio(context.Settings.Audio, new ModuleLogger(logger, nameof(Audio)));
var keyboard = new Keyboard(new ModuleLogger(logger, nameof(Keyboard)));
var audio = new Audio(context.Settings.Audio, ModuleLogger(nameof(Audio)));
var keyboard = new Keyboard(ModuleLogger(nameof(Keyboard)));
var logInfo = new LogNotificationInfo(text);
var logController = new LogNotificationController(logger, uiFactory);
var powerSupply = new PowerSupply(new ModuleLogger(logger, nameof(PowerSupply)));
var wirelessAdapter = new WirelessAdapter(new ModuleLogger(logger, nameof(WirelessAdapter)));
var powerSupply = new PowerSupply(ModuleLogger(nameof(PowerSupply)));
var wirelessAdapter = new WirelessAdapter(ModuleLogger(nameof(WirelessAdapter)));
var activators = new IActionCenterActivator[]
{
new KeyboardActivator(new ModuleLogger(logger, nameof(KeyboardActivator))),
new TouchActivator(new ModuleLogger(logger, nameof(TouchActivator)))
new KeyboardActivator(ModuleLogger(nameof(KeyboardActivator))),
new TouchActivator(ModuleLogger(nameof(TouchActivator)))
};
var operation = new ShellOperation(
actionCenter,
@ -298,9 +296,9 @@ namespace SafeExamBrowser.Client
switch (uiMode)
{
case UserInterfaceMode.Mobile:
return new Mobile.Taskbar(new ModuleLogger(logger, nameof(Mobile.Taskbar)));
return new Mobile.Taskbar(ModuleLogger(nameof(Mobile.Taskbar)));
default:
return new Desktop.Taskbar(new ModuleLogger(logger, nameof(Desktop.Taskbar)));
return new Desktop.Taskbar(ModuleLogger(nameof(Desktop.Taskbar)));
}
}
@ -319,5 +317,10 @@ namespace SafeExamBrowser.Client
{
ClientController.UpdateAppConfig();
}
private IModuleLogger ModuleLogger(string moduleInfo)
{
return new ModuleLogger(logger, moduleInfo);
}
}
}

View file

@ -36,7 +36,7 @@ namespace SafeExamBrowser.Client.Operations
public override OperationResult Perform()
{
logger.Info("Initializing applications...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeProcessMonitoring);
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeApplications);
var result = InitializeApplications();
@ -51,9 +51,9 @@ namespace SafeExamBrowser.Client.Operations
public override OperationResult Revert()
{
logger.Info("Finalizing applications...");
StatusChanged?.Invoke(TextKey.OperationStatus_StopProcessMonitoring);
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeApplications);
TerminateApplications();
FinalizeApplications();
StopMonitor();
return OperationResult.Success;
@ -64,25 +64,48 @@ namespace SafeExamBrowser.Client.Operations
var initialization = applicationMonitor.Initialize(Context.Settings.Applications);
var result = OperationResult.Success;
if (initialization.RunningApplications.Any())
if (initialization.FailedAutoTerminations.Any())
{
result = HandleAutoTerminationFailure(initialization.FailedAutoTerminations);
}
else if (initialization.RunningApplications.Any())
{
result = TryTerminate(initialization.RunningApplications);
}
if (result == OperationResult.Success)
{
foreach (var application in Context.Settings.Applications.Whitelist)
{
Create(application);
}
CreateApplications();
}
return result;
}
private void CreateApplications()
{
foreach (var application in Context.Settings.Applications.Whitelist)
{
Create(application);
}
}
private void Create(WhitelistApplication application)
{
// TODO: Use IApplicationFactory to create new application according to configuration, load into Context.Applications
// StatusChanged?.Invoke();
}
private void FinalizeApplications()
{
}
private OperationResult HandleAutoTerminationFailure(IList<RunningApplication> applications)
{
logger.Error($"{applications.Count} application(s) could not be automatically terminated: {string.Join(", ", applications.Select(a => a.Name))}");
ActionRequired?.Invoke(new ApplicationTerminationFailedEventArgs(applications));
return OperationResult.Failed;
}
private void StartMonitor()
@ -101,35 +124,42 @@ namespace SafeExamBrowser.Client.Operations
}
}
private void TerminateApplications()
private OperationResult TryTerminate(IEnumerable<RunningApplication> runningApplications)
{
}
private OperationResult TryTerminate(IEnumerable<RunningApplicationInfo> runningApplications)
{
var args = new ProcessTerminationEventArgs();
var args = new ApplicationTerminationEventArgs(runningApplications);
var failed = new List<RunningApplication>();
var result = OperationResult.Success;
ActionRequired?.Invoke(args);
if (args.TerminateProcesses)
{
// TODO: Terminate all processes of all running applications
foreach (var application in runningApplications)
{
var success = applicationMonitor.TryTerminate(application);
//foreach (var application in runningApplications)
//{
// foreach (var process in application.Processes)
// {
// process.Kill();
// }
//}
if (success)
{
logger.Info($"Successfully terminated application '{application.Name}'.");
}
else
{
result = OperationResult.Failed;
failed.Add(application);
logger.Error($"Failed to automatically terminate application '{application.Name}'!");
}
}
}
else
{
result = OperationResult.Aborted;
}
if (failed.Any())
{
ActionRequired?.Invoke(new ApplicationTerminationFailedEventArgs(failed));
}
return result;
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Monitoring.Contracts.Applications;
namespace SafeExamBrowser.Client.Operations.Events
{
internal class ApplicationTerminationEventArgs : ActionRequiredEventArgs
{
internal IEnumerable<RunningApplication> RunningApplications { get; }
internal bool TerminateProcesses { get; set; }
internal ApplicationTerminationEventArgs(IEnumerable<RunningApplication> runningApplications)
{
RunningApplications = runningApplications;
}
}
}

View file

@ -6,12 +6,19 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Monitoring.Contracts.Applications;
namespace SafeExamBrowser.Client.Operations.Events
{
internal class ProcessTerminationEventArgs : ActionRequiredEventArgs
internal class ApplicationTerminationFailedEventArgs : ActionRequiredEventArgs
{
public bool TerminateProcesses { get; set; }
internal IEnumerable<RunningApplication> Applications { get; }
internal ApplicationTerminationFailedEventArgs(IEnumerable<RunningApplication> applications)
{
Applications = applications;
}
}
}

View file

@ -76,7 +76,8 @@
<Compile Include="Operations\ClientHostDisconnectionOperation.cs" />
<Compile Include="Operations\ClientOperation.cs" />
<Compile Include="Operations\ConfigurationOperation.cs" />
<Compile Include="Operations\Events\ProcessTerminationEventArgs.cs" />
<Compile Include="Operations\Events\ApplicationTerminationEventArgs.cs" />
<Compile Include="Operations\Events\ApplicationTerminationFailedEventArgs.cs" />
<Compile Include="Operations\RuntimeConnectionOperation.cs" />
<Compile Include="Communication\ClientHost.cs" />
<Compile Include="CompositionRoot.cs" />

View file

@ -22,8 +22,13 @@ namespace SafeExamBrowser.I18n.Contracts
BrowserWindow_ZoomMenuItem,
Build,
LogWindow_Title,
MessageBox_ApplicationAutoTerminationDataLossWarning,
MessageBox_ApplicationAutoTerminationQuestion,
MessageBox_ApplicationAutoTerminationQuestionTitle,
MessageBox_ApplicationError,
MessageBox_ApplicationErrorTitle,
MessageBox_ApplicationTerminationFailure,
MessageBox_ApplicationTerminationFailureTitle,
MessageBox_BrowserNavigationBlocked,
MessageBox_BrowserNavigationBlockedTitle,
MessageBox_CancelButton,
@ -70,15 +75,15 @@ namespace SafeExamBrowser.I18n.Contracts
Notification_LogTooltip,
OperationStatus_CloseRuntimeConnection,
OperationStatus_EmptyClipboard,
OperationStatus_FinalizeApplications,
OperationStatus_FinalizeServiceSession,
OperationStatus_InitializeApplications,
OperationStatus_InitializeBrowser,
OperationStatus_InitializeConfiguration,
OperationStatus_InitializeKioskMode,
OperationStatus_InitializeProcessMonitoring,
OperationStatus_InitializeRuntimeConnection,
OperationStatus_InitializeServiceSession,
OperationStatus_InitializeShell,
OperationStatus_InitializeWindowMonitoring,
OperationStatus_InitializeWorkingArea,
OperationStatus_RestartCommunicationHost,
OperationStatus_RestoreWorkingArea,
@ -92,8 +97,6 @@ namespace SafeExamBrowser.I18n.Contracts
OperationStatus_StopCommunicationHost,
OperationStatus_StopKeyboardInterception,
OperationStatus_StopMouseInterception,
OperationStatus_StopProcessMonitoring,
OperationStatus_StopWindowMonitoring,
OperationStatus_TerminateBrowser,
OperationStatus_TerminateShell,
OperationStatus_WaitExplorerStartup,

View file

@ -24,12 +24,27 @@
<Entry key="LogWindow_Title">
Application Log
</Entry>
<Entry key="MessageBox_ApplicationAutoTerminationQuestion">
The applications listed below need to be terminated before a new session can be started. Would you like to automatically terminate them now?
</Entry>
<Entry key="MessageBox_ApplicationAutoTerminationQuestionTitle">
Running Applications Detected
</Entry>
<Entry key="MessageBox_ApplicationAutoTerminationDataLossWarning">
IMPORTANT: Any unsaved application data might be lost!
</Entry>
<Entry key="MessageBox_ApplicationError">
An unrecoverable error has occurred! Please consult the application log for more information. The application will now shut down...
</Entry>
<Entry key="MessageBox_ApplicationErrorTitle">
Application Error
</Entry>
<Entry key="MessageBox_ApplicationTerminationFailure">
The applications listed below could not be terminated! Please terminate them manually and try again...
</Entry>
<Entry key="MessageBox_ApplicationTerminationFailureTitle">
Automatic Termination Failed
</Entry>
<Entry key="MessageBox_BrowserNavigationBlocked">
Access to "%%URL%%" is not allowed according to the application configuration.
</Entry>
@ -168,9 +183,15 @@
<Entry key="OperationStatus_EmptyClipboard">
Emptying clipboard
</Entry>
<Entry key="OperationStatus_FinalizeApplications">
Finalizing applications
</Entry>
<Entry key="OperationStatus_FinalizeServiceSession">
Finalizing service session
</Entry>
<Entry key="OperationStatus_InitializeApplications">
Initializing applications
</Entry>
<Entry key="OperationStatus_InitializeBrowser">
Initializing browser
</Entry>
@ -180,9 +201,6 @@
<Entry key="OperationStatus_InitializeKioskMode">
Initializing kiosk mode
</Entry>
<Entry key="OperationStatus_InitializeProcessMonitoring">
Initializing process monitoring
</Entry>
<Entry key="OperationStatus_InitializeRuntimeConnection">
Initializing runtime connection
</Entry>
@ -195,9 +213,6 @@
<Entry key="OperationStatus_InitializeShell">
Initializing user interface
</Entry>
<Entry key="OperationStatus_InitializeWindowMonitoring">
Initializing window monitoring
</Entry>
<Entry key="OperationStatus_InitializeWorkingArea">
Initializing working area
</Entry>
@ -234,12 +249,6 @@
<Entry key="OperationStatus_StopMouseInterception">
Stopping mouse interception
</Entry>
<Entry key="OperationStatus_StopProcessMonitoring">
Stopping process monitoring
</Entry>
<Entry key="OperationStatus_StopWindowMonitoring">
Stopping window monitoring
</Entry>
<Entry key="OperationStatus_TerminateBrowser">
Terminating browser
</Entry>

View file

@ -35,5 +35,10 @@ namespace SafeExamBrowser.Monitoring.Contracts.Applications
/// Stops the application monitoring.
/// </summary>
void Stop();
/// <summary>
/// Attempts to terminate all processes of the specified application. Returns <c>true</c> if successful, otherwise <c>false</c>.
/// </summary>
bool TryTerminate(RunningApplication application);
}
}

View file

@ -15,14 +15,20 @@ namespace SafeExamBrowser.Monitoring.Contracts.Applications
/// </summary>
public class InitializationResult
{
/// <summary>
/// A list of currently running applications which could not be automatically terminated.
/// </summary>
public IList<RunningApplication> FailedAutoTerminations { get; }
/// <summary>
/// A list of currently running applications which need to be terminated.
/// </summary>
public IEnumerable<RunningApplicationInfo> RunningApplications { get; }
public IList<RunningApplication> RunningApplications { get; }
public InitializationResult()
{
RunningApplications = new List<RunningApplicationInfo>();
FailedAutoTerminations = new List<RunningApplication>();
RunningApplications = new List<RunningApplication>();
}
}
}

View file

@ -14,16 +14,22 @@ namespace SafeExamBrowser.Monitoring.Contracts.Applications
/// <summary>
/// Provides information about a running application.
/// </summary>
public class RunningApplicationInfo
public class RunningApplication
{
/// <summary>
/// The name of the application.
/// </summary>
public string Name { get; set; }
public string Name { get; }
/// <summary>
/// A list of processes which belong to the application.
/// </summary>
public IEnumerable<IProcess> Processes { get; set; }
public IList<IProcess> Processes { get; }
public RunningApplication(string name)
{
Name = name;
Processes = new List<IProcess>();
}
}
}

View file

@ -53,7 +53,7 @@
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Compile Include="Applications\RunningApplicationInfo.cs" />
<Compile Include="Applications\RunningApplication.cs" />
<Compile Include="Display\Events\DisplayChangedEventHandler.cs" />
<Compile Include="Applications\Events\ExplorerStartedEventHandler.cs" />
<Compile Include="Display\IDisplayMonitor.cs" />

View file

@ -7,8 +7,10 @@
*/
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Threading;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Applications.Events;
@ -20,34 +22,73 @@ namespace SafeExamBrowser.Monitoring.Applications
public class ApplicationMonitor : IApplicationMonitor
{
private IntPtr activeWindow;
private IList<BlacklistApplication> blacklist;
private Guid? captureHookId;
private ManagementEventWatcher explorerWatcher;
private Guid? foregroundHookId;
private ILogger logger;
private INativeMethods nativeMethods;
private ManagementEventWatcher explorerWatcher;
private IProcessFactory processFactory;
private IList<WhitelistApplication> whitelist;
public event ExplorerStartedEventHandler ExplorerStarted;
public ApplicationMonitor(ILogger logger, INativeMethods nativeMethods)
public ApplicationMonitor(ILogger logger, INativeMethods nativeMethods, IProcessFactory processFactory)
{
this.blacklist = new List<BlacklistApplication>();
this.logger = logger;
this.nativeMethods = nativeMethods;
this.processFactory = processFactory;
this.whitelist = new List<WhitelistApplication>();
}
public InitializationResult Initialize(ApplicationSettings settings)
{
// TODO
// Initialize blacklist
// Initialize whitelist
// Check for running processes
var result = new InitializationResult();
return new InitializationResult();
foreach (var application in settings.Blacklist)
{
blacklist.Add(application);
}
foreach (var application in settings.Whitelist)
{
whitelist.Add(application);
}
logger.Debug($"Initialized blacklist with {blacklist.Count} applications: {string.Join(", ", blacklist.Select(a => a.ExecutableName))}");
logger.Debug($"Initialized whitelist with {whitelist.Count} applications: {string.Join(", ", whitelist.Select(a => a.ExecutableName))}");
foreach (var process in processFactory.GetAllRunning())
{
foreach (var application in blacklist)
{
var isMatch = BelongsToApplication(process, application);
if (isMatch && !application.AutoTerminate)
{
AddForTermination(application.ExecutableName, process, result);
}
else if (isMatch && application.AutoTerminate && !TryTerminate(process))
{
AddFailed(application.ExecutableName, process, result);
}
}
foreach (var application in whitelist)
{
// TODO: Check if application is running, auto-terminate or add to result.
}
}
return result;
}
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();
@ -78,9 +119,52 @@ namespace SafeExamBrowser.Monitoring.Applications
}
}
public bool Terminate(int processId)
public bool TryTerminate(RunningApplication application)
{
return false;
var success = true;
foreach (var process in application.Processes)
{
success &= TryTerminate(process);
}
return success;
}
private void AddFailed(string name, IProcess process, InitializationResult result)
{
var application = result.FailedAutoTerminations.FirstOrDefault(a => a.Name == name);
if (application == default(RunningApplication))
{
application = new RunningApplication(name);
result.FailedAutoTerminations.Add(application);
}
application.Processes.Add(process);
logger.Error($"Process '{process.Name}' belongs to application '{application.Name}' and could not be terminated automatically!");
}
private void AddForTermination(string name, IProcess process, InitializationResult result)
{
var application = result.RunningApplications.FirstOrDefault(a => a.Name == name);
if (application == default(RunningApplication))
{
application = new RunningApplication(name);
result.RunningApplications.Add(application);
}
application.Processes.Add(process);
logger.Debug($"Process '{process.Name}' belongs to application '{application.Name}' and needs to be terminated.");
}
private bool BelongsToApplication(IProcess process, BlacklistApplication application)
{
var sameName = process.Name.Equals(application.ExecutableName, StringComparison.OrdinalIgnoreCase);
var sameOriginalName = process.OriginalName?.Equals(application.ExecutableOriginalName, StringComparison.OrdinalIgnoreCase) == true;
return sameName || sameOriginalName;
}
private void Check(IntPtr window)
@ -109,19 +193,20 @@ namespace SafeExamBrowser.Monitoring.Applications
private bool IsAllowed(IntPtr window)
{
var processId = nativeMethods.GetProcessIdFor(window);
var process = Process.GetProcessById(Convert.ToInt32(processId));
// TODO: Allow only if in whitelist!
//var process = processFactory.GetById(Convert.ToInt32(processId));
if (process != null)
{
var allowed = process.ProcessName == "SafeExamBrowser" || process.ProcessName == "SafeExamBrowser.Client";
//if (process != null)
//{
// var allowed = process.Name == "SafeExamBrowser" || process.Name == "SafeExamBrowser.Client";
if (!allowed)
{
logger.Warn($"Window with handle = {window} belongs to not allowed process '{process.ProcessName}'!");
}
// if (!allowed)
// {
// logger.Warn($"Window with handle = {window} belongs to not allowed process '{process.Name}'!");
// }
return allowed;
}
// return allowed;
//}
return true;
}
@ -143,6 +228,57 @@ namespace SafeExamBrowser.Monitoring.Applications
return success;
}
private bool TryTerminate(IProcess process)
{
const int MAX_ATTEMPTS = 5;
const int TIMEOUT = 100;
try
{
for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++)
{
if (process.TryClose())
{
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)
{
logger.Error($"An error occurred while attempting to terminate process '{process.Name}'!", e);
}
return process.HasTerminated;
}
private void ExplorerWatcher_EventArrived(object sender, EventArrivedEventArgs e)
{
var eventName = e.NewEvent.ClassPath.ClassName;

View file

@ -186,7 +186,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Once);
proxy.Verify(p => p.Disconnect(), Times.Once);
process.Verify(p => p.Kill(), Times.Never);
process.Verify(p => p.TryKill(), Times.Never);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);
@ -195,12 +195,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestMethod]
public void Revert_MustKillClientIfStoppingFailed()
{
process.Setup(p => p.Kill()).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true));
process.Setup(p => p.TryKill()).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true));
PerformNormally();
sut.Revert();
process.Verify(p => p.Kill(), Times.AtLeastOnce);
process.Verify(p => p.TryKill(), Times.AtLeastOnce);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);
@ -212,7 +212,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
PerformNormally();
sut.Revert();
process.Verify(p => p.Kill(), Times.Exactly(5));
process.Verify(p => p.TryKill(), Times.Exactly(5));
Assert.IsNotNull(sessionContext.ClientProcess);
Assert.IsNotNull(sessionContext.ClientProxy);
@ -227,7 +227,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Never);
proxy.Verify(p => p.Disconnect(), Times.Never);
process.Verify(p => p.Kill(), Times.Never);
process.Verify(p => p.TryKill(), 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.Kill(), Times.Never);
process.Verify(p => p.TryKill(), Times.Never);
Assert.IsNull(sessionContext.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy);

View file

@ -240,32 +240,30 @@ namespace SafeExamBrowser.Runtime.Operations
return success;
}
private bool TryKillClient(int attempt = 0)
private bool TryKillClient()
{
const int MAX_ATTEMPTS = 5;
if (attempt == MAX_ATTEMPTS)
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)
{
logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
logger.Info($"Attempt {attempt}/{MAX_ATTEMPTS} to kill client process with ID = {ClientProcess.Id}.");
return false;
if (ClientProcess.TryKill())
{
break;
}
}
logger.Info($"Killing client process with ID = {ClientProcess.Id}.");
ClientProcess.Kill();
if (ClientProcess.HasTerminated)
{
logger.Info("Client process has terminated.");
return true;
}
else
{
logger.Warn("Failed to kill client process. Trying again...");
return TryKillClient(++attempt);
logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
}
return ClientProcess.HasTerminated;
}
}
}

View file

@ -12,18 +12,18 @@ using System.Collections.Generic;
namespace SafeExamBrowser.Settings.Applications
{
/// <summary>
/// TODO
/// Defines all settings for third-party applications and application monitoring.
/// </summary>
[Serializable]
public class ApplicationSettings
{
/// <summary>
///
/// All applications which are not allowed to run during a session.
/// </summary>
public IList<BlacklistApplication> Blacklist { get; set; }
/// <summary>
///
/// All applications which are allowed to run during a session.
/// </summary>
public IList<WhitelistApplication> Whitelist { get; set; }

View file

@ -11,10 +11,14 @@ using System;
namespace SafeExamBrowser.Settings.Applications
{
/// <summary>
/// TODO
/// Defines an application which is whitelisted, i.e. allowed to run during a session.
/// </summary>
[Serializable]
public class WhitelistApplication
{
/// <summary>
/// The name of the main executable of the application.
/// </summary>
public string ExecutableName { get; set; }
}
}

View file

@ -25,14 +25,29 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// </summary>
int Id { get; }
/// <summary>
/// The file name of the process executable.
/// </summary>
string Name { get; }
/// <summary>
/// The original file name of the process executable, if available.
/// </summary>
string OriginalName { get; }
/// <summary>
/// Event fired when the process has terminated.
/// </summary>
event ProcessTerminatedEventHandler Terminated;
/// <summary>
/// Immediately terminates the process.
/// Attempts to gracefully terminate the process by closing its main window. This will only work for interactive processes which have a main window.
/// </summary>
void Kill();
bool TryClose();
/// <summary>
/// Attempts to immediately kill the process.
/// </summary>
bool TryKill();
}
}

View file

@ -6,6 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
namespace SafeExamBrowser.WindowsApi.Contracts
{
/// <summary>
@ -19,10 +21,20 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// </summary>
IDesktop StartupDesktop { set; }
/// <summary>
/// Retrieves all currently running processes.
/// </summary>
IEnumerable<IProcess> GetAllRunning();
/// <summary>
/// Starts a new process with the given command-line arguments.
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">If the process could not be started.</exception>
IProcess StartNew(string path, params string[] args);
/// <summary>
/// Attempts to retrieve a process by its identifier. Returns <c>true</c> if a process was found, otherwise <c>false</c>.
/// </summary>
bool TryGetById(int id, out IProcess process);
}
}

View file

@ -6,6 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
@ -13,9 +19,12 @@ namespace SafeExamBrowser.WindowsApi
{
internal class Process : IProcess
{
private bool eventInitialized, originalNameInitialized;
private ILogger logger;
private string originalName;
private System.Diagnostics.Process process;
public event ProcessTerminatedEventHandler Terminated;
private event ProcessTerminatedEventHandler TerminatedEvent;
public int Id
{
@ -24,29 +33,141 @@ namespace SafeExamBrowser.WindowsApi
public bool HasTerminated
{
get { process.Refresh(); return process.HasExited; }
get { return IsTerminated(); }
}
public Process(int id)
public string Name { get; }
public string OriginalName
{
process = System.Diagnostics.Process.GetProcessById(id);
process.Exited += Process_Exited;
process.EnableRaisingEvents = true;
get { return originalNameInitialized ? originalName : InitializeOriginalName(); }
}
public void Kill()
public event ProcessTerminatedEventHandler Terminated
{
process.Refresh();
add { TerminatedEvent += value; InitializeEvent(); }
remove { TerminatedEvent -= value; }
}
if (!process.HasExited)
internal Process(System.Diagnostics.Process process, ILogger logger)
{
this.Name = process.ProcessName;
this.process = process;
this.logger = logger;
}
internal Process(System.Diagnostics.Process process, string originalName, ILogger logger) : this(process, logger)
{
this.originalName = originalName;
this.originalNameInitialized = true;
}
public bool TryClose()
{
try
{
process.Kill();
process.Refresh();
if (!process.HasExited)
{
process.CloseMainWindow();
}
return process.HasExited;
}
catch (Exception e)
{
logger.Error("Failed to close main window!", e);
}
return false;
}
public bool TryKill()
{
try
{
process.Refresh();
if (!process.HasExited)
{
process.Kill();
}
return process.HasExited;
}
catch (Exception e)
{
logger.Error("Failed to kill process!", e);
}
return false;
}
private bool IsTerminated()
{
try
{
process.Refresh();
return process.HasExited;
}
catch (Exception e)
{
logger.Error("Failed to check whether process is terminated!", e);
}
return false;
}
private void InitializeEvent()
{
if (!eventInitialized)
{
eventInitialized = true;
process.Exited += Process_Exited;
process.EnableRaisingEvents = true;
}
}
private void Process_Exited(object sender, System.EventArgs e)
private string InitializeOriginalName()
{
Terminated?.Invoke(process.ExitCode);
try
{
using (var searcher = new ManagementObjectSearcher($"SELECT ExecutablePath FROM Win32_Process WHERE ProcessId = {process.Id}"))
using (var results = searcher.Get())
using (var processData = results.Cast<ManagementObject>().First())
{
var executablePath = Convert.ToString(processData["ExecutablePath"]);
if (File.Exists(executablePath))
{
var executableInfo = FileVersionInfo.GetVersionInfo(executablePath);
var originalName = Path.GetFileNameWithoutExtension(executableInfo.OriginalFilename);
this.originalName = originalName;
}
else
{
logger.Warn("Could not find original name!");
}
}
}
catch (Exception e)
{
logger.Error("Failed to initialize original name!", e);
}
finally
{
originalNameInitialized = true;
}
return originalName;
}
private void Process_Exited(object sender, EventArgs e)
{
TerminatedEvent?.Invoke(process.ExitCode);
}
}
}

View file

@ -7,27 +7,44 @@
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Types;
namespace SafeExamBrowser.WindowsApi
{
public class ProcessFactory : IProcessFactory
{
private ILogger logger;
private IModuleLogger logger;
public IDesktop StartupDesktop { private get; set; }
public ProcessFactory(ILogger logger)
public ProcessFactory(IModuleLogger logger)
{
this.logger = logger;
}
public IEnumerable<IProcess> GetAllRunning()
{
var processes = System.Diagnostics.Process.GetProcesses();
var originalNames = LoadOriginalNames();
foreach (var process in processes)
{
var originalName = originalNames.FirstOrDefault(n => n.processId == process.Id).originalName;
yield return new Process(process, originalName, LoggerFor(process));
}
}
public IProcess StartNew(string path, params string[] args)
{
var commandLine = $"{'"' + path + '"'} {String.Join(" ", args)}";
@ -48,11 +65,73 @@ namespace SafeExamBrowser.WindowsApi
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var process = new Process(processInfo.dwProcessId);
var raw = System.Diagnostics.Process.GetProcessById(processInfo.dwProcessId);
var process = new Process(raw, LoggerFor(raw));
logger.Info($"Successfully started process '{Path.GetFileName(path)}' with ID = {process.Id}.");
return process;
}
public bool TryGetById(int id, out IProcess process)
{
var raw = System.Diagnostics.Process.GetProcesses().FirstOrDefault(p => p.Id == id);
process = default(IProcess);
if (raw != default(System.Diagnostics.Process))
{
process = new Process(raw, LoggerFor(raw));
}
return process != default(IProcess);
}
private IEnumerable<(int processId, string originalName)> LoadOriginalNames()
{
var names = new List<(int, string)>();
try
{
using (var searcher = new ManagementObjectSearcher($"SELECT Name, ProcessId, ExecutablePath FROM Win32_Process"))
using (var results = searcher.Get())
{
var processData = results.Cast<ManagementObject>().ToList();
foreach (var process in processData)
{
using (process)
{
var processId = Convert.ToInt32(process["ProcessId"]);
var processName = Convert.ToString(process["Name"]);
var executablePath = Convert.ToString(process["ExecutablePath"]);
if (File.Exists(executablePath))
{
var executableInfo = FileVersionInfo.GetVersionInfo(executablePath);
var originalName = Path.GetFileNameWithoutExtension(executableInfo.OriginalFilename);
names.Add((processId, originalName));
}
else
{
names.Add((processId, default(string)));
}
}
}
}
}
catch (Exception e)
{
logger.Error("Failed to retrieve original names for processes!", e);
}
return names;
}
private ILogger LoggerFor(System.Diagnostics.Process process)
{
return logger.CloneFor($"{nameof(Process)} '{process.ProcessName}' ({process.Id})");
}
}
}

View file

@ -50,6 +50,7 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Management" />
</ItemGroup>
<ItemGroup>
<Compile Include="Constants\Constant.cs" />