diff --git a/AppControl Manager/AppControl Manager.csproj b/AppControl Manager/AppControl Manager.csproj index 62fad821d..6063fbaf6 100644 --- a/AppControl Manager/AppControl Manager.csproj +++ b/AppControl Manager/AppControl Manager.csproj @@ -91,7 +91,7 @@ AppControlManager False send - 1.8.7.0 + 1.8.8.0 $(FileVersion) en-US LICENSE diff --git a/AppControl Manager/IntelGathering/GetMDEAdvancedHuntingLogsData.cs b/AppControl Manager/IntelGathering/GetMDEAdvancedHuntingLogsData.cs index 3625ade90..e1bbaa2de 100644 --- a/AppControl Manager/IntelGathering/GetMDEAdvancedHuntingLogsData.cs +++ b/AppControl Manager/IntelGathering/GetMDEAdvancedHuntingLogsData.cs @@ -18,7 +18,6 @@ internal static class GetMDEAdvancedHuntingLogsData internal static HashSet Retrieve(List data) { - // HashSet to store the output, ensures the data are unique and signed data are prioritized over unsigned data FileIdentitySignatureBasedHashSet fileIdentities = new(); @@ -89,8 +88,8 @@ internal static HashSet Retrieve(List data SHA256Hash = possibleCodeIntegrityAuditEvent.SHA256, SHA1FlatHash = possibleCodeIntegrityAuditEvent.Sha1FlatHash, SHA256FlatHash = possibleCodeIntegrityAuditEvent.Sha256FlatHash, - USN = GetLongValue(possibleCodeIntegrityAuditEvent.USN), - SISigningScenario = GetIntValue(possibleCodeIntegrityAuditEvent.SiSigningScenario) ?? 1, + USN = possibleCodeIntegrityAuditEvent.USN, + SISigningScenario = possibleCodeIntegrityAuditEvent.SiSigningScenario ?? 1, PolicyName = possibleCodeIntegrityAuditEvent.PolicyName, PolicyID = possibleCodeIntegrityAuditEvent.PolicyID, PolicyHash = possibleCodeIntegrityAuditEvent.PolicyHash, @@ -130,13 +129,13 @@ internal static HashSet Retrieve(List data FileSignerInfo signerInfo = new() { - TotalSignatureCount = GetIntValue(correlatedEvent.TotalSignatureCount), - Signature = GetIntValue(correlatedEvent.Signature), + TotalSignatureCount = correlatedEvent.TotalSignatureCount, + Signature = correlatedEvent.Signature, Hash = correlatedEvent.Hash, SignatureType = GetSignatureType(GetIntValue(correlatedEvent.SignatureType)), ValidatedSigningLevel = GetValidatedRequestedSigningLevel(GetIntValue(correlatedEvent.ValidatedSigningLevel)), VerificationError = GetVerificationError(GetIntValue(correlatedEvent.VerificationError)), - Flags = GetIntValue(correlatedEvent.Flags), + Flags = correlatedEvent.Flags, NotValidBefore = GetEventDataDateTimeValue(correlatedEvent.NotValidBefore), NotValidAfter = GetEventDataDateTimeValue(correlatedEvent.NotValidAfter), PublisherName = PublisherName, @@ -196,8 +195,8 @@ internal static HashSet Retrieve(List data SHA256Hash = possibleCodeIntegrityBlockEvent.SHA256, SHA1FlatHash = possibleCodeIntegrityBlockEvent.Sha1FlatHash, SHA256FlatHash = possibleCodeIntegrityBlockEvent.Sha256FlatHash, - USN = GetLongValue(possibleCodeIntegrityBlockEvent.USN), - SISigningScenario = GetIntValue(possibleCodeIntegrityBlockEvent.SiSigningScenario) ?? 1, + USN = possibleCodeIntegrityBlockEvent.USN, + SISigningScenario = possibleCodeIntegrityBlockEvent.SiSigningScenario ?? 1, PolicyName = possibleCodeIntegrityBlockEvent.PolicyName, PolicyID = possibleCodeIntegrityBlockEvent.PolicyID, PolicyHash = possibleCodeIntegrityBlockEvent.PolicyHash, @@ -237,13 +236,13 @@ internal static HashSet Retrieve(List data FileSignerInfo signerInfo = new() { - TotalSignatureCount = GetIntValue(correlatedEvent.TotalSignatureCount), - Signature = GetIntValue(correlatedEvent.Signature), + TotalSignatureCount = correlatedEvent.TotalSignatureCount, + Signature = correlatedEvent.Signature, Hash = correlatedEvent.Hash, SignatureType = GetSignatureType(GetIntValue(correlatedEvent.SignatureType)), ValidatedSigningLevel = GetValidatedRequestedSigningLevel(GetIntValue(correlatedEvent.ValidatedSigningLevel)), VerificationError = GetVerificationError(GetIntValue(correlatedEvent.VerificationError)), - Flags = GetIntValue(correlatedEvent.Flags), + Flags = correlatedEvent.Flags, NotValidBefore = GetEventDataDateTimeValue(correlatedEvent.NotValidBefore), NotValidAfter = GetEventDataDateTimeValue(correlatedEvent.NotValidAfter), PublisherName = PublisherName, @@ -303,8 +302,8 @@ internal static HashSet Retrieve(List data SHA256Hash = possibleAppLockerAuditEvent.SHA256, SHA1FlatHash = possibleAppLockerAuditEvent.Sha1FlatHash, SHA256FlatHash = possibleAppLockerAuditEvent.Sha256FlatHash, - USN = GetLongValue(possibleAppLockerAuditEvent.USN), - SISigningScenario = GetIntValue(possibleAppLockerAuditEvent.SiSigningScenario) ?? 1, + USN = possibleAppLockerAuditEvent.USN, + SISigningScenario = possibleAppLockerAuditEvent.SiSigningScenario ?? 1, PolicyName = possibleAppLockerAuditEvent.PolicyName, PolicyID = possibleAppLockerAuditEvent.PolicyID, PolicyHash = possibleAppLockerAuditEvent.PolicyHash, @@ -344,13 +343,13 @@ internal static HashSet Retrieve(List data FileSignerInfo signerInfo = new() { - TotalSignatureCount = GetIntValue(correlatedEvent.TotalSignatureCount), - Signature = GetIntValue(correlatedEvent.Signature), + TotalSignatureCount = correlatedEvent.TotalSignatureCount, + Signature = correlatedEvent.Signature, Hash = correlatedEvent.Hash, SignatureType = GetSignatureType(GetIntValue(correlatedEvent.SignatureType)), ValidatedSigningLevel = GetValidatedRequestedSigningLevel(GetIntValue(correlatedEvent.ValidatedSigningLevel)), VerificationError = GetVerificationError(GetIntValue(correlatedEvent.VerificationError)), - Flags = GetIntValue(correlatedEvent.Flags), + Flags = correlatedEvent.Flags, NotValidBefore = GetEventDataDateTimeValue(correlatedEvent.NotValidBefore), NotValidAfter = GetEventDataDateTimeValue(correlatedEvent.NotValidAfter), PublisherName = PublisherName, @@ -408,8 +407,8 @@ internal static HashSet Retrieve(List data SHA256Hash = possibleAppLockerBlockEvent.SHA256, SHA1FlatHash = possibleAppLockerBlockEvent.Sha1FlatHash, SHA256FlatHash = possibleAppLockerBlockEvent.Sha256FlatHash, - USN = GetLongValue(possibleAppLockerBlockEvent.USN), - SISigningScenario = GetIntValue(possibleAppLockerBlockEvent.SiSigningScenario) ?? 1, + USN = possibleAppLockerBlockEvent.USN, + SISigningScenario = possibleAppLockerBlockEvent.SiSigningScenario ?? 1, PolicyName = possibleAppLockerBlockEvent.PolicyName, PolicyID = possibleAppLockerBlockEvent.PolicyID, PolicyHash = possibleAppLockerBlockEvent.PolicyHash, @@ -449,13 +448,13 @@ internal static HashSet Retrieve(List data FileSignerInfo signerInfo = new() { - TotalSignatureCount = GetIntValue(correlatedEvent.TotalSignatureCount), - Signature = GetIntValue(correlatedEvent.Signature), + TotalSignatureCount = correlatedEvent.TotalSignatureCount, + Signature = correlatedEvent.Signature, Hash = correlatedEvent.Hash, SignatureType = GetSignatureType(GetIntValue(correlatedEvent.SignatureType)), ValidatedSigningLevel = GetValidatedRequestedSigningLevel(GetIntValue(correlatedEvent.ValidatedSigningLevel)), VerificationError = GetVerificationError(GetIntValue(correlatedEvent.VerificationError)), - Flags = GetIntValue(correlatedEvent.Flags), + Flags = correlatedEvent.Flags, NotValidBefore = GetEventDataDateTimeValue(correlatedEvent.NotValidBefore), NotValidAfter = GetEventDataDateTimeValue(correlatedEvent.NotValidAfter), PublisherName = PublisherName, @@ -480,9 +479,7 @@ internal static HashSet Retrieve(List data // Add the entire event package to the output list _ = fileIdentities.Add(eventData); - } - } @@ -508,7 +505,6 @@ internal static HashSet Retrieve(List data return null; } - /// /// Method to safely get an integer value from string /// @@ -519,30 +515,24 @@ internal static HashSet Retrieve(List data return data is not null && int.TryParse(data, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result) ? result : null; } - - /// - /// Converts string to DateTime + /// Safely converts string to DateTime /// private static DateTime? GetEventDataDateTimeValue(string? data) { return data is not null && DateTime.TryParse(data, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime result) ? result : null; } - - - private static long? GetLongValue(string? data) - { - return data is not null && long.TryParse(data, NumberStyles.Integer, CultureInfo.InvariantCulture, out long result) ? result : null; - } - + /// + /// Safely converts string to GUID + /// + /// + /// private static Guid? GetGuidValue(string? data) { return data is not null && Guid.TryParse(data, out Guid guid) ? guid : null; } - - /// /// Resolves the Validated/Requested Signing Level int to friendly string /// @@ -562,7 +552,6 @@ internal static HashSet Retrieve(List data } } - /// /// Resolves the VerificationError int to a friendly string /// @@ -581,7 +570,6 @@ internal static HashSet Retrieve(List data } } - /// /// Resolves the SignatureType int to a friendly string /// @@ -600,8 +588,6 @@ internal static HashSet Retrieve(List data } } - #endregion - } diff --git a/AppControl Manager/IntelGathering/MDEAdvancedHuntingData.cs b/AppControl Manager/IntelGathering/MDEAdvancedHuntingData.cs index 97d8c97fe..f6c9d4b5e 100644 --- a/AppControl Manager/IntelGathering/MDEAdvancedHuntingData.cs +++ b/AppControl Manager/IntelGathering/MDEAdvancedHuntingData.cs @@ -1,71 +1,749 @@ - +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + namespace AppControlManager.IntelGathering; -// an internal class to store the structure of the new CSV data +// The following converters are needed for when parsing the MDE AH CSV file +// The parsing from JSON string via MS Graph doesn't require it, but we use it for both + +public class NullableBoolJsonConverter : JsonConverter +{ + public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // If the token is a boolean, return it directly. + if (reader.TokenType == JsonTokenType.True) + { + return true; + } + if (reader.TokenType == JsonTokenType.False) + { + return false; + } + + // If the token is a string, attempt to parse it. + if (reader.TokenType == JsonTokenType.String) + { + string? strValue = reader.GetString(); + if (bool.TryParse(strValue, out bool boolValue)) + { + return boolValue; + } + // handle numeric strings "1" or "0", just for future proofing + if (strValue == "1") + { + return true; + } + if (strValue == "0") + { + return false; + } + } + + // If the token is null, return null. + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + // return null if conversion is not possible. + return null; + } + + public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteBooleanValue(value.Value); + } + else + { + writer.WriteNullValue(); + } + } +} + + +public class NullableIntJsonConverter : JsonConverter +{ + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // If the token is a number, read it directly. + if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int value)) + { + return value; + } + + // If the token is a string, try to parse it. + if (reader.TokenType == JsonTokenType.String) + { + string? stringValue = reader.GetString(); + if (int.TryParse(stringValue, out value)) + { + return value; + } + } + + // For any other token, return null + return null; + } + + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteNumberValue(value.Value); + } + else + { + writer.WriteNullValue(); + } + } +} + + +public class NullableLongJsonConverter : JsonConverter +{ + public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // If the token is a long number, try to read it directly. + if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt64(out long value)) + { + return value; + } + + // If the token is a string, try to parse it. + if (reader.TokenType == JsonTokenType.String) + { + string? stringValue = reader.GetString(); + if (long.TryParse(stringValue, out value)) + { + return value; + } + } + + // Otherwise, return null + return null; + } + + public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteNumberValue(value.Value); + } + else + { + writer.WriteNullValue(); + } + } +} + + +// The top-level object because the JSON file contains a "results" array. +internal sealed class MDEAdvancedHuntingDataRootObject +{ + [JsonInclude] + [JsonPropertyName("results")] + internal List Results { get; set; } = []; +} + +// A custom converter on the type so that we can automatically merge properties +// from the nested AdditionalFields JSON. +// So this class contains its own properties + properties of the AdditionalFields class +[JsonConverter(typeof(MDEAdvancedHuntingDataConverter))] internal sealed class MDEAdvancedHuntingData { + // Main properties (from the top-level JSON object) + [JsonInclude] + [JsonPropertyName("Timestamp")] internal string? Timestamp { get; set; } + + [JsonInclude] + [JsonPropertyName("DeviceId")] internal string? DeviceId { get; set; } + + [JsonInclude] + [JsonPropertyName("DeviceName")] internal string? DeviceName { get; set; } + + [JsonInclude] + [JsonPropertyName("ActionType")] internal string? ActionType { get; set; } + + [JsonInclude] + [JsonPropertyName("FileName")] internal string? FileName { get; set; } + + [JsonInclude] + [JsonPropertyName("FolderPath")] internal string? FolderPath { get; set; } + + [JsonInclude] + [JsonPropertyName("SHA1")] internal string? SHA1 { get; set; } + + [JsonInclude] + [JsonPropertyName("SHA256")] internal string? SHA256 { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessSHA1")] internal string? InitiatingProcessSHA1 { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessSHA256")] internal string? InitiatingProcessSHA256 { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessMD5")] internal string? InitiatingProcessMD5 { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessFileName")] internal string? InitiatingProcessFileName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessFileSize")] internal string? InitiatingProcessFileSize { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessFolderPath")] internal string? InitiatingProcessFolderPath { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessId")] internal string? InitiatingProcessId { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessCommandLine")] internal string? InitiatingProcessCommandLine { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessCreationTime")] internal string? InitiatingProcessCreationTime { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessAccountDomain")] internal string? InitiatingProcessAccountDomain { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessAccountName")] internal string? InitiatingProcessAccountName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessAccountSid")] internal string? InitiatingProcessAccountSid { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessVersionInfoCompanyName")] internal string? InitiatingProcessVersionInfoCompanyName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessVersionInfoProductName")] internal string? InitiatingProcessVersionInfoProductName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessVersionInfoProductVersion")] internal string? InitiatingProcessVersionInfoProductVersion { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessVersionInfoInternalFileName")] internal string? InitiatingProcessVersionInfoInternalFileName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessVersionInfoOriginalFileName")] internal string? InitiatingProcessVersionInfoOriginalFileName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessVersionInfoFileDescription")] internal string? InitiatingProcessVersionInfoFileDescription { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessParentId")] internal string? InitiatingProcessParentId { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessParentFileName")] internal string? InitiatingProcessParentFileName { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessParentCreationTime")] internal string? InitiatingProcessParentCreationTime { get; set; } + + [JsonInclude] + [JsonPropertyName("InitiatingProcessLogonId")] internal string? InitiatingProcessLogonId { get; set; } + + [JsonInclude] + [JsonPropertyName("ReportId")] internal string? ReportId { get; set; } - // Additional Fields JSON properties + // Additional fields merged from the AdditionalFields JSON string. + [JsonInclude] + [JsonPropertyName("PolicyID")] + internal string? PolicyID { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyName")] + internal string? PolicyName { get; set; } + + [JsonInclude] + [JsonPropertyName("RequestedSigningLevel")] + internal string? RequestedSigningLevel { get; set; } + + [JsonInclude] + [JsonPropertyName("ValidatedSigningLevel")] + internal string? ValidatedSigningLevel { get; set; } + + [JsonInclude] + [JsonPropertyName("ProcessName")] + internal string? ProcessName { get; set; } + + [JsonInclude] + [JsonPropertyName("StatusCode")] + internal string? StatusCode { get; set; } + + [JsonInclude] + [JsonPropertyName("Sha1FlatHash")] + internal string? Sha1FlatHash { get; set; } + + [JsonInclude] + [JsonPropertyName("Sha256FlatHash")] + internal string? Sha256FlatHash { get; set; } + + [JsonInclude] + [JsonPropertyName("USN")] + [JsonConverter(typeof(NullableLongJsonConverter))] + internal long? USN { get; set; } + + [JsonInclude] + [JsonPropertyName("SiSigningScenario")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? SiSigningScenario { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyHash")] + internal string? PolicyHash { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyGuid")] + internal string? PolicyGuid { get; set; } + + [JsonInclude] + [JsonPropertyName("UserWriteable")] + [JsonConverter(typeof(NullableBoolJsonConverter))] + internal bool? UserWriteable { get; set; } + + [JsonInclude] + [JsonPropertyName("OriginalFileName")] + internal string? OriginalFileName { get; set; } + + [JsonInclude] + [JsonPropertyName("InternalName")] + internal string? InternalName { get; set; } + + [JsonInclude] + [JsonPropertyName("FileDescription")] + internal string? FileDescription { get; set; } + + [JsonInclude] + [JsonPropertyName("FileVersion")] + internal string? FileVersion { get; set; } + + [JsonInclude] + [JsonPropertyName("EtwActivityId")] + internal string? EtwActivityId { get; set; } + + [JsonInclude] + [JsonPropertyName("IssuerName")] + internal string? IssuerName { get; set; } + + [JsonInclude] + [JsonPropertyName("IssuerTBSHash")] + internal string? IssuerTBSHash { get; set; } + + [JsonInclude] + [JsonPropertyName("NotValidAfter")] + internal string? NotValidAfter { get; set; } + + [JsonInclude] + [JsonPropertyName("NotValidBefore")] + internal string? NotValidBefore { get; set; } + + [JsonInclude] + [JsonPropertyName("PublisherName")] + internal string? PublisherName { get; set; } + + [JsonInclude] + [JsonPropertyName("PublisherTBSHash")] + internal string? PublisherTBSHash { get; set; } + + [JsonInclude] + [JsonPropertyName("SignatureType")] + internal string? SignatureType { get; set; } + + [JsonInclude] + [JsonPropertyName("TotalSignatureCount")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? TotalSignatureCount { get; set; } + + [JsonInclude] + [JsonPropertyName("VerificationError")] + internal string? VerificationError { get; set; } + + [JsonInclude] + [JsonPropertyName("Signature")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? Signature { get; set; } + + [JsonInclude] + [JsonPropertyName("Hash")] + internal string? Hash { get; set; } + + [JsonInclude] + [JsonPropertyName("Flags")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? Flags { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyBits")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? PolicyBits { get; set; } +} + +// This type represents the additional fields that are stored as a JSON string +// inside the "AdditionalFields" property. +internal sealed class AdditionalFields +{ + [JsonInclude] + [JsonPropertyName("PolicyID")] internal string? PolicyID { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyName")] internal string? PolicyName { get; set; } + + [JsonInclude] + [JsonPropertyName("RequestedSigningLevel")] internal string? RequestedSigningLevel { get; set; } + + [JsonInclude] + [JsonPropertyName("ValidatedSigningLevel")] internal string? ValidatedSigningLevel { get; set; } + + [JsonInclude] + [JsonPropertyName("ProcessName")] internal string? ProcessName { get; set; } + + [JsonInclude] + [JsonPropertyName("StatusCode")] internal string? StatusCode { get; set; } + + [JsonInclude] + [JsonPropertyName("Sha1FlatHash")] internal string? Sha1FlatHash { get; set; } + + [JsonInclude] + [JsonPropertyName("Sha256FlatHash")] internal string? Sha256FlatHash { get; set; } - internal string? USN { get; set; } - internal string? SiSigningScenario { get; set; } + + [JsonInclude] + [JsonPropertyName("USN")] + [JsonConverter(typeof(NullableLongJsonConverter))] + internal long? USN { get; set; } + + [JsonInclude] + [JsonPropertyName("SiSigningScenario")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? SiSigningScenario { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyHash")] internal string? PolicyHash { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyGuid")] internal string? PolicyGuid { get; set; } + + [JsonInclude] + [JsonPropertyName("UserWriteable")] + [JsonConverter(typeof(NullableBoolJsonConverter))] internal bool? UserWriteable { get; set; } + + [JsonInclude] + [JsonPropertyName("OriginalFileName")] internal string? OriginalFileName { get; set; } + + [JsonInclude] + [JsonPropertyName("InternalName")] internal string? InternalName { get; set; } + + [JsonInclude] + [JsonPropertyName("FileDescription")] internal string? FileDescription { get; set; } + + [JsonInclude] + [JsonPropertyName("FileVersion")] internal string? FileVersion { get; set; } + + [JsonInclude] + [JsonPropertyName("EtwActivityId")] internal string? EtwActivityId { get; set; } + + [JsonInclude] + [JsonPropertyName("IssuerName")] internal string? IssuerName { get; set; } + + [JsonInclude] + [JsonPropertyName("IssuerTBSHash")] internal string? IssuerTBSHash { get; set; } + + [JsonInclude] + [JsonPropertyName("NotValidAfter")] internal string? NotValidAfter { get; set; } + + [JsonInclude] + [JsonPropertyName("NotValidBefore")] internal string? NotValidBefore { get; set; } + + [JsonInclude] + [JsonPropertyName("PublisherName")] internal string? PublisherName { get; set; } + + [JsonInclude] + [JsonPropertyName("PublisherTBSHash")] internal string? PublisherTBSHash { get; set; } + + [JsonInclude] + [JsonPropertyName("SignatureType")] internal string? SignatureType { get; set; } - internal string? TotalSignatureCount { get; set; } + + [JsonInclude] + [JsonPropertyName("TotalSignatureCount")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? TotalSignatureCount { get; set; } + + [JsonInclude] + [JsonPropertyName("VerificationError")] internal string? VerificationError { get; set; } - internal string? Signature { get; set; } + + [JsonInclude] + [JsonPropertyName("Signature")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? Signature { get; set; } + + [JsonInclude] + [JsonPropertyName("Hash")] internal string? Hash { get; set; } - internal string? Flags { get; set; } - internal string? PolicyBits { get; set; } + + [JsonInclude] + [JsonPropertyName("Flags")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? Flags { get; set; } + + [JsonInclude] + [JsonPropertyName("PolicyBits")] + [JsonConverter(typeof(NullableIntJsonConverter))] + internal int? PolicyBits { get; set; } +} + +// Custom converter for MDEAdvancedHuntingData that reads both the top-level properties +// and, if present, parses the AdditionalFields string (which is JSON) into an AdditionalFields instance. +internal sealed class MDEAdvancedHuntingDataConverter : JsonConverter +{ + public override MDEAdvancedHuntingData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using JsonDocument document = JsonDocument.ParseValue(ref reader); + JsonElement root = document.RootElement; + + MDEAdvancedHuntingData result = new() + { + // Main properties + Timestamp = GetStringOrNull(root, "Timestamp"), + DeviceId = GetStringOrNull(root, "DeviceId"), + DeviceName = GetStringOrNull(root, "DeviceName"), + ActionType = GetStringOrNull(root, "ActionType"), + FileName = GetStringOrNull(root, "FileName"), + FolderPath = GetStringOrNull(root, "FolderPath"), + SHA1 = GetStringOrNull(root, "SHA1"), + SHA256 = GetStringOrNull(root, "SHA256"), + InitiatingProcessSHA1 = GetStringOrNull(root, "InitiatingProcessSHA1"), + InitiatingProcessSHA256 = GetStringOrNull(root, "InitiatingProcessSHA256"), + InitiatingProcessMD5 = GetStringOrNull(root, "InitiatingProcessMD5"), + InitiatingProcessFileName = GetStringOrNull(root, "InitiatingProcessFileName"), + InitiatingProcessFileSize = GetStringOrNull(root, "InitiatingProcessFileSize"), + InitiatingProcessFolderPath = GetStringOrNull(root, "InitiatingProcessFolderPath"), + InitiatingProcessId = GetStringOrNull(root, "InitiatingProcessId"), + InitiatingProcessCommandLine = GetStringOrNull(root, "InitiatingProcessCommandLine"), + InitiatingProcessCreationTime = GetStringOrNull(root, "InitiatingProcessCreationTime"), + InitiatingProcessAccountDomain = GetStringOrNull(root, "InitiatingProcessAccountDomain"), + InitiatingProcessAccountName = GetStringOrNull(root, "InitiatingProcessAccountName"), + InitiatingProcessAccountSid = GetStringOrNull(root, "InitiatingProcessAccountSid"), + InitiatingProcessVersionInfoCompanyName = GetStringOrNull(root, "InitiatingProcessVersionInfoCompanyName"), + InitiatingProcessVersionInfoProductName = GetStringOrNull(root, "InitiatingProcessVersionInfoProductName"), + InitiatingProcessVersionInfoProductVersion = GetStringOrNull(root, "InitiatingProcessVersionInfoProductVersion"), + InitiatingProcessVersionInfoInternalFileName = GetStringOrNull(root, "InitiatingProcessVersionInfoInternalFileName"), + InitiatingProcessVersionInfoOriginalFileName = GetStringOrNull(root, "InitiatingProcessVersionInfoOriginalFileName"), + InitiatingProcessVersionInfoFileDescription = GetStringOrNull(root, "InitiatingProcessVersionInfoFileDescription"), + InitiatingProcessParentId = GetStringOrNull(root, "InitiatingProcessParentId"), + InitiatingProcessParentFileName = GetStringOrNull(root, "InitiatingProcessParentFileName"), + InitiatingProcessParentCreationTime = GetStringOrNull(root, "InitiatingProcessParentCreationTime"), + InitiatingProcessLogonId = GetStringOrNull(root, "InitiatingProcessLogonId"), + ReportId = GetStringOrNull(root, "ReportId") + }; + + // If the JSON element contains an AdditionalFields property, + // parse its string value (which is expected to be JSON) into an AdditionalFields instance + // using the source-generated context. + if (root.TryGetProperty("AdditionalFields", out JsonElement additionalFieldsElement)) + { + if (additionalFieldsElement.ValueKind == JsonValueKind.String) + { + string? additionalJson = additionalFieldsElement.GetString(); + if (!string.IsNullOrWhiteSpace(additionalJson)) + { + AdditionalFields? additional = JsonSerializer.Deserialize( + additionalJson, + MDEAdvancedHuntingJSONSerializationContext.Default.AdditionalFields); + if (additional is not null) + { + result.PolicyID = additional.PolicyID; + result.PolicyName = additional.PolicyName; + result.RequestedSigningLevel = additional.RequestedSigningLevel; + result.ValidatedSigningLevel = additional.ValidatedSigningLevel; + result.ProcessName = additional.ProcessName; + result.StatusCode = additional.StatusCode; + result.Sha1FlatHash = additional.Sha1FlatHash; + result.Sha256FlatHash = additional.Sha256FlatHash; + result.USN = additional.USN; + result.SiSigningScenario = additional.SiSigningScenario; + result.PolicyHash = additional.PolicyHash; + result.PolicyGuid = additional.PolicyGuid; + result.UserWriteable = additional.UserWriteable; + result.OriginalFileName = additional.OriginalFileName; + result.InternalName = additional.InternalName; + result.FileDescription = additional.FileDescription; + result.FileVersion = additional.FileVersion; + result.EtwActivityId = additional.EtwActivityId; + result.IssuerName = additional.IssuerName; + result.IssuerTBSHash = additional.IssuerTBSHash; + result.NotValidAfter = additional.NotValidAfter; + result.NotValidBefore = additional.NotValidBefore; + result.PublisherName = additional.PublisherName; + result.PublisherTBSHash = additional.PublisherTBSHash; + result.SignatureType = additional.SignatureType; + result.TotalSignatureCount = additional.TotalSignatureCount; + result.VerificationError = additional.VerificationError; + result.Signature = additional.Signature; + result.Hash = additional.Hash; + result.Flags = additional.Flags; + result.PolicyBits = additional.PolicyBits; + } + } + } + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, MDEAdvancedHuntingData value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + // Write main properties + writer.WriteString("Timestamp", value.Timestamp); + writer.WriteString("DeviceId", value.DeviceId); + writer.WriteString("DeviceName", value.DeviceName); + writer.WriteString("ActionType", value.ActionType); + writer.WriteString("FileName", value.FileName); + writer.WriteString("FolderPath", value.FolderPath); + writer.WriteString("SHA1", value.SHA1); + writer.WriteString("SHA256", value.SHA256); + writer.WriteString("InitiatingProcessSHA1", value.InitiatingProcessSHA1); + writer.WriteString("InitiatingProcessSHA256", value.InitiatingProcessSHA256); + writer.WriteString("InitiatingProcessMD5", value.InitiatingProcessMD5); + writer.WriteString("InitiatingProcessFileName", value.InitiatingProcessFileName); + writer.WriteString("InitiatingProcessFileSize", value.InitiatingProcessFileSize); + writer.WriteString("InitiatingProcessFolderPath", value.InitiatingProcessFolderPath); + writer.WriteString("InitiatingProcessId", value.InitiatingProcessId); + writer.WriteString("InitiatingProcessCommandLine", value.InitiatingProcessCommandLine); + writer.WriteString("InitiatingProcessCreationTime", value.InitiatingProcessCreationTime); + writer.WriteString("InitiatingProcessAccountDomain", value.InitiatingProcessAccountDomain); + writer.WriteString("InitiatingProcessAccountName", value.InitiatingProcessAccountName); + writer.WriteString("InitiatingProcessAccountSid", value.InitiatingProcessAccountSid); + writer.WriteString("InitiatingProcessVersionInfoCompanyName", value.InitiatingProcessVersionInfoCompanyName); + writer.WriteString("InitiatingProcessVersionInfoProductName", value.InitiatingProcessVersionInfoProductName); + writer.WriteString("InitiatingProcessVersionInfoProductVersion", value.InitiatingProcessVersionInfoProductVersion); + writer.WriteString("InitiatingProcessVersionInfoInternalFileName", value.InitiatingProcessVersionInfoInternalFileName); + writer.WriteString("InitiatingProcessVersionInfoOriginalFileName", value.InitiatingProcessVersionInfoOriginalFileName); + writer.WriteString("InitiatingProcessVersionInfoFileDescription", value.InitiatingProcessVersionInfoFileDescription); + writer.WriteString("InitiatingProcessParentId", value.InitiatingProcessParentId); + writer.WriteString("InitiatingProcessParentFileName", value.InitiatingProcessParentFileName); + writer.WriteString("InitiatingProcessParentCreationTime", value.InitiatingProcessParentCreationTime); + writer.WriteString("InitiatingProcessLogonId", value.InitiatingProcessLogonId); + writer.WriteString("ReportId", value.ReportId); + + // Create an AdditionalFields instance from the additional properties. + AdditionalFields additional = new() + { + PolicyID = value.PolicyID, + PolicyName = value.PolicyName, + RequestedSigningLevel = value.RequestedSigningLevel, + ValidatedSigningLevel = value.ValidatedSigningLevel, + ProcessName = value.ProcessName, + StatusCode = value.StatusCode, + Sha1FlatHash = value.Sha1FlatHash, + Sha256FlatHash = value.Sha256FlatHash, + USN = value.USN, + SiSigningScenario = value.SiSigningScenario, + PolicyHash = value.PolicyHash, + PolicyGuid = value.PolicyGuid, + UserWriteable = value.UserWriteable, + OriginalFileName = value.OriginalFileName, + InternalName = value.InternalName, + FileDescription = value.FileDescription, + FileVersion = value.FileVersion, + EtwActivityId = value.EtwActivityId, + IssuerName = value.IssuerName, + IssuerTBSHash = value.IssuerTBSHash, + NotValidAfter = value.NotValidAfter, + NotValidBefore = value.NotValidBefore, + PublisherName = value.PublisherName, + PublisherTBSHash = value.PublisherTBSHash, + SignatureType = value.SignatureType, + TotalSignatureCount = value.TotalSignatureCount, + VerificationError = value.VerificationError, + Signature = value.Signature, + Hash = value.Hash, + Flags = value.Flags, + PolicyBits = value.PolicyBits + }; + + // Serialize the AdditionalFields object using the source-generated context. + string additionalFieldsJson = JsonSerializer.Serialize( + additional, + MDEAdvancedHuntingJSONSerializationContext.Default.AdditionalFields); + writer.WriteString("AdditionalFields", additionalFieldsJson); + + writer.WriteEndObject(); + } + + private static string? GetStringOrNull(JsonElement element, string propertyName) + { + if (element.TryGetProperty(propertyName, out JsonElement prop)) + { + return prop.ValueKind == JsonValueKind.String ? prop.GetString() : prop.ToString(); + } + return null; + } +} + +// This partial class tells System.Text.Json which types to generate serialization code for. +[JsonSerializable(typeof(MDEAdvancedHuntingDataRootObject))] +[JsonSerializable(typeof(AdditionalFields))] +[JsonSourceGenerationOptions(WriteIndented = true)] +internal sealed partial class MDEAdvancedHuntingJSONSerializationContext : JsonSerializerContext +{ } diff --git a/AppControl Manager/IntelGathering/OptimizeMDECSVData.cs b/AppControl Manager/IntelGathering/OptimizeMDECSVData.cs index af9244f55..5374523c2 100644 --- a/AppControl Manager/IntelGathering/OptimizeMDECSVData.cs +++ b/AppControl Manager/IntelGathering/OptimizeMDECSVData.cs @@ -3,79 +3,63 @@ using System.Linq; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace AppControlManager.IntelGathering; - -// Generates precomputed serialization metadata for Dictionary at compile time, -// avoiding runtime reflection and improving performance for serialization and deserialization. -// Also makes it compatible with Trimming and Native AOT scenarios. -[JsonSerializable(typeof(Dictionary))] -public partial class MyJsonContext : JsonSerializerContext -{ -} - - internal static partial class OptimizeMDECSVData { - - /// /// Public method of this class. - /// Optimizes the MDE CSV data by adding the nested properties in the "AdditionalFields" property to the parent record as first-level properties, all in one class + /// Optimizes the MDE CSV data by adding the nested properties in the "AdditionalFields" property to the parent record as first-level properties, all in one class. /// /// /// public static List Optimize(string CSVFilePath) { List csvRecords = ReadCsv(CSVFilePath); - return csvRecords; } - - /// - /// Converts an entire MDE Advanced Hunting CSV file into a list of classes + /// Converts an entire MDE Advanced Hunting CSV file into a list of classes. /// /// /// /// private static List ReadCsv(string filePath) { - // Create a list to store each CSV row record + // Create a list to store each CSV row record. List records = []; - // Read the CSV file line by line + // Read the CSV file line by line. using StreamReader reader = new(filePath); - // Read the header line which is the first line + // Read the header line which is the first line. string? header = reader.ReadLine() ?? throw new InvalidDataException("CSV file is empty or header is missing."); - // Parse the header line + // Parse the header line. string[] headers = ParseCsvLine(header); - // Map header names to their indices so columns can be located precisely regardless of their positions in the CSV file + // Map header names to their indices so columns can be located precisely regardless of their positions in the CSV file. Dictionary headerMap = headers .Select((name, index) => new { name, index }) .ToDictionary(x => x.name, x => x.index); - // Read the remaining lines of the CSV file until the end of the stream is reached (EOF) + // Read the remaining lines of the CSV file until the end of the stream is reached (EOF). while (!reader.EndOfStream) { - // Read the next line + // Read the next line. string? line = reader.ReadLine(); - // Skip empty lines + // Skip empty lines. if (line is null) continue; - // Split the line by commas + // Split the line by commas. string[] values = ParseCsvLine(line); - // Initialize a new CsvRecord instance - // P.S not all rows have the same properties + // Initialize a new MDEAdvancedHuntingData instance. + // P.S not all rows have the same properties. MDEAdvancedHuntingData record = new() { Timestamp = GetValue(values, headerMap, "Timestamp"), @@ -111,66 +95,64 @@ private static List ReadCsv(string filePath) ReportId = GetValue(values, headerMap, "ReportId") }; - - // Get the JSON string from the CSV which is in the AdditionalFields property + // Get the JSON string from the CSV which is in the AdditionalFields property. string? additionalFieldsString = GetValue(values, headerMap, "AdditionalFields"); - - // Parse the AdditionalFields JSON if it exists - if (additionalFieldsString is not null && !string.IsNullOrWhiteSpace(additionalFieldsString)) + // Parse the AdditionalFields JSON if it exists. + if (!string.IsNullOrWhiteSpace(additionalFieldsString)) { + // Format the JSON string so the next method won't throw an error. + string formattedJSONString = EnsureAllValuesAreQuoted(additionalFieldsString); - // Format the JSON string so the next method won't throw error - string FormattedJSONString = EnsureAllValuesAreQuoted(additionalFieldsString); - - // Deserialize the JSON content into a dictionary using the generated context - Dictionary? additionalFields = JsonSerializer.Deserialize(FormattedJSONString, MyJsonContext.Default.DictionaryStringString); + // Deserialize the JSON content into an AdditionalFields instance using the generated context. + AdditionalFields? additionalFields = JsonSerializer.Deserialize( + formattedJSONString, + MDEAdvancedHuntingJSONSerializationContext.Default.AdditionalFields); if (additionalFields is not null) { - // Populate the new properties from the JSON - record.PolicyID = additionalFields.TryGetValue("PolicyID", out string? PolicyID) ? PolicyID : null; - record.PolicyName = additionalFields.TryGetValue("PolicyName", out string? PolicyName) ? PolicyName : null; - record.RequestedSigningLevel = additionalFields.TryGetValue("Requested Signing Level", out string? RequestedSigningLevel) ? RequestedSigningLevel : null; - record.ValidatedSigningLevel = additionalFields.TryGetValue("Validated Signing Level", out string? ValidatedSigningLevel) ? ValidatedSigningLevel : null; - record.ProcessName = additionalFields.TryGetValue("ProcessName", out string? ProcessName) ? ProcessName : null; - record.StatusCode = additionalFields.TryGetValue("StatusCode", out string? StatusCode) ? StatusCode : null; - record.Sha1FlatHash = additionalFields.TryGetValue("Sha1FlatHash", out string? Sha1FlatHash) ? Sha1FlatHash : null; - record.Sha256FlatHash = additionalFields.TryGetValue("Sha256FlatHash", out string? Sha256FlatHash) ? Sha256FlatHash : null; - record.USN = additionalFields.TryGetValue("USN", out string? USN) ? USN : null; - record.SiSigningScenario = additionalFields.TryGetValue("SiSigningScenario", out string? SiSigningScenario) ? SiSigningScenario : null; - record.PolicyHash = additionalFields.TryGetValue("PolicyHash", out string? PolicyHash) ? PolicyHash : null; - record.PolicyGuid = additionalFields.TryGetValue("PolicyGuid", out string? PolicyGuid) ? PolicyGuid : null; - record.UserWriteable = additionalFields.TryGetValue("UserWriteable", out string? UserWriteable) ? bool.Parse(UserWriteable) : null; - record.OriginalFileName = additionalFields.TryGetValue("OriginalFileName", out string? OriginalFileName) ? OriginalFileName : null; - record.InternalName = additionalFields.TryGetValue("InternalName", out string? InternalName) ? InternalName : null; - record.FileDescription = additionalFields.TryGetValue("FileDescription", out string? FileDescription) ? FileDescription : null; - record.FileVersion = additionalFields.TryGetValue("FileVersion", out string? FileVersion) ? FileVersion : null; - record.EtwActivityId = additionalFields.TryGetValue("EtwActivityId", out string? EtwActivityId) ? EtwActivityId : null; - record.IssuerName = additionalFields.TryGetValue("IssuerName", out string? IssuerName) ? IssuerName : null; - record.IssuerTBSHash = additionalFields.TryGetValue("IssuerTBSHash", out string? IssuerTBSHash) ? IssuerTBSHash : null; - record.NotValidAfter = additionalFields.TryGetValue("NotValidAfter", out string? NotValidAfter) ? NotValidAfter : null; - record.NotValidBefore = additionalFields.TryGetValue("NotValidBefore", out string? NotValidBefore) ? NotValidBefore : null; - record.PublisherName = additionalFields.TryGetValue("PublisherName", out string? PublisherName) ? PublisherName : null; - record.PublisherTBSHash = additionalFields.TryGetValue("PublisherTBSHash", out string? PublisherTBSHash) ? PublisherTBSHash : null; - record.SignatureType = additionalFields.TryGetValue("SignatureType", out string? SignatureType) ? SignatureType : null; - record.TotalSignatureCount = additionalFields.TryGetValue("TotalSignatureCount", out string? TotalSignatureCount) ? TotalSignatureCount : null; - record.VerificationError = additionalFields.TryGetValue("VerificationError", out string? VerificationError) ? VerificationError : null; - record.Signature = additionalFields.TryGetValue("Signature", out string? Signature) ? Signature : null; - record.Hash = additionalFields.TryGetValue("Hash", out string? Hash) ? Hash : null; - record.Flags = additionalFields.TryGetValue("Flags", out string? Flags) ? Flags : null; - record.PolicyBits = additionalFields.TryGetValue("PolicyBits", out string? PolicyBits) ? PolicyBits : null; + // Populate the new properties from the JSON. + record.PolicyID = additionalFields.PolicyID; + record.PolicyName = additionalFields.PolicyName; + record.RequestedSigningLevel = additionalFields.RequestedSigningLevel; + record.ValidatedSigningLevel = additionalFields.ValidatedSigningLevel; + record.ProcessName = additionalFields.ProcessName; + record.StatusCode = additionalFields.StatusCode; + record.Sha1FlatHash = additionalFields.Sha1FlatHash; + record.Sha256FlatHash = additionalFields.Sha256FlatHash; + record.USN = additionalFields.USN; + record.SiSigningScenario = additionalFields.SiSigningScenario; + record.PolicyHash = additionalFields.PolicyHash; + record.PolicyGuid = additionalFields.PolicyGuid; + record.UserWriteable = additionalFields.UserWriteable; + record.OriginalFileName = additionalFields.OriginalFileName; + record.InternalName = additionalFields.InternalName; + record.FileDescription = additionalFields.FileDescription; + record.FileVersion = additionalFields.FileVersion; + record.EtwActivityId = additionalFields.EtwActivityId; + record.IssuerName = additionalFields.IssuerName; + record.IssuerTBSHash = additionalFields.IssuerTBSHash; + record.NotValidAfter = additionalFields.NotValidAfter; + record.NotValidBefore = additionalFields.NotValidBefore; + record.PublisherName = additionalFields.PublisherName; + record.PublisherTBSHash = additionalFields.PublisherTBSHash; + record.SignatureType = additionalFields.SignatureType; + record.TotalSignatureCount = additionalFields.TotalSignatureCount; + record.VerificationError = additionalFields.VerificationError; + record.Signature = additionalFields.Signature; + record.Hash = additionalFields.Hash; + record.Flags = additionalFields.Flags; + record.PolicyBits = additionalFields.PolicyBits; } } - // Add the populated record to the list + // Add the populated record to the list. records.Add(record); } return records; } - /// /// Ensures the JSON string is well formatted. If a field has no double quotes, it will add them around it. /// @@ -178,19 +160,16 @@ private static List ReadCsv(string filePath) /// private static string EnsureAllValuesAreQuoted(string jsonString) { - // Regex to match unquoted values that are not inside quotes + // Regex to match unquoted values that are not inside quotes. Regex regex = JsonFixerRegex(); - // Replace the matched unquoted values with the same value wrapped in double quotes + // Replace the matched unquoted values with the same value wrapped in double quotes. string result = regex.Replace(jsonString, match => $"\"{match.Value.Trim()}\""); - return result; } - - /// - /// Parses each line/row of the CSV file + /// Parses each line/row of the CSV file. /// /// /// @@ -200,57 +179,55 @@ private static string[] ParseCsvLine(string line) StringBuilder currentField = new(); bool inQuotes = false; - // Iterate through each character in the line + // Iterate through each character in the line. for (int i = 0; i < line.Length; i++) { char c = line[i]; - // Handle quotes + // Handle quotes. if (c == '"') { // Handle escaped quotes ("") if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') { - _ = currentField.Append('"'); // Append a single quote if it's an escape sequence - i++; // Skip the next quote + _ = currentField.Append('"'); // Append a single quote if it's an escape sequence. + i++; // Skip the next quote. } else { - inQuotes = !inQuotes; // Toggle the inQuotes flag + inQuotes = !inQuotes; // Toggle the inQuotes flag. } } - - // Handle commas + // Handle commas. else if (c == ',' && !inQuotes) { - // When we hit a comma outside of quotes, the field is complete + // When we hit a comma outside of quotes, the field is complete. fields.Add(currentField.ToString()); _ = currentField.Clear(); } - else { - // Add characters to the current field + // Add characters to the current field. _ = currentField.Append(c); } } - // Add the last field to the list + // Add the last field to the list. fields.Add(currentField.ToString()); - // Clean up: Remove the outermost quotes for non-JSON fields - // If a field has more than one set/pair of double quotes around it, only one pair will be removed + // Clean up: Remove the outermost quotes for non-JSON fields. + // If a field has more than one set/pair of double quotes around it, only one pair will be removed. for (int i = 0; i < fields.Count; i++) { string field = fields[i]; - // If the field is a JSON field, we don't want to remove quotes + // If the field is a JSON field, we don't want to remove quotes. if (field.StartsWith('{') && field.EndsWith('}')) { - continue; // Skip JSON fields + continue; // Skip JSON fields. } - // Remove leading and trailing quotes if they exist (for non-JSON fields) + // Remove leading and trailing quotes if they exist (for non-JSON fields). if (field.StartsWith('"') && field.EndsWith('"') && field.Length > 1) { fields[i] = field[1..^1]; @@ -260,10 +237,9 @@ private static string[] ParseCsvLine(string line) return [.. fields]; } - /// - /// Gets the value of a column from the CSV row and returns it - /// Returns null if the column does not exist or the value is empty + /// Gets the value of a column from the CSV row and returns it. + /// Returns null if the column does not exist or the value is empty. /// /// /// @@ -278,7 +254,6 @@ private static string[] ParseCsvLine(string line) return null; } - // 1. (?<=:) // Positive Lookbehind: Asserts that the match must be preceded by a colon `:`. // Ensures that we're matching a value that appears immediately after a key-value colon. @@ -288,7 +263,7 @@ private static string[] ParseCsvLine(string line) // Allows for optional spaces between the colon and the value. // // 3. (?!\"\") - // Negative Lookahead: Ensures that the match is NOT followed by a double quote `"`. + // Negative Lookahead: Ensures that the match is NOT followed by a double quote `"` . // This prevents already quoted values from being matched. // // 4. ([^\"",\s]+) @@ -303,10 +278,9 @@ private static string[] ParseCsvLine(string line) // // Summary: // Some MDE AH AdditionalFields JSON content have unquoted fields, this takes care of them. - // The regex Will Fail if the field that is not quoted contains a comma(s), space(s) or double quote(s) in it, before the comma that marks the end of the field. + // The regex will fail if the field that is not quoted contains a comma(s), space(s) or double quote(s) + // in it before the comma that marks the end of the field. // This is because the regex is designed to match unquoted fields that are single words/digits. - // [GeneratedRegex(@"(?<=:)\s*(?!\"")([^\"",\s]+)(?=\s*,|\s*})", RegexOptions.Compiled)] private static partial Regex JsonFixerRegex(); - } diff --git a/AppControl Manager/Others/Intune.cs b/AppControl Manager/Others/MicrosoftGraph.cs similarity index 69% rename from AppControl Manager/Others/Intune.cs rename to AppControl Manager/Others/MicrosoftGraph.cs index 2cfa4366f..f6853c799 100644 --- a/AppControl Manager/Others/Intune.cs +++ b/AppControl Manager/Others/MicrosoftGraph.cs @@ -10,26 +10,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; -using static AppControlManager.Others.Intune; +using static AppControlManager.Others.MicrosoftGraph; namespace AppControlManager.Others; -internal static class Intune +internal static class MicrosoftGraph { - + // For Microsoft Graph Command Line Tools private const string ClientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e"; - // Scopes required to create and assign device configurations - // https://learn.microsoft.com/en-us/graph/permissions-reference - private static readonly string[] Scopes = [ - "Group.Read.All", // For Groups enumeration - "DeviceManagementConfiguration.ReadWrite.All" // For uploading policy - ]; - private const string DeviceConfigurationsURL = "https://graph.microsoft.com/v1.0/deviceManagement/deviceConfigurations"; - private static AuthenticationResult? authenticationResult; - // Initialize the Public Client Application private static readonly IPublicClientApplication App = PublicClientApplicationBuilder.Create(ClientId) .WithAuthority(AzureCloudInstance.AzurePublic, "common") @@ -45,6 +36,115 @@ internal static class Intune // To manage Sign in cancellation private static CancellationTokenSource? _cts; + // Used to determine which scope to use + internal enum AuthenticationContext + { + Intune, + MDEAdvancedHunting + } + + // The correlation between scopes and required permissions + private static readonly Dictionary Scopes = new() { + + // Scopes required to create and assign device configurations for Intune + // https://learn.microsoft.com/en-us/graph/permissions-reference + { AuthenticationContext.Intune, [ + "Group.Read.All", // For Groups enumeration + "DeviceManagementConfiguration.ReadWrite.All" // For uploading policy + ]}, + + // Scopes required to retrieve MDE Advanced Hunting results + // https://learn.microsoft.com/en-us/graph/api/security-security-runhuntingquery + {AuthenticationContext.MDEAdvancedHunting, ["ThreatHunting.Read.All"]} + }; + + // This class defines every account that is authenticated by the user + private sealed class AccountIdentity + { + internal required string AccountIdentifier { get; set; } + internal required string Username { get; set; } + internal required string TenantID { get; set; } + internal required AuthenticationResult AuthResult { get; set; } + internal required IAccount Account { get; set; } + } + + // A dictionary to keep the record of all of the authenticated accounts + private static readonly Dictionary SavedAccounts = []; + + + /// + /// Perform an Advanced Hunting query using Microsoft Defender for Endpoint + /// Accepts a device name as an optional parameter for filtering + /// + /// + /// + internal static async Task RunMDEAdvancedHuntingQuery(string? deviceName) + { + if (!SavedAccounts.TryGetValue(AuthenticationContext.MDEAdvancedHunting, out AccountIdentity? account)) + { + throw new InvalidOperationException("You need to authenticate first."); + } + + using SecHttpClient httpClient = new(); + + string? output = null; + + // Set up the HTTP headers + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", account.AuthResult.AccessToken); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + QueryPayload queryPayload; + + if (string.IsNullOrWhiteSpace(deviceName)) + { + // Defining the query + queryPayload = new() + { + Query = """ +DeviceEvents +| where ActionType startswith "AppControlCodeIntegrity" + or ActionType startswith "AppControlCIScriptBlocked" + or ActionType startswith "AppControlCIScriptAudited" +""" + }; + } + else + { + queryPayload = new() + { + Query = $""" +DeviceEvents +| where (ActionType startswith "AppControlCodeIntegrity" + or ActionType startswith "AppControlCIScriptBlocked" + or ActionType startswith "AppControlCIScriptAudited") + and DeviceName == "{deviceName}" +""" + }; + } + + string jsonPayload = JsonSerializer.Serialize(queryPayload, IntuneJsonContext.Default.QueryPayload); + + using StringContent content = new(jsonPayload, Encoding.UTF8, "application/json"); + + // Make the POST request + HttpResponseMessage response = await httpClient.PostAsync(new Uri("https://graph.microsoft.com/v1.0/security/runHuntingQuery"), content); + + if (response.IsSuccessStatusCode) + { + output = await response.Content.ReadAsStringAsync(); + Logger.Write("MDE Advanced Hunting Query has been Successful."); + + return output; + } + else + { + Logger.Write($"Failed to run MDE Advanced Hunting Query. Status code: {response.StatusCode}"); + string errorContent = await response.Content.ReadAsStringAsync(); + throw new InvalidOperationException($"Error details: {errorContent}"); + } + } + + /// /// Fetches the security groups /// @@ -52,7 +152,7 @@ internal static class Intune /// internal static async Task FetchGroups() { - if (authenticationResult is null) + if (!SavedAccounts.TryGetValue(AuthenticationContext.Intune, out AccountIdentity? account)) { throw new InvalidOperationException("You need to authenticate first."); } @@ -60,7 +160,7 @@ internal static async Task FetchGroups() using SecHttpClient httpClient = new(); // Set up the HTTP headers - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", account.AuthResult.AccessToken); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Make the request to get all groups @@ -118,42 +218,56 @@ internal static Dictionary GetGroups() /// Signs into a tenant /// /// - internal static async Task SignIn() + internal static async Task SignIn(AuthenticationContext context) { - authenticationResult = null; + AuthenticationResult? authResult = null; - // Attempt silent authentication - IEnumerable accounts = await App.GetAccountsAsync(); + bool error = false; - try - { - authenticationResult = await App.AcquireTokenSilent(Scopes, accounts.FirstOrDefault()) - .ExecuteAsync(); - } - catch (MsalUiRequiredException) + if (SavedAccounts.TryGetValue(context, out AccountIdentity? account)) { + IEnumerable accounts = await App.GetAccountsAsync(); - // Fall back to interactive login if silent authentication fails - _cts = new CancellationTokenSource(); - - try - { - authenticationResult = await App.AcquireTokenInteractive(Scopes) - .WithPrompt(Prompt.SelectAccount) - .WithUseEmbeddedWebView(false) - .ExecuteAsync(_cts.Token); - } - catch (OperationCanceledException) + if (accounts.Any(x => x == account.Account)) { - throw new OperationCanceledException("The operation was canceled after 1 minute. Browser might have been closed or timeout occurred."); + Logger.Write($"Account with the UserName {account.Username} for the context {context} is already authenticated"); + return; } - finally + } + + // Fall back to interactive login if silent authentication fails + _cts = new CancellationTokenSource(); + + try + { + authResult = await App.AcquireTokenInteractive(Scopes[context]) + .WithPrompt(Prompt.SelectAccount) + .WithUseEmbeddedWebView(false) + .ExecuteAsync(_cts.Token); + } + catch (OperationCanceledException) + { + error = true; + throw new OperationCanceledException("The operation was canceled after 1 minute. Browser might have been closed or timeout occurred."); + } + finally + { + if (!error && authResult is not null) { - _cts.Dispose(); - _cts = null; // Reset for future operations + // Add the account that was successfully authenticated to the dictionary + SavedAccounts[context] = new AccountIdentity + { + AuthResult = authResult, + AccountIdentifier = authResult.Account.HomeAccountId.Identifier, + Username = authResult.Account.Username, + TenantID = authResult.TenantId, + Account = authResult.Account + }; } + _cts.Dispose(); + _cts = null; // Reset for future operations } } @@ -174,29 +288,21 @@ internal static void CancelSignIn() /// Signs out the user /// /// - internal static async Task SignOut() + internal static async Task SignOut(AuthenticationContext context) { - if (authenticationResult is null) + if (SavedAccounts.TryGetValue(context, out AccountIdentity? account)) { - Logger.Write("No user is currently signed in."); - return; + await App.RemoveAsync(account.Account); + Logger.Write($"Signed out account: {account.Username}"); + if (!SavedAccounts.Remove(context)) + { + throw new InvalidOperationException($"Failed to remove the account with the username {account.Username} from the saved accounts."); + } } - - - // Get the cached accounts - IEnumerable accounts = await App.GetAccountsAsync(); - - foreach (IAccount account in accounts) + else { - await App.RemoveAsync(account); - Logger.Write($"Signed out account: {account.Username}"); + Logger.Write($"No user is currently signed in for the context {context}."); } - - // Clear the current authentication result - authenticationResult = null; - - Logger.Write("All accounts signed out successfully."); - } @@ -212,6 +318,11 @@ internal static async Task SignOut() internal static async Task UploadPolicyToIntune(string policyPath, string? groupName, string? policyName, string policyID) { + if (!SavedAccounts.TryGetValue(AuthenticationContext.Intune, out AccountIdentity? account)) + { + throw new InvalidOperationException("You need to authenticate first."); + } + DirectoryInfo stagingArea = StagingArea.NewStagingArea("IntuneCIPUpload"); string tempPolicyPath = Path.Combine(stagingArea.FullName, "policy.bin"); @@ -221,22 +332,16 @@ internal static async Task UploadPolicyToIntune(string policyPath, string? group // https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/deployment/deploy-appcontrol-policies-using-intune#deploy-app-control-policies-with-custom-oma-uri string base64String = ConvertBinFileToBase64(tempPolicyPath, 350000); - if (authenticationResult is null) - { - throw new InvalidOperationException("You need to authenticate first"); - } - // Call Microsoft Graph API to create the custom policy - string? policyId = await CreateCustomPolicy(authenticationResult.AccessToken, base64String, policyName, policyID); + string? policyId = await CreateCustomIntunePolicy(account.AuthResult.AccessToken, base64String, policyName, policyID); Logger.Write($"{policyId} is the ID of the policy that was created"); - if (!string.IsNullOrWhiteSpace(groupName) && policyId is not null) { if (Groups.TryGetValue(groupName, out string? groupId)) { - await AssignPolicyToGroup(policyId, authenticationResult.AccessToken, groupId); + await AssignIntunePolicyToGroup(policyId, account.AuthResult.AccessToken, groupId); } } @@ -253,7 +358,7 @@ internal static async Task UploadPolicyToIntune(string policyPath, string? group /// /// /// - private static async Task AssignPolicyToGroup(string policyId, string accessToken, string groupID) + private static async Task AssignIntunePolicyToGroup(string policyId, string accessToken, string groupID) { using SecHttpClient httpClient = new(); @@ -305,7 +410,7 @@ private static async Task AssignPolicyToGroup(string policyId, string accessToke /// /// /// - private static async Task CreateCustomPolicy(string accessToken, string policyData, string? policyName, string policyID) + private static async Task CreateCustomIntunePolicy(string accessToken, string policyData, string? policyName, string policyID) { string descriptionText = $"Application Control Policy Uploaded from AppControl Manager on {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss 'UTC'}"; @@ -497,6 +602,12 @@ public sealed class AssignmentPayload public Dictionary? Target { get; set; } } + public sealed class QueryPayload + { + [JsonPropertyName("Query")] + public string? Query { get; set; } + } + private static string ConvertBinFileToBase64(string filePath, int maxSizeInBytes) { @@ -519,6 +630,7 @@ private static string ConvertBinFileToBase64(string filePath, int maxSizeInBytes [JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(AssignmentPayload))] +[JsonSerializable(typeof(QueryPayload))] [JsonSerializable(typeof(Windows10CustomConfiguration))] [JsonSerializable(typeof(OmaSettingBase64))] internal sealed partial class IntuneJsonContext : JsonSerializerContext diff --git a/AppControl Manager/Package.appxmanifest b/AppControl Manager/Package.appxmanifest index db188d95d..1f9c2f4cc 100644 --- a/AppControl Manager/Package.appxmanifest +++ b/AppControl Manager/Package.appxmanifest @@ -11,7 +11,7 @@ + Version="1.8.8.0" /> diff --git a/AppControl Manager/Pages/AllowNewApps/AllowNewAppsEventLogsDataGrid.xaml b/AppControl Manager/Pages/AllowNewApps/AllowNewAppsEventLogsDataGrid.xaml index 2fae8aa45..7bc00a353 100644 --- a/AppControl Manager/Pages/AllowNewApps/AllowNewAppsEventLogsDataGrid.xaml +++ b/AppControl Manager/Pages/AllowNewApps/AllowNewAppsEventLogsDataGrid.xaml @@ -77,7 +77,7 @@ - + - + - + diff --git a/AppControl Manager/Pages/BuildNewCertificate.xaml b/AppControl Manager/Pages/BuildNewCertificate.xaml index 183d34b1e..d50fae503 100644 --- a/AppControl Manager/Pages/BuildNewCertificate.xaml +++ b/AppControl Manager/Pages/BuildNewCertificate.xaml @@ -47,7 +47,7 @@ - + diff --git a/AppControl Manager/Pages/ConfigurePolicyRuleOptions.xaml b/AppControl Manager/Pages/ConfigurePolicyRuleOptions.xaml index e36952073..a5cc0fb47 100644 --- a/AppControl Manager/Pages/ConfigurePolicyRuleOptions.xaml +++ b/AppControl Manager/Pages/ConfigurePolicyRuleOptions.xaml @@ -33,7 +33,7 @@ - + diff --git a/AppControl Manager/Pages/CreateDenyPolicy.xaml b/AppControl Manager/Pages/CreateDenyPolicy.xaml index e23ad08de..adc4f0b49 100644 --- a/AppControl Manager/Pages/CreateDenyPolicy.xaml +++ b/AppControl Manager/Pages/CreateDenyPolicy.xaml @@ -70,7 +70,7 @@ - + @@ -344,7 +344,7 @@ Style="{StaticResource AccentButtonStyle}" Margin="0,0,15,0" ToolTipService.Tool - + @@ -88,7 +88,7 @@ - + - + diff --git a/AppControl Manager/Pages/CreateSupplementalPolicy.xaml b/AppControl Manager/Pages/CreateSupplementalPolicy.xaml index 5aa817295..2e5e09915 100644 --- a/AppControl Manager/Pages/CreateSupplementalPolicy.xaml +++ b/AppControl Manager/Pages/CreateSupplementalPolicy.xaml @@ -73,7 +73,7 @@ - + @@ -741,7 +741,7 @@ - + - + diff --git a/AppControl Manager/Pages/Deployment.xaml.cs b/AppControl Manager/Pages/Deployment.xaml.cs index 0b26377d4..7f988f05d 100644 --- a/AppControl Manager/Pages/Deployment.xaml.cs +++ b/AppControl Manager/Pages/Deployment.xaml.cs @@ -650,7 +650,7 @@ private async void IntuneSignInButton_Click(object sender, RoutedEventArgs e) IntuneSignInButton.IsEnabled = false; - await Intune.SignIn(); + await MicrosoftGraph.SignIn(MicrosoftGraph.AuthenticationContext.Intune); StatusInfoBar.Message = "Successfully signed into Intune"; StatusInfoBar.Severity = InfoBarSeverity.Success; @@ -715,7 +715,7 @@ private async void IntuneSignOutButton_Click(object sender, RoutedEventArgs e) IntuneSignOutButton.IsEnabled = false; - await Intune.SignOut(); + await MicrosoftGraph.SignOut(MicrosoftGraph.AuthenticationContext.Intune); signOutSuccessful = true; @@ -754,8 +754,8 @@ private async void IntuneSignOutButton_Click(object sender, RoutedEventArgs e) private async void RefreshIntuneGroupsButton_Click(object sender, RoutedEventArgs e) { - await Intune.FetchGroups(); - Dictionary groups = Intune.GetGroups(); + await MicrosoftGraph.FetchGroups(); + Dictionary groups = MicrosoftGraph.GetGroups(); // Update the ComboBox with group names IntuneGroupsComboBox.ItemsSource = groups.Keys; @@ -798,7 +798,7 @@ await Task.Run(() => }); - await Intune.UploadPolicyToIntune(file, groupID, policyName, policyID); + await MicrosoftGraph.UploadPolicyToIntune(file, groupID, policyName, policyID); } @@ -809,7 +809,7 @@ await Task.Run(() => /// private void IntuneCancelSignInButton_Click(object sender, RoutedEventArgs e) { - Intune.CancelSignIn(); + MicrosoftGraph.CancelSignIn(); } private void BrowseForXMLPolicyFilesButton_RightTapped(object sender, RightTappedRoutedEventArgs e) diff --git a/AppControl Manager/Pages/EventLogsPolicyCreation.xaml b/AppControl Manager/Pages/EventLogsPolicyCreation.xaml index 5ae2f7cd9..07c3c25ae 100644 --- a/AppControl Manager/Pages/EventLogsPolicyCreation.xaml +++ b/AppControl Manager/Pages/EventLogsPolicyCreation.xaml @@ -113,7 +113,7 @@ MinWidth="400" IsReadOnly="True" /> - + @@ -156,7 +156,7 @@ MinWidth="400" IsReadOnly="True" /> - + @@ -185,7 +185,7 @@ MinWidth="400" IsReadOnly="True" /> Spacing="8"> + SelectedIndex="1" SelectionChanged="SegmentedControl_SelectionChanged"> - + - /// Changes the main button's text that creates the policy, based on the selected method of creation - /// - /// - private void CreatePolicyButtonTextChange(string text) - { - CreatePolicyButton.Content = text; - } - - /// /// The button that browses for XML file the logs will be added to /// @@ -704,8 +694,6 @@ private void AddToPolicyButton_Click(object sender, RoutedEventArgs e) PolicyToAddLogsTo = selectedFile; Logger.Write($"Selected {PolicyToAddLogsTo} to add the logs to."); - - CreatePolicyButtonTextChange("Add logs to the selected policy"); } } @@ -718,7 +706,6 @@ private void AddToPolicyButton_Click(object sender, RoutedEventArgs e) /// private void BasePolicyFileButton_Click(object sender, RoutedEventArgs e) { - string? selectedFile = FileDialogHelper.ShowFilePickerDialog(GlobalVars.XMLFilePickerFilter); if (!string.IsNullOrEmpty(selectedFile)) @@ -727,14 +714,10 @@ private void BasePolicyFileButton_Click(object sender, RoutedEventArgs e) BasePolicyXMLFile = selectedFile; Logger.Write($"Selected {BasePolicyXMLFile} to associate the Supplemental policy with."); - - CreatePolicyButtonTextChange("Create Policy for Selected Base"); } - } - /// /// The button to submit a base policy GUID that will be used to set the base policy ID in the Supplemental policy file that will be created. /// @@ -746,8 +729,6 @@ private void BaseGUIDSubmitButton_Click(object sender, RoutedEventArgs e) if (Guid.TryParse(BaseGUIDTextBox.Text, out Guid guid)) { BasePolicyGUID = guid; - - CreatePolicyButtonTextChange("Create Policy for Base GUID"); } else { @@ -790,7 +771,6 @@ private async void CreatePolicyButton_Click(SplitButton sender, SplitButtonClick throw new InvalidOperationException("You must select an option from the policy creation list"); } - // Create a policy name if it wasn't provided DateTime now = DateTime.Now; string formattedDate = now.ToString("MM-dd-yyyy 'at' HH-mm-ss"); @@ -806,7 +786,6 @@ private async void CreatePolicyButton_Click(SplitButton sender, SplitButtonClick } - // All of the File Identities that will be used to put in the policy XML file List SelectedLogs = []; @@ -827,11 +806,12 @@ private async void CreatePolicyButton_Click(SplitButton sender, SplitButtonClick } - // If user selected to deploy the policy // Need to retrieve it while we're still at the UI thread bool DeployAtTheEnd = DeployPolicyToggle.IsChecked; + // See which section of the Segmented control is selected for policy creation + int selectedCreationMethod = segmentedControl.SelectedIndex; await Task.Run(() => { @@ -848,112 +828,127 @@ await Task.Run(() => // Insert the data into the empty policy file Master.Initiate(DataPackage, EmptyPolicyPath, SiPolicyIntel.Authorization.Allow); - - - if (PolicyToAddLogsTo is not null) + switch (selectedCreationMethod) { + case 0: + { + if (PolicyToAddLogsTo is not null) + { + // Set policy name and reset the policy ID of our new policy + string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, null, null); + + // Remove all policy rule options prior to merging the policies since we don't need to add/remove any policy rule options to/from the user input policy + CiRuleOptions.Set(filePath: EmptyPolicyPath, RemoveAll: true); + + // Merge the created policy with the user-selected policy which will result in adding the new rules to it + SiPolicy.Merger.Merge(PolicyToAddLogsTo, [EmptyPolicyPath]); + + UpdateHvciOptions.Update(PolicyToAddLogsTo); + + // If user selected to deploy the policy + if (DeployAtTheEnd) + { + string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); + + PolicyToCIPConverter.Convert(PolicyToAddLogsTo, CIPPath); + + CiToolHelper.UpdatePolicy(CIPPath); + } + } + else + { + throw new InvalidOperationException("No policy file was selected to add the logs to."); + } + + break; + } + case 1: + { + if (BasePolicyXMLFile is not null) + { + string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); + + // Instantiate the user selected Base policy - To get its BasePolicyID + CodeIntegrityPolicy codeIntegrityPolicy = new(BasePolicyXMLFile, null); + + // Set the BasePolicyID of our new policy to the one from user selected policy + string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, codeIntegrityPolicy.BasePolicyID, null); + + // Configure policy rule options + CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); + + // Set policy version + SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); + + // Copying the policy file to the User Config directory - outside of the temporary staging area + File.Copy(EmptyPolicyPath, OutputPath, true); + + // If user selected to deploy the policy + if (DeployAtTheEnd) + { + string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); + + PolicyToCIPConverter.Convert(OutputPath, CIPPath); + + CiToolHelper.UpdatePolicy(CIPPath); + } + } + else + { + throw new InvalidOperationException("No policy file was selected to associate the Supplemental policy with."); + } + + break; + } + case 2: + { + + if (BasePolicyGUID is not null) + { + string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); + + // Set the BasePolicyID of our new policy to the one supplied by user + string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, BasePolicyGUID.ToString(), null); + + // Configure policy rule options + CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); + + // Set policy version + SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); + + // Copying the policy file to the User Config directory - outside of the temporary staging area + File.Copy(EmptyPolicyPath, OutputPath, true); + + // If user selected to deploy the policy + if (DeployAtTheEnd) + { + string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); + + PolicyToCIPConverter.Convert(OutputPath, CIPPath); + + CiToolHelper.UpdatePolicy(CIPPath); + } + } + else + { + throw new InvalidOperationException("No Base Policy GUID was provided to use as the BasePolicyID of the supplemental policy."); + } - // Set policy name and reset the policy ID of our new policy - string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, null, null); - - // Remove all policy rule options prior to merging the policies since we don't need to add/remove any policy rule options to/from the user input policy - CiRuleOptions.Set(filePath: EmptyPolicyPath, RemoveAll: true); - - // Merge the created policy with the user-selected policy which will result in adding the new rules to it - SiPolicy.Merger.Merge(PolicyToAddLogsTo, [EmptyPolicyPath]); - - UpdateHvciOptions.Update(PolicyToAddLogsTo); - - - // If user selected to deploy the policy - if (DeployAtTheEnd) - { - - string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); - - PolicyToCIPConverter.Convert(PolicyToAddLogsTo, CIPPath); - - CiToolHelper.UpdatePolicy(CIPPath); - - } + break; + } + default: + { + break; + } } - else if (BasePolicyXMLFile is not null) - { - string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); - - // Instantiate the user selected Base policy - To get its BasePolicyID - CodeIntegrityPolicy codeIntegrityPolicy = new(BasePolicyXMLFile, null); - - // Set the BasePolicyID of our new policy to the one from user selected policy - string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, codeIntegrityPolicy.BasePolicyID, null); - - // Configure policy rule options - CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); - - // Set policy version - SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); - - // Copying the policy file to the User Config directory - outside of the temporary staging area - File.Copy(EmptyPolicyPath, OutputPath, true); - - - // If user selected to deploy the policy - if (DeployAtTheEnd) - { - - string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); - - PolicyToCIPConverter.Convert(OutputPath, CIPPath); - - CiToolHelper.UpdatePolicy(CIPPath); - - } - - } - else if (BasePolicyGUID is not null) - { - string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); - - - // Set the BasePolicyID of our new policy to the one supplied by user - string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, BasePolicyGUID.ToString(), null); - - - // Configure policy rule options - CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); - - - // Set policy version - SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); - - // Copying the policy file to the User Config directory - outside of the temporary staging area - File.Copy(EmptyPolicyPath, OutputPath, true); - - - // If user selected to deploy the policy - if (DeployAtTheEnd) - { - - string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); - - PolicyToCIPConverter.Convert(OutputPath, CIPPath); - - CiToolHelper.UpdatePolicy(CIPPath); - - } - - } - - }); } finally { - // Enable the policy creator button again CreatePolicyButton.IsEnabled = true; @@ -964,7 +959,6 @@ await Task.Run(() => // Display the progress ring on the ScanLogs button ScanLogsProgressRing.IsActive = false; ScanLogsProgressRing.Visibility = Visibility.Collapsed; - } } @@ -1015,4 +1009,22 @@ private void BrowseForAppLockerEVTXFilesButton_Holding(object sender, HoldingRou if (!SelectedAppLockerEVTXFilesFlyout.IsOpen) SelectedAppLockerEVTXFilesFlyout.ShowAt(BrowseForEVTXDropDownButton); } + + + /// + /// Event handler for for the segmented button's selection change + /// + /// + /// + private void SegmentedControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + CreatePolicyButton.Content = segmentedControl.SelectedIndex switch + { + 0 => "Add logs to the selected policy", + 1 => "Create Policy for Selected Base", + 2 => "Create Policy for Base GUID", + _ => "Create Policy" + }; + + } } diff --git a/AppControl Manager/Pages/GetCIHashes.xaml b/AppControl Manager/Pages/GetCIHashes.xaml index 19705fb6c..1ed7d5c1a 100644 --- a/AppControl Manager/Pages/GetCIHashes.xaml +++ b/AppControl Manager/Pages/GetCIHashes.xaml @@ -17,7 +17,7 @@ - + diff --git a/AppControl Manager/Pages/GetSecurePolicySettings.xaml b/AppControl Manager/Pages/GetSecurePolicySettings.xaml index db7fe84dd..36c1cb877 100644 --- a/AppControl Manager/Pages/GetSecurePolicySettings.xaml +++ b/AppControl Manager/Pages/GetSecurePolicySettings.xaml @@ -31,7 +31,7 @@ - + diff --git a/AppControl Manager/Pages/Logs.xaml b/AppControl Manager/Pages/Logs.xaml index 9590b8edd..e169aae40 100644 --- a/AppControl Manager/Pages/Logs.xaml +++ b/AppControl Manager/Pages/Logs.xaml @@ -17,7 +17,7 @@ - + diff --git a/AppControl Manager/Pages/MDEAHPolicyCreation.xaml b/AppControl Manager/Pages/MDEAHPolicyCreation.xaml index 1a53853de..9735d88bb 100644 --- a/AppControl Manager/Pages/MDEAHPolicyCreation.xaml +++ b/AppControl Manager/Pages/MDEAHPolicyCreation.xaml @@ -53,6 +53,8 @@ + + @@ -70,7 +72,7 @@ - + @@ -89,15 +91,41 @@ + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + - @@ -275,15 +349,6 @@ Spacing="8"> - - - - - - - - FilePublisher Publisher Hash + + - + _selectedItem == SelectorBarItemMain; + public bool IsCloudSelected => _selectedItem == SelectorBarItemCloud; + public bool IsCreateSelected => _selectedItem == SelectorBarItemCreate; + + private void MenuSelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) + { + _selectedItem = sender.SelectedItem; + OnPropertyChanged(nameof(IsLocalSelected)); + OnPropertyChanged(nameof(IsCloudSelected)); + OnPropertyChanged(nameof(IsCreateSelected)); + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + + // To store the FileIdentities displayed on the DataGrid // Binding happens on the XAML but methods related to search update the ItemSource of the DataGrid from code behind otherwise there will not be an expected result internal ObservableCollection FileIdentities { get; set; } @@ -40,12 +68,13 @@ public sealed partial class MDEAHPolicyCreation : Page // The user selected scan level private ScanLevels scanLevel = ScanLevels.FilePublisher; - - public MDEAHPolicyCreation() { this.InitializeComponent(); + // Default selection for the toolbar menu's selector bar + _selectedItem = SelectorBarItemMain; + // Make sure navigating to/from this page maintains its state this.NavigationCacheMode = NavigationCacheMode.Required; @@ -57,7 +86,6 @@ public MDEAHPolicyCreation() FilterByDateCalendarPicker.DateChanged += FilterByDateCalendarPicker_DateChanged; } - #region // Without the following steps, when the user begins data fetching process and then navigates away from this page @@ -230,12 +258,10 @@ await Task.Run(() => // Enable the button again ScanLogs.IsEnabled = true; - // Stop displaying the Progress Ring ScanLogsProgressRing.IsActive = false; ScanLogsProgressRing.Visibility = Visibility.Collapsed; - // Enable the Policy creator button again CreatePolicyButton.IsEnabled = true; } @@ -484,7 +510,6 @@ private void DataGridFlyoutMenuCopy_Click(object sender, RoutedEventArgs e) // Append each row's data with property labels _ = dataBuilder.AppendLine(ConvertRowToText(selectedItem)); _ = dataBuilder.AppendLine(new string('-', 50)); // Separator between rows - } // Create a DataPackage and set the formatted text as the content @@ -527,7 +552,6 @@ private static string ConvertRowToText(FileIdentity row) } - /// /// Event handler for the Copy Individual Items SubMenu. Populates items in the flyout of the data grid. /// @@ -669,16 +693,6 @@ private void UpdateTotalLogs(bool? Zero = null) } - /// - /// Changes the main button's text that creates the policy, based on the selected method of creation - /// - /// - private void CreatePolicyButtonTextChange(string text) - { - CreatePolicyButton.Content = text; - } - - /// /// The button that browses for XML file the logs will be added to /// @@ -686,7 +700,6 @@ private void CreatePolicyButtonTextChange(string text) /// private void AddToPolicyButton_Click(object sender, RoutedEventArgs e) { - string? selectedFile = FileDialogHelper.ShowFilePickerDialog(GlobalVars.XMLFilePickerFilter); if (!string.IsNullOrEmpty(selectedFile)) @@ -695,10 +708,7 @@ private void AddToPolicyButton_Click(object sender, RoutedEventArgs e) PolicyToAddLogsTo = selectedFile; Logger.Write($"Selected {PolicyToAddLogsTo} to add the logs to."); - - CreatePolicyButtonTextChange("Add logs to the selected policy"); } - } @@ -709,7 +719,6 @@ private void AddToPolicyButton_Click(object sender, RoutedEventArgs e) /// private void BasePolicyFileButton_Click(object sender, RoutedEventArgs e) { - string? selectedFile = FileDialogHelper.ShowFilePickerDialog(GlobalVars.XMLFilePickerFilter); if (!string.IsNullOrEmpty(selectedFile)) @@ -718,10 +727,7 @@ private void BasePolicyFileButton_Click(object sender, RoutedEventArgs e) BasePolicyXMLFile = selectedFile; Logger.Write($"Selected {BasePolicyXMLFile} to associate the Supplemental policy with."); - - CreatePolicyButtonTextChange("Create Policy for Selected Base"); } - } @@ -737,14 +743,11 @@ private void BaseGUIDSubmitButton_Click(object sender, RoutedEventArgs e) if (Guid.TryParse(BaseGUIDTextBox.Text, out Guid guid)) { BasePolicyGUID = guid; - - CreatePolicyButtonTextChange("Create Policy for Base GUID"); } else { throw new ArgumentException("Invalid GUID"); } - } @@ -769,13 +772,11 @@ private async void CreatePolicyButton_Click(SplitButton sender, SplitButtonClick ScanLogsProgressRing.IsActive = true; ScanLogsProgressRing.Visibility = Visibility.Visible; - if (FileIdentities.Count is 0) { throw new InvalidOperationException("There are no logs. Use the scan button first or adjust the filters."); } - if (PolicyToAddLogsTo is null && BasePolicyXMLFile is null && BasePolicyGUID is null) { throw new InvalidOperationException("You must select an option from the policy creation list"); @@ -797,7 +798,6 @@ private async void CreatePolicyButton_Click(SplitButton sender, SplitButtonClick } - // All of the File Identities that will be used to put in the policy XML file List SelectedLogs = []; @@ -818,11 +818,12 @@ private async void CreatePolicyButton_Click(SplitButton sender, SplitButtonClick } - // If user selected to deploy the policy // Need to retrieve it while we're still at the UI thread bool DeployAtTheEnd = DeployPolicyToggle.IsChecked; + // See which section of the Segmented control is selected for policy creation + int selectedCreationMethod = segmentedControl.SelectedIndex; await Task.Run(() => { @@ -839,120 +840,139 @@ await Task.Run(() => // Insert the data into the empty policy file Master.Initiate(DataPackage, EmptyPolicyPath, SiPolicyIntel.Authorization.Allow); - - - if (PolicyToAddLogsTo is not null) + switch (selectedCreationMethod) { + case 0: + { + if (PolicyToAddLogsTo is not null) + { + // Set policy name and reset the policy ID of our new policy + string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, null, null); - // Set policy name and reset the policy ID of our new policy - string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, null, null); + // Remove all policy rule options prior to merging the policies since we don't need to add/remove any policy rule options to/from the user input policy + CiRuleOptions.Set(filePath: EmptyPolicyPath, RemoveAll: true); - // Remove all policy rule options prior to merging the policies since we don't need to add/remove any policy rule options to/from the user input policy - CiRuleOptions.Set(filePath: EmptyPolicyPath, RemoveAll: true); + // Merge the created policy with the user-selected policy which will result in adding the new rules to it + SiPolicy.Merger.Merge(PolicyToAddLogsTo, [EmptyPolicyPath]); - // Merge the created policy with the user-selected policy which will result in adding the new rules to it - SiPolicy.Merger.Merge(PolicyToAddLogsTo, [EmptyPolicyPath]); + UpdateHvciOptions.Update(PolicyToAddLogsTo); - UpdateHvciOptions.Update(PolicyToAddLogsTo); + // If user selected to deploy the policy + if (DeployAtTheEnd) + { + string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); - // If user selected to deploy the policy - if (DeployAtTheEnd) - { - string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); + PolicyToCIPConverter.Convert(PolicyToAddLogsTo, CIPPath); - PolicyToCIPConverter.Convert(PolicyToAddLogsTo, CIPPath); + CiToolHelper.UpdatePolicy(CIPPath); + } + } + else + { + throw new InvalidOperationException("No policy file was selected to add the logs to."); + } - CiToolHelper.UpdatePolicy(CIPPath); - } - } - - else if (BasePolicyXMLFile is not null) - { - string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); + break; + } + case 1: + { + if (BasePolicyXMLFile is not null) + { + string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); - // Instantiate the user selected Base policy - To get its BasePolicyID - CodeIntegrityPolicy codeIntegrityPolicy = new(BasePolicyXMLFile, null); + // Instantiate the user selected Base policy - To get its BasePolicyID + CodeIntegrityPolicy codeIntegrityPolicy = new(BasePolicyXMLFile, null); - // Set the BasePolicyID of our new policy to the one from user selected policy - string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, codeIntegrityPolicy.BasePolicyID, null); + // Set the BasePolicyID of our new policy to the one from user selected policy + string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, codeIntegrityPolicy.BasePolicyID, null); - // Configure policy rule options - CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); + // Configure policy rule options + CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); - // Set policy version - SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); + // Set policy version + SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); - // Copying the policy file to the User Config directory - outside of the temporary staging area - File.Copy(EmptyPolicyPath, OutputPath, true); + // Copying the policy file to the User Config directory - outside of the temporary staging area + File.Copy(EmptyPolicyPath, OutputPath, true); - // If user selected to deploy the policy - if (DeployAtTheEnd) - { - string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); + // If user selected to deploy the policy + if (DeployAtTheEnd) + { + string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); - PolicyToCIPConverter.Convert(OutputPath, CIPPath); + PolicyToCIPConverter.Convert(OutputPath, CIPPath); - CiToolHelper.UpdatePolicy(CIPPath); - } - - } - else if (BasePolicyGUID is not null) - { - string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); + CiToolHelper.UpdatePolicy(CIPPath); + } + } + else + { + throw new InvalidOperationException("No policy file was selected to associate the Supplemental policy with."); + } + break; + } + case 2: + { - // Set the BasePolicyID of our new policy to the one supplied by user - string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, BasePolicyGUID.ToString(), null); + if (BasePolicyGUID is not null) + { + string OutputPath = Path.Combine(GlobalVars.UserConfigDir, $"{policyName}.xml"); - // Configure policy rule options - CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); + // Set the BasePolicyID of our new policy to the one supplied by user + string supplementalPolicyID = SetCiPolicyInfo.Set(EmptyPolicyPath, true, policyName, BasePolicyGUID.ToString(), null); + // Configure policy rule options + CiRuleOptions.Set(filePath: EmptyPolicyPath, template: CiRuleOptions.PolicyTemplate.Supplemental); - // Set policy version - SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); - // Copying the policy file to the User Config directory - outside of the temporary staging area - File.Copy(EmptyPolicyPath, OutputPath, true); + // Set policy version + SetCiPolicyInfo.Set(EmptyPolicyPath, new Version("1.0.0.0")); + // Copying the policy file to the User Config directory - outside of the temporary staging area + File.Copy(EmptyPolicyPath, OutputPath, true); - // If user selected to deploy the policy - if (DeployAtTheEnd) - { - string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); + // If user selected to deploy the policy + if (DeployAtTheEnd) + { - PolicyToCIPConverter.Convert(OutputPath, CIPPath); + string CIPPath = Path.Combine(stagingArea.FullName, $"{supplementalPolicyID}.cip"); - CiToolHelper.UpdatePolicy(CIPPath); + PolicyToCIPConverter.Convert(OutputPath, CIPPath); - } + CiToolHelper.UpdatePolicy(CIPPath); + } + } + else + { + throw new InvalidOperationException("No Base Policy GUID was provided to use as the BasePolicyID of the supplemental policy."); + } + break; + } + default: + { + break; + } } - - }); - } - finally { - // Enable the policy creator button again CreatePolicyButton.IsEnabled = true; - // enable the scan logs button again ScanLogs.IsEnabled = true; // Display the progress ring on the ScanLogs button ScanLogsProgressRing.IsActive = false; ScanLogsProgressRing.Visibility = Visibility.Collapsed; - } - } @@ -994,4 +1014,247 @@ private void BrowseForLogs_Flyout_Clear_Click(object sender, RoutedEventArgs e) MDEAdvancedHuntingLogs = null; } + + + /// + /// Event handler for the Cancel Sign In button + /// + /// + /// + private void MSGraphCancelSignInButton_Click(object sender, RoutedEventArgs e) + { + MicrosoftGraph.CancelSignIn(); + } + + + /// + /// Event handler for the SignIn button for Microsoft Graph + /// + /// + /// + private async void MSGraphSignInButton_Click(object sender, RoutedEventArgs e) + { + + bool signInSuccessful = false; + + try + { + MainInfoBar.Visibility = Visibility.Visible; + MainInfoBar.IsOpen = true; + MainInfoBar.Message = "Signing into MSGraph"; + MainInfoBar.Severity = InfoBarSeverity.Informational; + MainInfoBar.IsClosable = false; + + MSGraphCancelSignInButton.IsEnabled = true; + + MSGraphSignInButton.IsEnabled = false; + + await MicrosoftGraph.SignIn(MicrosoftGraph.AuthenticationContext.MDEAdvancedHunting); + + MainInfoBar.Message = "Successfully signed into MSGraph"; + MainInfoBar.Severity = InfoBarSeverity.Success; + + // Enable the sign out button + MSGraphSignOutButton.IsEnabled = true; + + // Enable the retrieve the logs button + RetrieveTheLogsButton.IsEnabled = true; + + signInSuccessful = true; + + } + + catch (OperationCanceledException) + { + signInSuccessful = false; + Logger.Write("Sign in to MSGraph was cancelled by the user"); + MainInfoBar.Message = "Sign in to MSGraph was cancelled by the user"; + MainInfoBar.Severity = InfoBarSeverity.Warning; + } + + catch (Exception ex) + { + MainInfoBar.Message = $"There was an error signing into MSGraph: {ex.Message}"; + MainInfoBar.Severity = InfoBarSeverity.Error; + + throw; + } + + finally + { + // If sign in wasn't successful, keep the button enabled + if (!signInSuccessful) + { + MSGraphSignInButton.IsEnabled = true; + } + + MainInfoBar.IsClosable = true; + + MSGraphCancelSignInButton.IsEnabled = false; + } + + } + + + /// + /// Event handler for signing out of Microsoft Graph + /// + /// + /// + private async void MSGraphSignOutButton_Click(object sender, RoutedEventArgs e) + { + + bool signOutSuccessful = false; + + try + { + MainInfoBar.Visibility = Visibility.Visible; + MainInfoBar.IsOpen = true; + MainInfoBar.Message = "Signing out of MSGraph"; + MainInfoBar.Severity = InfoBarSeverity.Informational; + MainInfoBar.IsClosable = false; + + MSGraphSignOutButton.IsEnabled = false; + + RetrieveTheLogsButton.IsEnabled = false; + + await MicrosoftGraph.SignOut(MicrosoftGraph.AuthenticationContext.MDEAdvancedHunting); + + signOutSuccessful = true; + + // Enable the Sign in button + MSGraphSignInButton.IsEnabled = true; + + MainInfoBar.Message = "Successfully signed out of MSGraph"; + MainInfoBar.Severity = InfoBarSeverity.Success; + + } + catch (Exception ex) + { + MainInfoBar.Message = $"There was an error signing out of MSGraph: {ex.Message}"; + MainInfoBar.Severity = InfoBarSeverity.Error; + + throw; + } + finally + { + // If sign out wasn't successful, keep the button enabled + if (!signOutSuccessful) + { + MSGraphSignOutButton.IsEnabled = true; + } + + MainInfoBar.IsClosable = true; + } + + } + + + /// + /// Event handler for the button that retrieves the logs + /// + /// + /// + private async void RetrieveTheLogsButton_Click(object sender, RoutedEventArgs e) + { + + bool errorsOccurred = false; + + MainInfoBar.Visibility = Visibility.Visible; + MainInfoBar.IsOpen = true; + MainInfoBar.Message = "Retrieving the Microsoft Defender for Endpoint Advanced Hunting data"; + MainInfoBar.Severity = InfoBarSeverity.Informational; + MainInfoBar.IsClosable = false; + + MDEAdvancedHuntingDataRootObject? root = null; + + try + { + RetrieveTheLogsButton.IsEnabled = false; + MSGraphDeviceNameButton.IsEnabled = false; + + // Retrieve the MDE Advanced Hunting data as a JSON string + string? result = await MicrosoftGraph.RunMDEAdvancedHuntingQuery(DeviceNameTextBox.Text); + + // If there were results + if (result is not null) + { + // Deserialize the JSON result + root = await Task.Run(() => JsonSerializer.Deserialize(result, MDEAdvancedHuntingJSONSerializationContext.Default.MDEAdvancedHuntingDataRootObject)); + + if (root is null) + { + MainInfoBar.Message = $"There were no logs to be retrieved"; + MainInfoBar.Severity = InfoBarSeverity.Warning; + errorsOccurred = true; + return; + } + + if (root.Results.Count is 0) + { + MainInfoBar.Message = $"0 logs were retrieved"; + MainInfoBar.Severity = InfoBarSeverity.Warning; + errorsOccurred = true; + return; + } + + Logger.Write("Deserialization complete. Number of records: " + (root.Results.Count)); + + // Grab the App Control Logs + HashSet Output = await Task.Run(() => GetMDEAdvancedHuntingLogsData.Retrieve(root.Results)); + + AllFileIdentities.Clear(); + FileIdentities.Clear(); + + // Store all of the data in the ObservableCollection and List + foreach (FileIdentity fileIdentity in Output) + { + AllFileIdentities.Add(fileIdentity); + + FileIdentities.Add(fileIdentity); + } + + UpdateTotalLogs(); + } + } + catch (Exception ex) + { + MainInfoBar.Message = $"There was an error retrieving the MDE Advanced Hunting logs from MSGraph: {ex.Message}"; + MainInfoBar.Severity = InfoBarSeverity.Error; + errorsOccurred = true; + throw; + } + finally + { + if (!errorsOccurred) + { + MainInfoBar.Message = $"Successfully retrieved {root?.Results.Count} logs from the cloud"; + MainInfoBar.Severity = InfoBarSeverity.Success; + } + + RetrieveTheLogsButton.IsEnabled = true; + MSGraphDeviceNameButton.IsEnabled = true; + + MainInfoBar.IsClosable = true; + } + } + + + /// + /// Event handler for for the segmented button's selection change + /// + /// + /// + private void SegmentedControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + CreatePolicyButton.Content = segmentedControl.SelectedIndex switch + { + 0 => "Add logs to the selected policy", + 1 => "Create Policy for Selected Base", + 2 => "Create Policy for Base GUID", + _ => "Create Policy" + }; + + } + } diff --git a/AppControl Manager/Pages/MergePolicies.xaml b/AppControl Manager/Pages/MergePolicies.xaml index 7787be180..3fd184c0f 100644 --- a/AppControl Manager/Pages/MergePolicies.xaml +++ b/AppControl Manager/Pages/MergePolicies.xaml @@ -36,7 +36,7 @@ - + diff --git a/AppControl Manager/Pages/Settings.xaml b/AppControl Manager/Pages/Settings.xaml index 890035dc0..ff6e7a285 100644 --- a/AppControl Manager/Pages/Settings.xaml +++ b/AppControl Manager/Pages/Settings.xaml @@ -33,7 +33,7 @@ - + @@ -203,24 +203,35 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + + diff --git a/AppControl Manager/Pages/Simulation.xaml b/AppControl Manager/Pages/Simulation.xaml index 205b41f6e..a9d9c220c 100644 --- a/AppControl Manager/Pages/Simulation.xaml +++ b/AppControl Manager/Pages/Simulation.xaml @@ -32,7 +32,7 @@ - + @@ -240,7 +240,7 @@ - + - + @@ -88,7 +88,7 @@ - + - + diff --git a/AppControl Manager/Pages/ValidatePolicy.xaml b/AppControl Manager/Pages/ValidatePolicy.xaml index 0d00b6148..2cae85bdf 100644 --- a/AppControl Manager/Pages/ValidatePolicy.xaml +++ b/AppControl Manager/Pages/ValidatePolicy.xaml @@ -33,7 +33,7 @@ - + diff --git a/AppControl Manager/Pages/ViewFileCertificates.xaml b/AppControl Manager/Pages/ViewFileCertificates.xaml index f9f886cdc..392bfa681 100644 --- a/AppControl Manager/Pages/ViewFileCertificates.xaml +++ b/AppControl Manager/Pages/ViewFileCertificates.xaml @@ -46,7 +46,7 @@ - + diff --git a/AppControl Manager/app.manifest b/AppControl Manager/app.manifest index 1cdf79164..5146bf162 100644 --- a/AppControl Manager/app.manifest +++ b/AppControl Manager/app.manifest @@ -2,7 +2,7 @@ - + diff --git a/README.md b/README.md index 4190ce4ee..ae754ff2a 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Unprotect-WindowsSecurity # CLI Mode
-### GitHub logo pink SVG [Install the AppControl Manager](https://github.com/HotCakeX/Harden-Windows-Security/wiki/AppControl-Manager) +### GitHub logo pink SVG [Install the AppControl Manager](https://github.com/HotCakeX/Harden-Windows-Security/wiki/AppControl-Manager) * [**YouTube demo**](https://www.youtube.com/watch?v=SzMs13n7elE) * [**Documentation**](https://github.com/HotCakeX/Harden-Windows-Security/wiki/AppControl-Manager) diff --git a/WDACConfig/ICON-SVG-ADVANCED.svg b/WDACConfig/ICON-SVG-ADVANCED.svg deleted file mode 100644 index 40bab4e3b..000000000 --- a/WDACConfig/ICON-SVG-ADVANCED.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/WDACConfig/ICON-SVG-Simplified.svg b/WDACConfig/ICON-SVG-Simplified.svg deleted file mode 100644 index b63077690..000000000 --- a/WDACConfig/ICON-SVG-Simplified.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WDACConfig/Icon smaller.png b/WDACConfig/Icon smaller.png deleted file mode 100644 index 5aaa2b2cc..000000000 Binary files a/WDACConfig/Icon smaller.png and /dev/null differ diff --git a/WDACConfig/IconFullSize.png b/WDACConfig/IconFullSize.png deleted file mode 100644 index f056cfff8..000000000 Binary files a/WDACConfig/IconFullSize.png and /dev/null differ diff --git a/WDACConfig/icon.png b/WDACConfig/icon.png deleted file mode 100644 index 30a0f61c3..000000000 Binary files a/WDACConfig/icon.png and /dev/null differ diff --git a/Wiki posts/App Control for Business/How to Use Microsoft Defender for Endpoint Advanced Hunting With WDAC App Control.md b/Wiki posts/App Control for Business/How to Use Microsoft Defender for Endpoint Advanced Hunting With WDAC App Control.md index 3f3bc9322..af4d935f0 100644 --- a/Wiki posts/App Control for Business/How to Use Microsoft Defender for Endpoint Advanced Hunting With WDAC App Control.md +++ b/Wiki posts/App Control for Business/How to Use Microsoft Defender for Endpoint Advanced Hunting With WDAC App Control.md @@ -1,4 +1,11 @@ -# How to Use Microsoft Defender for Endpoint Advanced Hunting With WDAC App Control +# How to Use Microsoft Defender for Endpoint Advanced Hunting With App Control + +
+ +> [!IMPORTANT]\ +> [AppControl Manager](https://github.com/HotCakeX/Harden-Windows-Security/wiki/Create-Policy-From-MDE-Advanced-Hunting) now supports automatically retrieving the Advanced Hunting logs from Microsoft Defender for Endpoint (MDE). The manual steps described below are only additional explanations. + +
App Control for Business is a highly effective security feature that empowers you to manage the execution of applications on your endpoints. diff --git a/Wiki posts/AppControl Manager/AppControl Manager.md b/Wiki posts/AppControl Manager/AppControl Manager.md index b2f31411b..efa71c60d 100644 --- a/Wiki posts/AppControl Manager/AppControl Manager.md +++ b/Wiki posts/AppControl Manager/AppControl Manager.md @@ -111,12 +111,16 @@ AppControl Manager is engineered with a security-first approach from the ground * The entire codebase is thoroughly commented, allowing code reviewers to effortlessly examine and verify every aspect of AppControl Manager's source code. +* AppControl Manager leverages [MSAL from Microsoft](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview) to manage Microsoft 365 authentications. This industry-standard library adheres to best practices for secure authentication token management. +
### Why Does AppControl Manager Require Administrator Privileges? * AppControl Manager operates exclusively within the "WDACConfig" directory located in the `Program Files` directory for all read and write operations. No data is accessed or modified outside this directory. This design ensures that non-elevated processes, unauthorized software, or unprivileged malware on the system cannot alter the policies you create, the certificates you generate, or the CIP binary files you deploy. +* AppControl Manager employs MediumIL (Medium Integrity Level) when running as an Administrator, ensuring that non-elevated processes cannot access its memory or attach debuggers. Given that the app handles sensitive information—such as Microsoft 365 authentication tokens stored in private variables—this design decision safeguards these tokens from unauthorized, unelevated access or tampering. + * Administrator privileges are required for scanning Code Integrity and AppLocker logs. These scans are integral to several application functions, providing enhanced insights and enabling the generation of precise supplemental policies tailored to your needs. * Deploying, removing, modifying, or checking the status of policies also necessitates Administrator privileges to ensure secure and reliable execution of these operations. diff --git a/Wiki posts/AppControl Manager/Create Policy From MDE Advanced Hunting.md b/Wiki posts/AppControl Manager/Create Policy From MDE Advanced Hunting.md index 399b473c2..6a8ff6e10 100644 --- a/Wiki posts/AppControl Manager/Create Policy From MDE Advanced Hunting.md +++ b/Wiki posts/AppControl Manager/Create Policy From MDE Advanced Hunting.md @@ -14,14 +14,44 @@ Use this [AppControl Manager](https://github.com/HotCakeX/Harden-Windows-Securit This page offers a data grid that has search functionality, sorting, removal of individual logs and copying entire rows or each cell to the clipboard. +You can also sign into your tenant to automatically retrieve Advanced Hunting logs related to Application Control and view, process, filter, search and convert them into App Control policies, all within the AppControl Manager application. + +Performing Advanced Hunting queries requires `ThreatHunting.Read.All` [permission](https://learn.microsoft.com/en-us/graph/api/security-security-runhuntingquery). +
## Configuration Details +* **Filters logs by date**: Use the calendar to filter the logs based on date they were generated. + +* **Search box**: Use this box to search for specific logs based on any available criteria/column. + +
+ +### Local Tab + * **Scan Logs**: Initially disabled until you select MDE Advanced Hunting CSV logs. * **Browse for MDE Advanced Huntings logs**: Use this button to browse for CSV files containing the Microsoft Defender for Endpoint Advanced Hunting exported CSV logs. +
+ +### Cloud Tab + +* **Sign In**: Sign into your tenant with Entra ID credentials. + +* **Sign Out**: Sign out of your tenant and discard any saved credentials from the app's memory. + +* **Cancel Sign In**: Cancel the sign-in process. If there was a problem during sign in process, use this button to cancel it and try again by pressing the `Sign In` button. + +* **Device Name**: Use this button to display a text box where you can enter a name of a device to filter the logs by before retrieving them. The device name will be included as part of the query that will be forwarded to the MDE Advanced Hunting API and the filtering will happen on the MDE side. + +* **Retrieve The Logs**: Use this button to retrieve the Advanced Hunting logs that are related to Application Control policies. The logs will be displayed in the data grid. + +
+ +### Create Tab + * **Create Policy -> Add to policy**: Use this option to select an existing Application Control XML policy file. The events you choose will be added directly to this file, expanding its coverage. * **Create Policy -> Base policy file**: This option allows you to specify a base XML policy file. The supplemental policy generated from the event logs will be linked to this base policy. @@ -30,8 +60,14 @@ This page offers a data grid that has search functionality, sorting, removal of * **Policy Name**: Enter the name of the policy that will be created from the MDE Advanced Hunting logs. -* **Filters logs by date**: Use the calendar to filter the logs based on date they were generated. - * **Scan Level**: You can choose from different scan levels. [Refer to this page for all the information about them.](https://github.com/HotCakeX/Harden-Windows-Security/wiki/WDAC-Rule-Levels-Comparison-and-Guide) +* **Actions -> Select All**: Use this option to select all the logs in the data grid. + +* **Actions -> De-select All**: Use this option to deselect all the logs in the data grid. + +* **Actions -> Clear Data**: Use this option to clear all of the processes and detected logs. + +* **Actions -> Deploy Policy After Creation**: Use this option to automatically deploy the App Control policy that you create with MDE Advanced Hunting logs to the local system. +
diff --git a/Wiki posts/Home Index.md b/Wiki posts/Home Index.md index ac040f420..ccadea9e2 100644 --- a/Wiki posts/Home Index.md +++ b/Wiki posts/Home Index.md @@ -6,7 +6,7 @@ # [Harden Windows Security GitHub Wiki content](https://github.com/HotCakeX/Harden-Windows-Security/wiki) -## AppControl Manager icon [AppControl Manager](https://github.com/HotCakeX/Harden-Windows-Security/wiki/AppControl-Manager) New Label +## AppControl Manager icon [AppControl Manager](https://github.com/HotCakeX/Harden-Windows-Security/wiki/AppControl-Manager) New Label - AppControl Manager Menu Item [Create AppControl Policy](https://github.com/HotCakeX/Harden-Windows-Security/wiki/Create-App-Control-Policy) - AppControl Manager Menu Item [Create Supplemental Policy](https://github.com/HotCakeX/Harden-Windows-Security/wiki/Create-Supplemental-App-Control-Policy)