diff --git a/src/Sddl.Parser/Ace.cs b/src/Sddl.Parser/Ace.cs index 8b048bf..b5734bc 100644 --- a/src/Sddl.Parser/Ace.cs +++ b/src/Sddl.Parser/Ace.cs @@ -16,7 +16,11 @@ public class Ace : Acm public string InheritObjectGuid { get; } public Sid AceSid { get; } - public Ace(string ace, SecurableObjectType type = SecurableObjectType.Unknown) + public Ace(string ace, SecurableObjectType type = SecurableObjectType.Unknown) : this(ace, type, null) + { + } + + public Ace(string ace, SecurableObjectType type, SidResolverOptions sidResolverOptions) { Raw = ace; @@ -90,7 +94,7 @@ public Ace(string ace, SecurableObjectType type = SecurableObjectType.Unknown) // account_sid if (parts.Length > 5 && parts[5].Length > 0) { - AceSid = new Sid(parts[5]); + AceSid = new Sid(parts[5], sidResolverOptions); } // resource_attribute diff --git a/src/Sddl.Parser/Acl.cs b/src/Sddl.Parser/Acl.cs index 6306cbf..853cbbc 100644 --- a/src/Sddl.Parser/Acl.cs +++ b/src/Sddl.Parser/Acl.cs @@ -11,7 +11,11 @@ public class Acl : Acm public string[] Flags { get; } public Ace[] Aces { get; } - public Acl(string acl, SecurableObjectType type = SecurableObjectType.Unknown) + public Acl(string acl, SecurableObjectType type = SecurableObjectType.Unknown) : this(acl, type, null) + { + } + + public Acl(string acl, SecurableObjectType type, SidResolverOptions sidResolverOptions) { Raw = acl; @@ -55,7 +59,7 @@ public Acl(string acl, SecurableObjectType type = SecurableObjectType.Unknown) } if (balance == 0) - aces.AddLast(new Ace(acl.Substring(begin + 1, length), type)); + aces.AddLast(new Ace(acl.Substring(begin + 1, length), type, sidResolverOptions)); } else if (balance <= 0) { diff --git a/src/Sddl.Parser/DirectoryServiceSidResolver.cs b/src/Sddl.Parser/DirectoryServiceSidResolver.cs new file mode 100644 index 0000000..5b754bf --- /dev/null +++ b/src/Sddl.Parser/DirectoryServiceSidResolver.cs @@ -0,0 +1,54 @@ +using System; +using System.Security.Principal; + +namespace Sddl.Parser +{ + /// + /// Resolves SID strings using SecurityIdentifier.Translate (DirectoryService lookup) + /// + public class DirectoryServiceSidResolver : ISidResolver + { + /// + /// Attempts to resolve a SID string to a meaningful name using DirectoryService lookup + /// + /// The SID string to resolve + /// The resolved name, or null if resolution fails + public string ResolveSid(string sidString) + { + if (string.IsNullOrEmpty(sidString)) + return null; + + try + { + // Try to parse as a SecurityIdentifier + SecurityIdentifier sid; + try + { + sid = new SecurityIdentifier(sidString); + } + catch + { + // Not a valid SID format + return null; + } + + // Attempt to translate to NTAccount using DirectoryService lookup + try + { + var account = (NTAccount)sid.Translate(typeof(NTAccount)); + return account.Value; + } + catch + { + // Translation failed - SID may not exist or may not be translatable + return null; + } + } + catch + { + // Any other error during resolution + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Sddl.Parser/ISidResolver.cs b/src/Sddl.Parser/ISidResolver.cs new file mode 100644 index 0000000..8e9aa79 --- /dev/null +++ b/src/Sddl.Parser/ISidResolver.cs @@ -0,0 +1,15 @@ +namespace Sddl.Parser +{ + /// + /// Interface for resolving SID strings to meaningful names + /// + public interface ISidResolver + { + /// + /// Attempts to resolve a SID string to a meaningful name + /// + /// The SID string to resolve + /// The resolved name, or null if resolution fails + string ResolveSid(string sidString); + } +} \ No newline at end of file diff --git a/src/Sddl.Parser/Sddl.Parser.csproj b/src/Sddl.Parser/Sddl.Parser.csproj index 951a14f..93da280 100755 --- a/src/Sddl.Parser/Sddl.Parser.csproj +++ b/src/Sddl.Parser/Sddl.Parser.csproj @@ -8,4 +8,8 @@ Sddl.Parser + + + + \ No newline at end of file diff --git a/src/Sddl.Parser/Sddl.cs b/src/Sddl.Parser/Sddl.cs index 8d92e19..3a6efd7 100644 --- a/src/Sddl.Parser/Sddl.cs +++ b/src/Sddl.Parser/Sddl.cs @@ -13,7 +13,11 @@ public class Sddl : Acm public Acl Dacl { get; } public Acl Sacl { get; } - public Sddl(string sddl, SecurableObjectType type = SecurableObjectType.Unknown) + public Sddl(string sddl, SecurableObjectType type = SecurableObjectType.Unknown) : this(sddl, type, null) + { + } + + public Sddl(string sddl, SecurableObjectType type, SidResolverOptions sidResolverOptions) { Raw = sddl; @@ -40,25 +44,25 @@ public Sddl(string sddl, SecurableObjectType type = SecurableObjectType.Unknown) if (components.TryGetValue(OwnerToken, out var owner)) { - Owner = new Sid(owner); + Owner = new Sid(owner, sidResolverOptions); components.Remove(OwnerToken); } if (components.TryGetValue(GroupToken, out var group)) { - Group = new Sid(group); + Group = new Sid(group, sidResolverOptions); components.Remove(GroupToken); } if (components.TryGetValue(DaclToken, out var dacl)) { - Dacl = new Acl(dacl, type); + Dacl = new Acl(dacl, type, sidResolverOptions); components.Remove(DaclToken); } if (components.TryGetValue(SaclToken, out var sacl)) { - Sacl = new Acl(sacl, type); + Sacl = new Acl(sacl, type, sidResolverOptions); components.Remove(SaclToken); } diff --git a/src/Sddl.Parser/Sid.cs b/src/Sddl.Parser/Sid.cs index 5746ca3..8343903 100644 --- a/src/Sddl.Parser/Sid.cs +++ b/src/Sddl.Parser/Sid.cs @@ -8,7 +8,11 @@ public class Sid : Acm public string Alias { get; } - public Sid(string sid) + public Sid(string sid) : this(sid, null) + { + } + + public Sid(string sid, SidResolverOptions options) { Raw = sid; @@ -16,9 +20,18 @@ public Sid(string sid) if (alias == null) { - Report(Error.SDP001.Format(sid)); - - alias = Format.Unknown(sid); + // Try DirectoryService lookup if enabled + if (options?.EnableDirectoryServiceLookup == true) + { + var resolver = options.SidResolver ?? new DirectoryServiceSidResolver(); + alias = resolver.ResolveSid(sid); + } + + if (alias == null) + { + Report(Error.SDP001.Format(sid)); + alias = Format.Unknown(sid); + } } Alias = alias; diff --git a/src/Sddl.Parser/SidResolverOptions.cs b/src/Sddl.Parser/SidResolverOptions.cs new file mode 100644 index 0000000..513f9d2 --- /dev/null +++ b/src/Sddl.Parser/SidResolverOptions.cs @@ -0,0 +1,19 @@ +namespace Sddl.Parser +{ + /// + /// Configuration options for SID resolution + /// + public class SidResolverOptions + { + /// + /// Gets or sets whether to enable DirectoryService lookup for unknown SIDs + /// + public bool EnableDirectoryServiceLookup { get; set; } = false; + + /// + /// Gets or sets the SID resolver to use for DirectoryService lookups. + /// If null and EnableDirectoryServiceLookup is true, a default DirectoryServiceSidResolver will be used. + /// + public ISidResolver SidResolver { get; set; } + } +} \ No newline at end of file diff --git a/test/Sddl.Parser.Tests/SidResolverTests.cs b/test/Sddl.Parser.Tests/SidResolverTests.cs new file mode 100644 index 0000000..ada0956 --- /dev/null +++ b/test/Sddl.Parser.Tests/SidResolverTests.cs @@ -0,0 +1,200 @@ +using System; +using Xunit; + +namespace Sddl.Parser.Tests +{ + public class SidResolverTests + { + [Fact] + public void Sid_WithoutOptions_ShouldUseLegacyBehavior() + { + // Arrange + var unknownSidString = "S-1-5-21-1234567890-1234567890-1234567890-1001"; + + // Act + var sid = new Sid(unknownSidString); + + // Assert + Assert.Equal(unknownSidString, sid.Raw); + Assert.Equal($"Unknown({unknownSidString})", sid.Alias); + Assert.False(sid.IsValid); + Assert.Single(sid.Errors); + } + + [Fact] + public void Sid_WithDirectoryServiceDisabled_ShouldUseLegacyBehavior() + { + // Arrange + var unknownSidString = "S-1-5-21-1234567890-1234567890-1234567890-1001"; + var options = new SidResolverOptions { EnableDirectoryServiceLookup = false }; + + // Act + var sid = new Sid(unknownSidString, options); + + // Assert + Assert.Equal(unknownSidString, sid.Raw); + Assert.Equal($"Unknown({unknownSidString})", sid.Alias); + Assert.False(sid.IsValid); + Assert.Single(sid.Errors); + } + + [Fact] + public void Sid_WithCustomResolver_ShouldUseCustomResolver() + { + // Arrange + var unknownSidString = "S-1-5-21-1234567890-1234567890-1234567890-1001"; + var customResolver = new TestSidResolver(); + var options = new SidResolverOptions + { + EnableDirectoryServiceLookup = true, + SidResolver = customResolver + }; + + // Act + var sid = new Sid(unknownSidString, options); + + // Assert + Assert.Equal(unknownSidString, sid.Raw); + Assert.Equal("TestDomain\\TestUser", sid.Alias); + Assert.True(sid.IsValid); + Assert.Empty(sid.Errors); + } + + [Fact] + public void Sid_WithCustomResolverReturningNull_ShouldFallbackToUnknown() + { + // Arrange + var unknownSidString = "S-1-5-21-9999999999-9999999999-9999999999-9999"; + var customResolver = new TestSidResolver(); + var options = new SidResolverOptions + { + EnableDirectoryServiceLookup = true, + SidResolver = customResolver + }; + + // Act + var sid = new Sid(unknownSidString, options); + + // Assert + Assert.Equal(unknownSidString, sid.Raw); + Assert.Equal($"Unknown({unknownSidString})", sid.Alias); + Assert.False(sid.IsValid); + Assert.Single(sid.Errors); + } + + [Fact] + public void Sid_KnownSid_ShouldIgnoreResolver() + { + // Arrange + var knownSidString = "SY"; + var customResolver = new TestSidResolver(); + var options = new SidResolverOptions + { + EnableDirectoryServiceLookup = true, + SidResolver = customResolver + }; + + // Act + var sid = new Sid(knownSidString, options); + + // Assert + Assert.Equal(knownSidString, sid.Raw); + Assert.Equal("Local System", sid.Alias); + Assert.True(sid.IsValid); + Assert.Empty(sid.Errors); + } + + [Fact] + public void Sddl_WithSidResolverOptions_ShouldPassOptionsToSids() + { + // Arrange + var sddlString = "O:S-1-5-21-1234567890-1234567890-1234567890-1001G:DAD:(A;;FA;;;S-1-5-21-1234567890-1234567890-1234567890-1001)"; + var customResolver = new TestSidResolver(); + var options = new SidResolverOptions + { + EnableDirectoryServiceLookup = true, + SidResolver = customResolver + }; + + // Act + var sddl = new Sddl(sddlString, SecurableObjectType.Unknown, options); + + // Assert + Assert.NotNull(sddl.Owner); + Assert.Equal("TestDomain\\TestUser", sddl.Owner.Alias); + Assert.True(sddl.Owner.IsValid); + + Assert.NotNull(sddl.Dacl); + Assert.NotEmpty(sddl.Dacl.Aces); + Assert.NotNull(sddl.Dacl.Aces[0].AceSid); + Assert.Equal("TestDomain\\TestUser", sddl.Dacl.Aces[0].AceSid.Alias); + Assert.True(sddl.Dacl.Aces[0].AceSid.IsValid); + } + + [Fact] + public void DirectoryServiceSidResolver_WithValidSid_ShouldHandleGracefully() + { + // Arrange + var resolver = new DirectoryServiceSidResolver(); + var validSidString = "S-1-1-0"; // Everyone - should be resolvable + + // Act + var result = resolver.ResolveSid(validSidString); + + // Assert - either returns a name or null, both are acceptable + // We can't assert specific behavior since it depends on the system + Assert.True(result == null || !string.IsNullOrEmpty(result)); + } + + [Fact] + public void DirectoryServiceSidResolver_WithInvalidSid_ShouldReturnNull() + { + // Arrange + var resolver = new DirectoryServiceSidResolver(); + var invalidSidString = "not-a-sid"; + + // Act + var result = resolver.ResolveSid(invalidSidString); + + // Assert + Assert.Null(result); + } + + [Fact] + public void DirectoryServiceSidResolver_WithNullSid_ShouldReturnNull() + { + // Arrange + var resolver = new DirectoryServiceSidResolver(); + + // Act + var result = resolver.ResolveSid(null); + + // Assert + Assert.Null(result); + } + + [Fact] + public void DirectoryServiceSidResolver_WithEmptySid_ShouldReturnNull() + { + // Arrange + var resolver = new DirectoryServiceSidResolver(); + + // Act + var result = resolver.ResolveSid(string.Empty); + + // Assert + Assert.Null(result); + } + } + + // Test helper class + public class TestSidResolver : ISidResolver + { + public string ResolveSid(string sidString) + { + if (sidString == "S-1-5-21-1234567890-1234567890-1234567890-1001") + return "TestDomain\\TestUser"; + return null; + } + } +} \ No newline at end of file