diff --git a/ProtectionScan/Features/MainFeature.cs b/ProtectionScan/Features/MainFeature.cs
index c4c206f4..5276a0e4 100644
--- a/ProtectionScan/Features/MainFeature.cs
+++ b/ProtectionScan/Features/MainFeature.cs
@@ -32,6 +32,9 @@ internal sealed class MainFeature : Feature
#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 @@ internal sealed class MainFeature : Feature
/// 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 @@ public MainFeature()
Add(DebugInput);
Add(FileOnlyInput);
#if NETCOREAPP
+ JsonInput.Add(NestedInput);
Add(JsonInput);
#endif
Add(NoContentsInput);
@@ -93,6 +102,7 @@ public override bool Execute()
FileOnly = GetBoolean(_fileOnlyName);
#if NETCOREAPP
Json = GetBoolean(_jsonName);
+ Nested = GetBoolean(_nestedName);
#endif
// Create scanner for all paths
@@ -248,9 +258,48 @@ private void WriteProtectionResultJson(string path, Dictionary();
+ var trimmedPath = path.TrimEnd(['\\', '/']);
+
+ // Sort the keys for consistent output
+ string[] keys = [.. protections.Keys];
+ Array.Sort(keys);
+
+ // 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.
+ DeepInsert(nestedDictionary, key.Substring(trimmedPath.Length), fileProtections);
+ }
+
+ // 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 +312,55 @@ private void WriteProtectionResultJson(string path, Dictionary
+ /// 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 DeepInsert(Dictionary nestedDictionary, string path, string[] protections)
+ {
+ var current = nestedDictionary;
+ path = path.TrimStart(Path.DirectorySeparatorChar);
+ var pathParts = path.Split(Path.DirectorySeparatorChar);
+
+ // Traverses the nested dictionary until the "leaf" dictionary is reached.
+ for (int i = 0; i < pathParts.Length; i++)
+ {
+ var part = pathParts[i];
+ if (i != (pathParts.Length - 1))
+ {
+ if (!current.ContainsKey(part)) // Inserts new subdictionaries if one doesn't already exist
+ {
+ var innerObject = new Dictionary();
+ current[part] = innerObject;
+ current = innerObject;
+ }
+ else // Traverses already existing subdictionaries
+ {
+ var innerObject = current[part];
+
+ // Handle instances where a protection was already assigned to the current node
+ if (innerObject is string[])
+ {
+ current[part] = new Dictionary();
+ current = (Dictionary)current[part];
+ current.Add("", innerObject);
+ }
+ else
+ {
+ current[part] = innerObject;
+ current = (Dictionary)innerObject;
+ }
+ }
+ }
+ else // If the "leaf" dictionary has been reached, add the file and its protections.
+ {
+ current.Add(part, protections);
+ }
+ }
+ }
#endif
}
}