From 95a20fb30ddacce560fcec70bfcca67ffff2bce7 Mon Sep 17 00:00:00 2001 From: HeroponRikiBestest <50224630+HeroponRikiBestest@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:48:22 -0500 Subject: [PATCH] Add nested json output for protectionscan (#391) * Attempt nested for real this time * forgot to include handling the base path * Reverted unnecesssary changes * Remove unneeded net6.0 gating * Add comments * Finish comments * Might as well safeguard if no protections are returned. * Use object instead of dynamic * Remove weird empty string root node handling * remove uneeded ref * Modify comment accordingly * Merge regular and nested json writing * Simplify object value checking * change flag handling Co-authored-by: Matt Nadareski * Initial fixes * Invert if-else to de-nest main logic * minor formatting fixes * Improved Json output * Remove unnecessary comments * That's just a string * Slight improvement * Simplify casting * attept further simplification * Further * Reduce nesting using inversion and continue * Further simplified logic * Replace my code with sabre's * De-nest using continue * newline * Remove all instances where it can end in a directory seperator --------- Co-authored-by: Matt Nadareski --- ProtectionScan/Features/MainFeature.cs | 109 ++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/ProtectionScan/Features/MainFeature.cs b/ProtectionScan/Features/MainFeature.cs index c4c206f4..7cee73d1 100644 --- a/ProtectionScan/Features/MainFeature.cs +++ b/ProtectionScan/Features/MainFeature.cs @@ -32,6 +32,9 @@ namespace ProtectionScan.Features #if NETCOREAPP private const string _jsonName = "json"; internal readonly FlagInput JsonInput = new(_jsonName, ["-j", "--json"], "Output to json file"); + + private const string _nestedName = "nested"; + internal readonly FlagInput NestedInput = new(_nestedName, ["-n", "--nested"], "Output to nested json file"); #endif private const string _noArchivesName = "no-archives"; @@ -63,6 +66,11 @@ namespace ProtectionScan.Features /// Enable JSON output /// public bool Json { get; private set; } + + /// + /// Enable nested JSON output + /// + public bool Nested { get; private set; } #endif public MainFeature() @@ -73,6 +81,7 @@ namespace ProtectionScan.Features Add(DebugInput); Add(FileOnlyInput); #if NETCOREAPP + JsonInput.Add(NestedInput); Add(JsonInput); #endif Add(NoContentsInput); @@ -93,6 +102,7 @@ namespace ProtectionScan.Features FileOnly = GetBoolean(_fileOnlyName); #if NETCOREAPP Json = GetBoolean(_jsonName); + Nested = GetBoolean(_nestedName); #endif // Create scanner for all paths @@ -248,9 +258,62 @@ namespace ProtectionScan.Features // Attempt to open a protection file for writing using var jsw = new StreamWriter(File.OpenWrite($"protection-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.json")); - // Create the output data var jsonSerializerOptions = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; - string serializedData = System.Text.Json.JsonSerializer.Serialize(protections, jsonSerializerOptions); + string serializedData; + if (Nested) + { + // A nested dictionary is used to achieve proper serialization. + var nestedDictionary = new Dictionary(); + var trimmedPath = path.TrimEnd(['\\', '/']); + + // Sort the keys for consistent output + string[] keys = [.. protections.Keys]; + Array.Sort(keys); + + var modifyNodeList = new List<(Dictionary, string, string[])>(); + + // Loop over all keys + foreach (string key in keys) + { + // Skip over files with no protection + var value = protections[key]; + if (value.Count == 0) + continue; + + // Sort the detected protections for consistent output + string[] fileProtections = [.. value]; + Array.Sort(fileProtections); + + // Inserts key and protections into nested dictionary, with the key trimmed of the base path. + InsertNode(nestedDictionary, key.Substring(trimmedPath.Length), fileProtections, modifyNodeList); + } + + // Adds the non-leaf-node protections back in + for (int i = 0; i < modifyNodeList.Count; i++) + { + var copyDictionary = modifyNodeList[i].Item1[modifyNodeList[i].Item2]; + + var modifyNode = new List(); + modifyNode.Add(modifyNodeList[i].Item3); + modifyNode.Add(copyDictionary); + + modifyNodeList[i].Item1[modifyNodeList[i].Item2] = modifyNode; + } + + // Move nested dictionary into final dictionary with the base path as a key. + var finalDictionary = new Dictionary>() + { + {trimmedPath, nestedDictionary} + }; + + // Create the output data + serializedData = System.Text.Json.JsonSerializer.Serialize(finalDictionary, jsonSerializerOptions); + } + else + { + // Create the output data + serializedData = System.Text.Json.JsonSerializer.Serialize(protections, jsonSerializerOptions); + } // Write the output data // TODO: this prints plus symbols wrong, probably some other things too @@ -263,6 +326,48 @@ namespace ProtectionScan.Features Console.WriteLine(); } } + + /// + /// Inserts file protection dictionary entries into a nested dictionary based on path + /// + /// File or directory path + /// The "key" for the given protection entry, already trimmed of its base path + /// The scanned protection(s) for a given file + public static void InsertNode(Dictionary nestedDictionary, string path, string[] protections, List<(Dictionary, string, string[])> modifyNodeList) + { + var current = nestedDictionary; + var pathParts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + + // Traverses the nested dictionary until the "leaf" dictionary is reached. + for (int i = 0; i < pathParts.Length - 1; i++) + { + var part = pathParts[i]; + + // Inserts new subdictionaries if one doesn't already exist + if (!current.ContainsKey(part)) + { + var innerDictionary = new Dictionary(); + current[part] = innerDictionary; + current = innerDictionary; + continue; + } + + var innerObject = current[part]; + + // Handle instances where a protection was already assigned to the current node + if (innerObject is string[] existingProtections) + { + modifyNodeList.Add((current, part, existingProtections)); + innerObject = new Dictionary(); + } + + current[part] = innerObject; + current = (Dictionary)current[part]; + } + + // If the "leaf" dictionary has been reached, add the file and its protections. + current.Add(pathParts[^1], protections); + } #endif } }