From 3ca1a418161adfe92f0e22b8853bdb35fc9fd2b8 Mon Sep 17 00:00:00 2001 From: Meik Date: Thu, 9 Jul 2026 22:43:41 +0200 Subject: [PATCH] Add NTFS ensure level filters --- LiamNtfs/C4IT.LIAM.Ntfs.cs | 174 ++++++++++++++++++++++++++++++++++++- README.md | 27 ++++++ 2 files changed, 198 insertions(+), 3 deletions(-) diff --git a/LiamNtfs/C4IT.LIAM.Ntfs.cs b/LiamNtfs/C4IT.LIAM.Ntfs.cs index 0d44b15..6b779d7 100644 --- a/LiamNtfs/C4IT.LIAM.Ntfs.cs +++ b/LiamNtfs/C4IT.LIAM.Ntfs.cs @@ -53,6 +53,20 @@ namespace C4IT.LIAM public int Level { get; set; } = -1; } + private sealed class cNtfsLevelRange + { + public bool IsConfigured { get; set; } + public bool IsValid { get; set; } = true; + public int MinLevel { get; set; } = int.MinValue; + public int MaxLevel { get; set; } = int.MaxValue; + public string ErrorMessage { get; set; } = string.Empty; + + public bool Contains(int level) + { + return !IsConfigured || level >= MinLevel && level <= MaxLevel; + } + } + private sealed class cNtfsDataAreaDiagnostics { private const int MaxSamples = 10; @@ -123,6 +137,10 @@ namespace C4IT.LIAM private const string AdditionalConfigurationExcludePathsKey = "NtfsExcludePaths"; private const string AdditionalConfigurationIncludePathsKey = "NtfsIncludePaths"; private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath"; + private const string AdditionalConfigurationPermissionGroupsMinLevelKey = "NtfsPermissionGroupsMinLevel"; + private const string AdditionalConfigurationPermissionGroupsMaxLevelKey = "NtfsPermissionGroupsMaxLevel"; + private const string AdditionalConfigurationTraverseGroupsMinLevelKey = "NtfsTraverseGroupsMinLevel"; + private const string AdditionalConfigurationTraverseGroupsMaxLevelKey = "NtfsTraverseGroupsMaxLevel"; private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement"; private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase"; private const string AdditionalConfigurationAdDomainControllersKey = "NtfsAdDomainControllers"; @@ -1155,6 +1173,16 @@ namespace C4IT.LIAM }); } + string levelSkipReason; + if (!IsPermissionLevelManagedPath(classification, out levelSkipReason)) + { + return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) + { + resultErrorId = 30008, + resultMessage = levelSkipReason + }); + } + var parentPath = Directory.GetParent(folderPath)?.FullName; var engine = CreateFilesystemEngine( folderPath, @@ -1208,7 +1236,19 @@ namespace C4IT.LIAM }); } - if (!IsTraversePermissionManagedPath(folderPath)) + var traverseLevelRange = GetTraverseLevelRange(); + if (!traverseLevelRange.IsValid) + { + var message = $"NTFS traverse ensure skipped for '{folderPath}' because {traverseLevelRange.ErrorMessage}"; + LogEntry(message, LogLevels.Warning); + return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) + { + resultErrorId = 30008, + resultMessage = message + }); + } + + if (!IsTraversePermissionCandidatePath(folderPath)) { return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) { @@ -1355,6 +1395,16 @@ namespace C4IT.LIAM } private bool IsPermissionManagedPath(string path, params eNtfsPathKind[] supportedKinds) + { + if (!IsPermissionManagedPathCandidate(path, supportedKinds)) + return false; + + var classification = ClassifyPath(path); + string levelSkipReason; + return IsPermissionLevelManagedPath(classification, out levelSkipReason); + } + + private bool IsPermissionManagedPathCandidate(string path, params eNtfsPathKind[] supportedKinds) { var classification = ClassifyPath(path); if (!IsSupportedPermissionManagedPathKind(classification, supportedKinds)) @@ -1365,13 +1415,25 @@ namespace C4IT.LIAM if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule)) return false; - return IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule); + if (!IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule)) + return false; + + return true; } private bool IsTraversePermissionManagedPath(string path) + { + if (!IsTraversePermissionCandidatePath(path)) + return false; + + string levelSkipReason; + return IsTraverseLevelManagedPath(ClassifyPath(path), out levelSkipReason); + } + + private bool IsTraversePermissionCandidatePath(string path) { if (string.IsNullOrWhiteSpace(GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey))) - return IsPermissionManagedFolderPath(path); + return IsPermissionManagedPathCandidate(path, eNtfsPathKind.Folder); var classification = ClassifyPath(path); if (classification == null || classification.Kind == eNtfsPathKind.ServerRoot || classification.Kind == eNtfsPathKind.Unknown) @@ -1385,6 +1447,112 @@ namespace C4IT.LIAM return Directory.Exists(path); } + private bool IsPermissionLevelManagedPath(cNtfsPathClassification classification, out string skipReason) + { + return IsLevelManagedPath( + classification, + GetPermissionLevelRange(), + "permission group ensure", + out skipReason); + } + + private bool IsTraverseLevelManagedPath(cNtfsPathClassification classification, out string skipReason) + { + return IsLevelManagedPath( + classification, + GetTraverseLevelRange(), + "traverse group ensure", + out skipReason); + } + + private bool IsLevelManagedPath(cNtfsPathClassification classification, cNtfsLevelRange range, string operationName, out string skipReason) + { + skipReason = string.Empty; + if (classification == null) + { + skipReason = $"NTFS {operationName} skipped because the path could not be classified."; + return false; + } + + if (range == null || !range.IsConfigured) + return true; + + if (!range.IsValid) + { + skipReason = $"NTFS {operationName} skipped for '{classification.NormalizedPath}' because {range.ErrorMessage}"; + LogEntry(skipReason, LogLevels.Warning); + return false; + } + + if (range.Contains(classification.Level)) + return true; + + skipReason = $"NTFS {operationName} skipped for '{classification.NormalizedPath}' because level {classification.Level} is outside configured range {range.MinLevel}..{range.MaxLevel}."; + LogEntry(skipReason, LogLevels.Debug); + return false; + } + + private cNtfsLevelRange GetPermissionLevelRange() + { + return GetConfiguredLevelRange( + AdditionalConfigurationPermissionGroupsMinLevelKey, + AdditionalConfigurationPermissionGroupsMaxLevelKey, + "permission group level range"); + } + + private cNtfsLevelRange GetTraverseLevelRange() + { + return GetConfiguredLevelRange( + AdditionalConfigurationTraverseGroupsMinLevelKey, + AdditionalConfigurationTraverseGroupsMaxLevelKey, + "traverse group level range"); + } + + private cNtfsLevelRange GetConfiguredLevelRange(string minKey, string maxKey, string label) + { + var range = new cNtfsLevelRange(); + var minValue = GetAdditionalConfigurationValue(minKey); + var maxValue = GetAdditionalConfigurationValue(maxKey); + var hasMin = !string.IsNullOrWhiteSpace(minValue); + var hasMax = !string.IsNullOrWhiteSpace(maxValue); + range.IsConfigured = hasMin || hasMax; + if (!range.IsConfigured) + return range; + + int parsedValue; + if (hasMin) + { + if (!int.TryParse(minValue, out parsedValue)) + { + range.IsValid = false; + range.ErrorMessage = $"AdditionalConfiguration '{minKey}' for {label} is not a valid integer: '{minValue}'."; + return range; + } + + range.MinLevel = parsedValue; + } + + if (hasMax) + { + if (!int.TryParse(maxValue, out parsedValue)) + { + range.IsValid = false; + range.ErrorMessage = $"AdditionalConfiguration '{maxKey}' for {label} is not a valid integer: '{maxValue}'."; + return range; + } + + range.MaxLevel = parsedValue; + } + + if (range.MinLevel > range.MaxLevel) + { + range.IsValid = false; + range.ErrorMessage = $"AdditionalConfiguration {label} is invalid because min level {range.MinLevel} is greater than max level {range.MaxLevel}."; + } + + return range; + } + private static bool IsSupportedPermissionManagedPathKind(cNtfsPathClassification classification, params eNtfsPathKind[] supportedKinds) { if (classification == null || supportedKinds == null || supportedKinds.Length == 0) diff --git a/README.md b/README.md index 4cea2d8..63d3e91 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ Im Diagnose-JSON erscheinen diese Werte unter `AdditionalConfiguration`. Paramet | `NtfsIncludePaths` | Pfadliste, getrennt mit `;`, | oder Zeilenumbruechen | Beschraenkt die NTFS-Verarbeitung auf passende Pfade. Unterstuetzt relative Pfade unterhalb des RootPath, absolute UNC-Pfade und einfache Wildcards mit `*`. Wenn der Parameter leer ist, sind alle Pfade eingeschlossen. | | `NtfsExcludePaths` | Pfadliste, getrennt mit `;`, | oder Zeilenumbruechen | Schliesst passende Pfade von der NTFS-Verarbeitung aus. Excludes gewinnen gegen Includes. Unterstuetzt relative Pfade, absolute UNC-Pfade und einfache Wildcards mit `*`. | | `NtfsTraverseBoundaryPath` | Relativer oder absoluter Pfad | Setzt eine Traverse-Grenze fuer die Traverse-Gruppenverarbeitung. Damit koennen Traverse-Gruppen ueber den eigentlichen Einsprung hinaus bis zu einer definierten Ebene sichergestellt werden. Der Boundary-Pfad selbst darf ebenfalls als Traverse-Ziel verarbeitet werden. | +| `NtfsPermissionGroupsMinLevel` / `NtfsPermissionGroupsMaxLevel` | Ganzzahl, z. B. `0`, `1`, `2` | Optionaler Levelbereich fuer Owner/Write/Read-Ensure. Ebene `0` ist der `RootPath`; Unterordner sind `1`, `2`, `3`, ... Wenn beide Parameter leer sind, bleibt das bisherige Verhalten unveraendert. | +| `NtfsTraverseGroupsMinLevel` / `NtfsTraverseGroupsMaxLevel` | Ganzzahl, z. B. `-1`, `0`, `2` | Optionaler Levelbereich fuer Traverse-Ensure. Ebene `0` ist der `RootPath`; Parent-Pfade oberhalb des `RootPath` sind `-1`, `-2`, ... Wenn beide Parameter leer sind, bleibt das bisherige Verhalten unveraendert. | | `NtfsGroupNameSanitizeReplacement` | Zeichenfolge, z. B. `_`, `.`, leer, `none`, `remove`, `` | Legt fest, womit ungueltige Zeichen in dynamischen gruppennamenrelevanten Pfadbestandteilen ersetzt werden. Standard ist `_`. Mit leerem Wert oder `none`/`remove`/`` werden ungueltige Zeichen entfernt und Pfadsegmente ohne Trennzeichen verbunden. | | `PreserveNtfsAdGroupNameCase` | `true`, `1`, `yes` | Unterbindet das automatische Uppercase fuer generierte NTFS-AD-Gruppennamen. Ohne diesen Parameter werden generierte Gruppennamen wie bisher in Grossbuchstaben erzeugt. | | `ForceStrictAdGroupNames` | `true`, `1`, `yes` | Erzwingt strikte AD-Gruppennamen. Wildcard-/ACL-basierte Wiederverwendung abweichender bestehender Gruppen wird damit eingeschraenkt; es werden nur exakt passende konfigurierte oder generierte Namen verwendet. | @@ -224,6 +226,10 @@ EnsureNtfsTraverseGroups=1 NtfsIncludePaths=Finance\*;HR\Reports NtfsExcludePaths=*\_archive\* NtfsTraverseBoundaryPath=\\fileserver\file_shares +NtfsPermissionGroupsMinLevel=1 +NtfsPermissionGroupsMaxLevel=2 +NtfsTraverseGroupsMinLevel=-1 +NtfsTraverseGroupsMaxLevel=2 NtfsGroupNameSanitizeReplacement=. PreserveNtfsAdGroupNameCase=1 NtfsAdDomainControllers=dc01.contoso.local,dc02.contoso.local @@ -261,6 +267,27 @@ EnsureNtfsTraverseGroups=1 In diesem Beispiel koennen Traverse-Gruppen fuer `...\Projekte` selbst und fuer die Ordner darunter sichergestellt werden. Der Boundary-Pfad selbst ist gueltig; er muss nicht zusaetzlich als `NtfsIncludePaths`-Match enthalten sein, solange es nur um Traverse geht. +#### Levelbereiche fuer automatisches Ensure + +Die optionalen Levelbereiche schraenken ein, auf welchen Ebenen Gruppen und ACLs automatisch sichergestellt werden. Wenn keiner der Level-Parameter gesetzt ist, bleibt das bisherige Verhalten unveraendert. + +Ebene `0` ist immer der konfigurierte `RootPath`. Unterordner unterhalb des `RootPath` haben positive Level (`1`, `2`, `3`, ...). Parent-Pfade oberhalb des `RootPath` haben negative Level (`-1`, `-2`, ...). Negative Level sind vor allem fuer Traverse relevant, wenn `NtfsTraverseBoundaryPath` oberhalb des `RootPath` liegt. + +Beispiel: + +```text +RootPath=\\server\share\Bereich +NtfsTraverseBoundaryPath=\\server\share +NtfsPermissionGroupsMinLevel=1 +NtfsPermissionGroupsMaxLevel=2 +NtfsTraverseGroupsMinLevel=-1 +NtfsTraverseGroupsMaxLevel=2 +``` + +Damit werden Owner/Write/Read-Gruppen nur fuer `\\server\share\Bereich\...` auf Ebene `1` und `2` erzeugt. Traverse-Gruppen duerfen zusaetzlich bis zum Share auf Ebene `-1` erzeugt werden. `NtfsTraverseBoundaryPath` bleibt die harte Grenze fuer Traverse; die Min-/Max-Level legen nur fest, welche Ebenen innerhalb dieses Korridors verarbeitet werden. + +Wenn nur ein Min- oder Max-Wert gesetzt ist, ist die andere Seite offen. Ungueltige Werte oder ein Min-Wert groesser als der Max-Wert fuehren dazu, dass die betroffene Ensure-Phase mit einer Warning uebersprungen wird. + #### WhatIf / Preview Wenn `WhatIf=1` gesetzt ist, fuehrt LIAM die automatische NTFS-Verarbeitung als Preview aus. Fuer Permission-Ensure und Traverse-Ensure werden geplante Gruppen, wiederverwendete Gruppen, geplante ACLs, bestehende ACLs, Traverse-Gruppen und Warnings im Ergebnis und im Debug-Log ausgegeben. Es werden keine AD-Gruppen, Gruppenmitgliedschaften oder ACLs dauerhaft geaendert.