467 lines
13 KiB
C#
467 lines
13 KiB
C#
/*
|
|
* Copyright (c) 2024 ETH Zürich, IT Services
|
|
*
|
|
* 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;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
using SafeExamBrowser.Logging.Contracts;
|
|
using SafeExamBrowser.Monitoring.Contracts.Applications;
|
|
using SafeExamBrowser.Monitoring.Contracts.Applications.Events;
|
|
using SafeExamBrowser.Settings.Applications;
|
|
using SafeExamBrowser.WindowsApi.Contracts;
|
|
|
|
namespace SafeExamBrowser.Monitoring.Applications
|
|
{
|
|
public class ApplicationMonitor : IApplicationMonitor
|
|
{
|
|
private readonly IList<BlacklistApplication> blacklist;
|
|
private readonly ILogger logger;
|
|
private readonly INativeMethods nativeMethods;
|
|
private readonly IProcessFactory processFactory;
|
|
private readonly Timer timer;
|
|
private readonly IList<WhitelistApplication> whitelist;
|
|
|
|
private Guid? captureHookId;
|
|
private Guid? foregroundHookId;
|
|
private IList<IProcess> processes;
|
|
private Window activeWindow;
|
|
|
|
public event ExplorerStartedEventHandler ExplorerStarted;
|
|
public event InstanceStartedEventHandler InstanceStarted;
|
|
public event TerminationFailedEventHandler TerminationFailed;
|
|
|
|
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>();
|
|
}
|
|
|
|
public InitializationResult Initialize(ApplicationSettings settings)
|
|
{
|
|
var result = new InitializationResult();
|
|
|
|
InitializeProcesses();
|
|
InitializeBlacklist(settings, result);
|
|
InitializeWhitelist(settings, result);
|
|
|
|
return result;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
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}.");
|
|
|
|
foregroundHookId = nativeMethods.RegisterSystemForegroundEvent(SystemEvent_WindowChanged);
|
|
logger.Info($"Registered system foreground event with ID = {foregroundHookId}.");
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
timer.Stop();
|
|
timer.Elapsed -= Timer_Elapsed;
|
|
logger.Info("Stopped monitoring applications.");
|
|
|
|
if (captureHookId.HasValue)
|
|
{
|
|
nativeMethods.DeregisterSystemEventHook(captureHookId.Value);
|
|
logger.Info($"Unregistered system capture start event with ID = {captureHookId}.");
|
|
}
|
|
|
|
if (foregroundHookId.HasValue)
|
|
{
|
|
nativeMethods.DeregisterSystemEventHook(foregroundHookId.Value);
|
|
logger.Info($"Unregistered system foreground event with ID = {foregroundHookId}.");
|
|
}
|
|
}
|
|
|
|
public bool TryGetActiveApplication(out ActiveApplication application)
|
|
{
|
|
application = default;
|
|
|
|
if (activeWindow != default && TryGetProcessFor(activeWindow, out var process))
|
|
{
|
|
var window = new Window
|
|
{
|
|
Handle = activeWindow.Handle,
|
|
Title = nativeMethods.GetWindowTitle(activeWindow.Handle)
|
|
};
|
|
|
|
application = new ActiveApplication(process, window);
|
|
}
|
|
|
|
return application != default;
|
|
}
|
|
|
|
public bool TryTerminate(RunningApplication application)
|
|
{
|
|
var success = true;
|
|
|
|
foreach (var process in application.Processes)
|
|
{
|
|
success &= TryTerminate(process);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
private void SystemEvent_WindowChanged(IntPtr handle)
|
|
{
|
|
if (handle != IntPtr.Zero && activeWindow?.Handle != handle)
|
|
{
|
|
var title = nativeMethods.GetWindowTitle(handle);
|
|
var window = new Window { Handle = handle, Title = title };
|
|
|
|
logger.Debug($"Window has changed from {activeWindow} to {window}.");
|
|
activeWindow = window;
|
|
|
|
Task.Run(() =>
|
|
{
|
|
if (!IsAllowed(window) && !TryHide(window))
|
|
{
|
|
Close(window);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
|
|
{
|
|
var failed = new List<RunningApplication>();
|
|
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();
|
|
|
|
foreach (var process in started)
|
|
{
|
|
logger.Debug($"Process {process} has been started [{process.GetAdditionalInfo()}].");
|
|
processes.Add(process);
|
|
|
|
if (process.Name == "explorer.exe")
|
|
{
|
|
HandleExplorerStart(process);
|
|
}
|
|
else if (!IsAllowed(process) && !TryTerminate(process))
|
|
{
|
|
AddFailed(process, failed);
|
|
}
|
|
else if (IsWhitelisted(process, out var applicationId))
|
|
{
|
|
HandleInstanceStart(applicationId.Value, process);
|
|
}
|
|
}
|
|
|
|
foreach (var process in terminated)
|
|
{
|
|
logger.Debug($"Process {process} has been terminated.");
|
|
processes.Remove(process);
|
|
}
|
|
|
|
if (failed.Any())
|
|
{
|
|
logger.Warn($"Failed to terminate these blacklisted applications: {string.Join(", ", failed.Select(a => a.Name))}.");
|
|
TerminationFailed?.Invoke(failed);
|
|
}
|
|
|
|
timer.Start();
|
|
}
|
|
|
|
private void AddFailed(IProcess process, List<RunningApplication> failed)
|
|
{
|
|
var name = blacklist.First(a => BelongsToApplication(process, a)).ExecutableName;
|
|
var application = failed.FirstOrDefault(a => a.Name == name);
|
|
|
|
if (application == default(RunningApplication))
|
|
{
|
|
application = new RunningApplication(name);
|
|
failed.Add(application);
|
|
}
|
|
|
|
application.Processes.Add(process);
|
|
}
|
|
|
|
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} 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} 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.OriginalName, StringComparison.OrdinalIgnoreCase) == true;
|
|
|
|
return sameName || sameOriginalName;
|
|
}
|
|
|
|
private bool BelongsToApplication(IProcess process, WhitelistApplication application)
|
|
{
|
|
var ignoreOriginalName = string.IsNullOrWhiteSpace(application.OriginalName);
|
|
var ignoreSignature = string.IsNullOrWhiteSpace(application.Signature);
|
|
var sameName = process.Name.Equals(application.ExecutableName, StringComparison.OrdinalIgnoreCase);
|
|
var sameOriginalName = process.OriginalName?.Equals(application.OriginalName, StringComparison.OrdinalIgnoreCase) == true;
|
|
var sameSignature = process.Signature?.Equals(application.Signature?.ToLower(), StringComparison.OrdinalIgnoreCase) == true;
|
|
|
|
return sameName && (ignoreOriginalName || sameOriginalName) && (ignoreSignature || sameSignature);
|
|
}
|
|
|
|
private bool BelongsToSafeExamBrowser(IProcess process)
|
|
{
|
|
var isClient = true;
|
|
var isRuntime = true;
|
|
|
|
isClient &= process.Name == "SafeExamBrowser.Client.exe";
|
|
isClient &= process.OriginalName == "SafeExamBrowser.Client.exe";
|
|
|
|
isRuntime &= process.Name == "SafeExamBrowser.exe";
|
|
isRuntime &= process.OriginalName == "SafeExamBrowser.exe";
|
|
|
|
#if !DEBUG
|
|
isClient &= process.Signature == "2bc82fe8e56a39f96bc6c4b91d6703a0379b76a2";
|
|
isRuntime &= process.Signature == "2bc82fe8e56a39f96bc6c4b91d6703a0379b76a2";
|
|
#endif
|
|
|
|
return isClient || isRuntime;
|
|
}
|
|
|
|
private void Close(Window window)
|
|
{
|
|
nativeMethods.SendCloseMessageTo(window.Handle);
|
|
logger.Info($"Sent close message to window {window}.");
|
|
}
|
|
|
|
private void HandleExplorerStart(IProcess process)
|
|
{
|
|
logger.Warn($"A new instance of Windows Explorer {process} has been started!");
|
|
Task.Run(() => ExplorerStarted?.Invoke());
|
|
}
|
|
|
|
private void HandleInstanceStart(Guid applicationId, IProcess process)
|
|
{
|
|
logger.Debug($"Detected start of whitelisted application instance {process}.");
|
|
Task.Run(() => InstanceStarted?.Invoke(applicationId, process));
|
|
}
|
|
|
|
private void InitializeProcesses()
|
|
{
|
|
processes = processFactory.GetAllRunning();
|
|
logger.Debug($"Initialized {processes.Count} currently running processes.");
|
|
}
|
|
|
|
private void InitializeBlacklist(ApplicationSettings settings, InitializationResult result)
|
|
{
|
|
foreach (var application in settings.Blacklist)
|
|
{
|
|
blacklist.Add(application);
|
|
}
|
|
|
|
logger.Debug($"Initialized blacklist with {blacklist.Count} applications{(blacklist.Any() ? $": {string.Join(", ", blacklist.Select(a => a.ExecutableName))}" : ".")}");
|
|
|
|
foreach (var process in processes)
|
|
{
|
|
foreach (var application in blacklist)
|
|
{
|
|
var isBlacklisted = BelongsToApplication(process, application);
|
|
|
|
if (isBlacklisted)
|
|
{
|
|
if (!application.AutoTerminate)
|
|
{
|
|
AddForTermination(application.ExecutableName, process, result);
|
|
}
|
|
else if (application.AutoTerminate && !TryTerminate(process))
|
|
{
|
|
AddFailed(application.ExecutableName, process, result);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitializeWhitelist(ApplicationSettings settings, InitializationResult result)
|
|
{
|
|
foreach (var application in settings.Whitelist)
|
|
{
|
|
whitelist.Add(application);
|
|
}
|
|
|
|
logger.Debug($"Initialized whitelist with {whitelist.Count} applications{(whitelist.Any() ? $": {string.Join(", ", whitelist.Select(a => a.ExecutableName))}" : ".")}");
|
|
|
|
foreach (var process in processes)
|
|
{
|
|
foreach (var application in whitelist)
|
|
{
|
|
var isWhitelisted = BelongsToApplication(process, application);
|
|
|
|
if (isWhitelisted)
|
|
{
|
|
if (!application.AllowRunning && !application.AutoTerminate)
|
|
{
|
|
AddForTermination(application.ExecutableName, process, result);
|
|
}
|
|
else if (!application.AllowRunning && application.AutoTerminate && !TryTerminate(process))
|
|
{
|
|
AddFailed(application.ExecutableName, process, result);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsAllowed(IProcess process)
|
|
{
|
|
foreach (var application in blacklist)
|
|
{
|
|
if (BelongsToApplication(process, application))
|
|
{
|
|
logger.Warn($"Process {process} belongs to blacklisted application '{application.ExecutableName}'!");
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsAllowed(Window window)
|
|
{
|
|
var allowed = false;
|
|
|
|
if (TryGetProcessFor(window, out var process))
|
|
{
|
|
allowed = BelongsToSafeExamBrowser(process) || IsWhitelisted(process, out _);
|
|
}
|
|
|
|
if (!allowed)
|
|
{
|
|
logger.Warn($"Window {window} belongs to not whitelisted process '{process?.Name ?? "n/a"}'!");
|
|
}
|
|
|
|
return allowed;
|
|
}
|
|
|
|
private bool IsWhitelisted(IProcess process, out Guid? applicationId)
|
|
{
|
|
applicationId = default;
|
|
|
|
foreach (var application in whitelist)
|
|
{
|
|
if (BelongsToApplication(process, application))
|
|
{
|
|
applicationId = application.Id;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetProcessFor(Window window, out IProcess process)
|
|
{
|
|
var processId = Convert.ToInt32(nativeMethods.GetProcessIdFor(window.Handle));
|
|
|
|
if (!processFactory.TryGetById(processId, out process))
|
|
{
|
|
logger.Error($"Could not find process for window {window} and process ID = {processId}!");
|
|
}
|
|
|
|
return process != default;
|
|
}
|
|
|
|
private bool TryHide(Window window)
|
|
{
|
|
var success = nativeMethods.HideWindow(window.Handle);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info($"Successfully hid window {window}.");
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Failed to hide window {window}!");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
private bool TryTerminate(IProcess process)
|
|
{
|
|
const int MAX_ATTEMPTS = 5;
|
|
const int TIMEOUT = 500;
|
|
|
|
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.TryKill(TIMEOUT))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (process.HasTerminated)
|
|
{
|
|
logger.Info($"Successfully terminated process {process}.");
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Failed to terminate process {process}!");
|
|
}
|
|
|
|
return process.HasTerminated;
|
|
}
|
|
}
|
|
}
|