Python Bindings: Windows Security Descriptors
The win-sd Python package (win_sd) provides PEP 561 typed bindings for
the win-sd Rust crate. Install with maturin or pip.
Installation
# From the repository
cd crates/win-sd-python
maturin develop
# Or build a wheel
maturin build --release
pip install target/wheels/win_sd-*.whl
Quick start
from win_sd import (
Sid, AccessMask, SecurityDescriptorBuilder, SecurityDescriptor,
IntegrityLevel, IntegrityPolicy, AccessToken, Privilege,
GenericMapping, Guid, DomainContext,
)
# Build a security descriptor
sd = (SecurityDescriptorBuilder()
.owner(Sid.local_system())
.group(Sid.administrators())
.deny(Sid.anonymous(), AccessMask.FILE_ALL_ACCESS)
.allow(Sid.administrators(), AccessMask.FILE_ALL_ACCESS)
.allow(Sid.everyone(), AccessMask.FILE_GENERIC_READ)
.build())
# Generate and parse SDDL
sddl = sd.to_sddl()
recovered = SecurityDescriptor.from_sddl(sddl)
assert recovered.owner == sd.owner
# Simple access check
result = sd.check_access([Sid.administrators(), Sid.everyone()],
AccessMask.FILE_GENERIC_READ)
assert result.granted
# Full access check with token
token = (AccessToken(Sid.administrators())
.with_groups([Sid.everyone()])
.with_integrity(IntegrityLevel.HIGH))
result = sd.check_access_full(token, AccessMask.FILE_GENERIC_READ)
assert result.granted
SIDs
from win_sd import Sid
# Parse from string
sid = Sid("S-1-5-32-544")
assert str(sid) == "S-1-5-32-544"
assert sid == Sid.administrators()
# Well-known SIDs
everyone = Sid.everyone()
system = Sid.local_system()
anon = Sid.anonymous()
users = Sid.users()
auth = Sid.authenticated_users()
Access masks
from win_sd import AccessMask
# Named constants
read = AccessMask.FILE_GENERIC_READ
write = AccessMask.FILE_GENERIC_WRITE
all_access = AccessMask.FILE_ALL_ACCESS
# Bitwise operations
rw = read | write
assert rw.contains(read)
assert rw.contains(write)
# Hex representation
print(all_access) # "0x001F01FF"
# Emptiness: idiomatic Python uses truthiness
mask = AccessMask.NONE
assert not mask # Pythonic
assert mask.is_empty() # explicit alternative
assert bool(AccessMask.FILE_READ_DATA) # non-empty is truthy
GUIDs
from win_sd import Guid
guid = Guid("bf967aba-0de6-11d0-a285-00aa003049e2")
assert str(guid) == "bf967aba-0de6-11d0-a285-00aa003049e2"
assert guid != Guid.ZERO
ACE types and entries
All 21 MS-DTYP ACE type variants are available as class constants:
from win_sd import Ace, AceType, AceFlags, AccessMask, Sid, Guid
# Basic ACEs
allow = Ace.allow(Sid.everyone(), AccessMask.FILE_GENERIC_READ)
deny = Ace.deny(Sid.anonymous(), AccessMask.FILE_ALL_ACCESS)
audit = Ace.audit(Sid.everyone(), AccessMask.FILE_WRITE_DATA)
# Object ACEs with GUIDs
guid = Guid("bf967aba-0de6-11d0-a285-00aa003049e2")
obj_ace = Ace.allow_object(Sid.administrators(), AccessMask(0x0010), guid, None)
assert obj_ace.ace_type.is_object_type
assert obj_ace.object_type == guid
# Mandatory integrity label ACEs
from win_sd import IntegrityLevel, IntegrityPolicy
label = Ace.mandatory_label(IntegrityLevel.HIGH, IntegrityPolicy.NO_WRITE_UP)
assert label.integrity_level() == IntegrityLevel.HIGH
assert label.ace_type.is_mandatory_label
# Scoped policy ACEs
sp = Ace.scoped_policy(Sid("S-1-5-21-100-200-300-999"))
assert sp.scoped_policy_sid() is not None
# ACE type classification
assert AceType.ACCESS_ALLOWED.is_allow
assert AceType.ACCESS_DENIED_OBJECT.is_deny
assert AceType.ACCESS_DENIED_OBJECT.is_object_type
assert AceType.ACCESS_ALLOWED_CALLBACK.is_callback
assert AceType.SYSTEM_MANDATORY_LABEL.is_sacl_type
# Validation
ace = Ace.allow(Sid.everyone(), AccessMask.FILE_GENERIC_READ)
ace.validate() # raises ValueError on structural issues
Security descriptor builder
from win_sd import (
SecurityDescriptorBuilder, AccessMask, Sid, AceFlags,
IntegrityLevel, IntegrityPolicy, Guid,
)
sd = (SecurityDescriptorBuilder()
.owner(Sid.local_system())
.group(Sid.administrators())
# Basic ACEs
.deny(Sid.anonymous(), AccessMask.FILE_ALL_ACCESS)
.allow(Sid.administrators(), AccessMask.FILE_ALL_ACCESS)
# ACEs with inheritance flags
.allow_with_flags(
Sid.everyone(),
AccessMask.FILE_GENERIC_READ,
AceFlags.CONTAINER_INHERIT | AceFlags.OBJECT_INHERIT,
)
# Object ACEs with GUIDs
.allow_object(
Sid.administrators(),
AccessMask(0x0010),
Guid("bf967aba-0de6-11d0-a285-00aa003049e2"),
None,
)
# Mandatory integrity label
.mandatory_label(IntegrityLevel.HIGH, IntegrityPolicy.NO_WRITE_UP)
# Protected DACL
.protected_dacl(True)
.build())
sd.validate() # raises ValueError if invalid
Access checks
Three levels of access check, from simple to full:
from win_sd import (
SecurityDescriptorBuilder, SecurityDescriptor, AccessMask, Sid,
Guid, AccessToken, Privilege, IntegrityLevel, GenericMapping,
)
sd = (SecurityDescriptorBuilder()
.owner(Sid.local_system())
.deny(Sid.anonymous(), AccessMask.FILE_ALL_ACCESS)
.allow(Sid.administrators(), AccessMask.FILE_ALL_ACCESS)
.allow(Sid.everyone(), AccessMask.FILE_GENERIC_READ)
.build())
# Simple: SID list + desired mask
result = sd.check_access(
[Sid.administrators(), Sid.everyone()],
AccessMask.FILE_GENERIC_READ,
)
assert result.granted
# Object: adds GUID filtering for Object ACEs
result = sd.check_access_object(
[Sid.administrators()],
AccessMask(0x0010),
Guid("bf967aba-0de6-11d0-a285-00aa003049e2"),
)
# Full: token with integrity, privileges, generic mapping
token = (AccessToken(Sid.administrators())
.with_groups([Sid.everyone()])
.with_integrity(IntegrityLevel.HIGH)
.with_privilege(Privilege.SE_SECURITY_PRIVILEGE))
result = sd.check_access_full(
token,
AccessMask.FILE_ALL_ACCESS,
None, # no object type GUID
GenericMapping.file(), # expand generic rights
)
assert result.granted
print(f"Granted: {result.granted_mask}, Denied: {result.denied_mask}")
SDDL with domain context
from win_sd import Sid, DomainContext, SecurityDescriptor
# Domain SID must be S-1-5-21-X-Y-Z format
domain = DomainContext(Sid("S-1-5-21-100-200-300"))
# Parse with domain-relative aliases (DA = Domain Admins)
sd = SecurityDescriptor.from_sddl_with_domain(
"O:DAG:DUD:(A;;FA;;;DA)", domain
)
assert sd.owner.rid == 512 # Domain Admins RID
# Generate uses domain aliases where possible
sddl = sd.to_sddl_with_domain(domain)
assert "DA" in sddl
SDDL alias functions
from win_sd import sid_from_alias, sid_to_alias, rights_from_codes, rights_to_codes, AccessMask
# SID aliases
sid = sid_from_alias("BA") # BUILTIN\Administrators
assert sid_to_alias(sid) == "BA"
# Rights aliases
mask = rights_from_codes("GRGW")
assert mask.contains(AccessMask.GENERIC_READ)
assert rights_to_codes(AccessMask.FILE_ALL_ACCESS) == "FA"
Binary serialization
from win_sd import SecurityDescriptor, SecurityDescriptorBuilder, AccessMask, Sid
sd = (SecurityDescriptorBuilder()
.owner(Sid.local_system())
.allow(Sid.everyone(), AccessMask.FILE_GENERIC_READ)
.build())
# To/from bytes (self-relative format)
data = sd.to_bytes()
recovered = SecurityDescriptor.from_bytes(data)
assert recovered.owner == sd.owner
Conditional expressions
Callback ACEs carry conditional expressions that are evaluated during
access checks. The ConditionalExpr class handles parsing, generation,
binary serialization, and evaluation.
Parsing from SDDL text
from win_sd import ConditionalExpr
# Simple comparison
expr = ConditionalExpr.from_sddl('@User.dept == "Sales"')
print(expr.to_sddl()) # @User.dept == "Sales"
# Membership check
expr = ConditionalExpr.from_sddl("Member_of{SID(BA)}")
# Logical operators
expr = ConditionalExpr.from_sddl(
'@User.dept == "Sales" && Exists @User.clearance'
)
# Composite literals
expr = ConditionalExpr.from_sddl(
'@User.roles Contains {"admin", "auditor"}'
)
# Not / existence
expr = ConditionalExpr.from_sddl("!(@User.disabled == 1)")
expr = ConditionalExpr.from_sddl("Exists @Resource.classification")
Supported SDDL syntax:
| Element | Examples |
|---|---|
| Attribute references | @User.name, @Resource.name, @Device.name, @Local.name |
| Comparison | ==, !=, <, <=, >, >= |
| Set operators | Contains, Any_of |
| Logical | &&, ||, !(...) |
| Existence | Exists @User.attr |
| Membership | Member_of{SID(BA)}, Not_Member_of{...}, Member_of_Any{...}, Device_Member_of{...} and 4 more variants |
| Literals | integers, "strings", SID(BA), SID(S-1-5-...), {"a", "b"} |
Binary format
Conditional expressions use the MS-DTYP binary RPN format (prefixed with
the bytes 0x61 0x72 0x74 0x78). Round-trip through binary:
from win_sd import ConditionalExpr, parse_conditional_binary, write_conditional_binary
expr = ConditionalExpr.from_sddl('@User.dept == "Engineering"')
# Serialize to binary
data = expr.to_bytes()
assert isinstance(data, bytes)
# Parse back
expr2 = ConditionalExpr.from_bytes(data)
assert expr2.to_sddl() == expr.to_sddl()
# Module-level convenience functions
data = write_conditional_binary(expr)
expr3 = parse_conditional_binary(data)
Evaluation
evaluate() takes a plain Python dict and returns True, False, or
None (unknown — attribute missing or type mismatch):
from win_sd import ConditionalExpr
expr = ConditionalExpr.from_sddl('@User.dept == "Sales"')
# Attribute match
assert expr.evaluate({"user": {"dept": "Sales"}}) is True
assert expr.evaluate({"user": {"dept": "Engineering"}}) is False
# Missing attribute → Unknown
assert expr.evaluate({"user": {}}) is None
The context dict accepts these keys:
| Key | Value type | Purpose |
|---|---|---|
"user" | dict[str, str] | User claim attributes |
"resource" | dict[str, str] | Resource attributes |
"device" | dict[str, str] | Device claim attributes |
"local" | dict[str, str] | Local attributes |
"groups" | list[str] | SID strings for Member_of checks |
from win_sd import ConditionalExpr
# Membership check
expr = ConditionalExpr.from_sddl("Member_of{SID(BA)}")
assert expr.evaluate({"groups": ["S-1-5-32-544"]}) is True
assert expr.evaluate({"groups": ["S-1-5-32-545"]}) is False
# Existence check
expr = ConditionalExpr.from_sddl("Exists @User.clearance")
assert expr.evaluate({"user": {"clearance": "secret"}}) is True
assert expr.evaluate({"user": {}}) is False
# Logical AND
expr = ConditionalExpr.from_sddl(
'@User.dept == "Sales" && @User.region == "EMEA"'
)
assert expr.evaluate({"user": {"dept": "Sales", "region": "EMEA"}}) is True
assert expr.evaluate({"user": {"dept": "Sales", "region": "APAC"}}) is False
Limitations: The Python evaluate context passes attribute values as
strings. Integer comparisons (>=, <, etc.) against SDDL integer
literals will return None because the types don’t match. For integer
comparisons, use the Rust API with a custom AttributeContext.
Extracting conditions from callback ACEs
The Ace.condition property parses the conditional expression from a
callback ACE’s application data:
from win_sd import SecurityDescriptor
sd = SecurityDescriptor.from_sddl(
'D:(XA;;FR;;;WD;(@User.dept == "Sales"))'
)
ace = sd.dacl.entries()[0]
if ace.ace_type.is_callback:
cond = ace.condition
if cond is not None:
print(cond.to_sddl()) # @User.dept == "Sales"
str/repr
str(expr) returns the SDDL text form. repr(expr) returns
ConditionalExpr('...').
from win_sd import ConditionalExpr
expr = ConditionalExpr.from_sddl("Exists @User.clearance")
print(str(expr)) # Exists @User.clearance
print(repr(expr)) # ConditionalExpr('Exists @User.clearance')
acls-rs bridge: PermissionMapping
The bridge connects Windows access masks to acls-rs PermissionSet
values. A PermissionMapping defines how bitmask values translate
to named permissions — different object types need different mappings
because the same bit positions carry different semantics.
Pre-defined mappings
from win_sd import PermissionMapping
file_map = PermissionMapping.file() # namespace "win"
ad_map = PermissionMapping.ad() # namespace "ad"
reg_map = PermissionMapping.registry() # namespace "reg"
print(ad_map.namespace) # "ad"
| Mapping | Namespace | Bit 0x0010 means | Bit 0x0020 means |
|---|---|---|---|
file() | "win" | file_write_ea | file_execute |
ad() | "ad" | read_prop | write_prop |
registry() | "reg" | notify | create_link |
Custom mappings
from win_sd import PermissionMapping
custom = (PermissionMapping("myapp")
.add(0x01, "view")
.add(0x02, "edit")
.add(0x04, "admin"))
perms = custom.to_permission_set(0x05) # view + admin
assert custom.from_permission_set(perms) == 0x05
AccessMask with mapping
The to_permission_set() and from_permission_set() methods accept
an optional mapping parameter. Without it, the default file mapping
is used:
from win_sd import AccessMask, PermissionMapping, AtomicPermission
# Default: file semantics
mask = AccessMask(0x0010)
file_perms = mask.to_permission_set()
assert AtomicPermission("win", "file_write_ea") in file_perms
# AD semantics: same bit, different meaning
ad_perms = mask.to_permission_set(PermissionMapping.ad())
assert AtomicPermission("ad", "read_prop") in ad_perms
# Round-trip through a mapping
ad = PermissionMapping.ad()
mask = AccessMask(0x0030) # READ_PROP | WRITE_PROP
perms = mask.to_permission_set(ad)
recovered = AccessMask.from_permission_set(perms, ad)
assert recovered == mask
SecurityDescriptor with mapping
from win_sd import (
AccessMask, PermissionMapping, SecurityDescriptorBuilder, Sid,
AtomicPermission,
)
sd = (SecurityDescriptorBuilder()
.owner(Sid.administrators())
.allow(Sid.everyone(), AccessMask(0x000F01B7))
.build())
# Effective permissions with AD semantics
ad = PermissionMapping.ad()
perms = sd.effective_permissions([Sid.everyone()], ad)
assert AtomicPermission("ad", "read_prop") in perms
assert AtomicPermission("ad", "write_prop") in perms
assert AtomicPermission("ad", "create_child") in perms
# GrantDenialPair with AD semantics
gd = sd.grant_denial_pair([Sid.everyone()], ad)
for p in gd.effective_permissions():
print(p) # ad:read_prop, ad:write_prop, ...
PermissionSet and GrantDenialPair
from win_sd import (
AtomicPermission, PermissionSet, GrantDenialPair,
)
# Build sets
ps = PermissionSet()
ps.insert(AtomicPermission("ad", "read_prop"))
ps.insert(AtomicPermission("ad", "write_prop"))
assert len(ps) == 2
assert AtomicPermission("ad", "read_prop") in ps
# Set operations
a = PermissionSet()
a.insert(AtomicPermission("ad", "read_prop"))
a.insert(AtomicPermission("ad", "write_prop"))
b = PermissionSet()
b.insert(AtomicPermission("ad", "write_prop"))
b.insert(AtomicPermission("ad", "delete_child"))
union = a | b # 3 permissions
inter = a & b # write_prop only
diff = a - b # read_prop only
# Iteration
for perm in a:
print(f"{perm.namespace()}:{perm.action()}")
# GrantDenialPair
grants = PermissionSet()
grants.insert(AtomicPermission("ad", "read_prop"))
grants.insert(AtomicPermission("ad", "write_prop"))
denials = PermissionSet()
denials.insert(AtomicPermission("ad", "write_prop"))
gd = GrantDenialPair(grants, denials)
effective = gd.effective_permissions()
assert AtomicPermission("ad", "read_prop") in effective
assert AtomicPermission("ad", "write_prop") not in effective
ACL inheritance
from win_sd import SecurityDescriptorBuilder, AccessMask, Sid, AceFlags
parent = (SecurityDescriptorBuilder()
.owner(Sid.local_system())
.allow_with_flags(
Sid.administrators(),
AccessMask.FILE_ALL_ACCESS,
AceFlags.CONTAINER_INHERIT | AceFlags.OBJECT_INHERIT,
)
.build())
# Create a child directory SD with inherited ACEs
child = parent.create_child(is_container=True)
assert child.dacl is not None
assert child.dacl.entries()[0].is_inherited
Principal model and policy
from win_sd import (
Sid, AccessMask, PrincipalModel, SdPolicyBuilder,
)
# Define principals and groups
domain = Sid("S-1-5-21-100-200-300")
alice = Sid("S-1-5-21-100-200-300-1001")
admins = Sid("S-1-5-21-100-200-300-512")
model = PrincipalModel()
model.add_group(admins, "Domain Admins")
model.add_principal(alice, "alice", [admins])
# Build a policy and generate an SD
builder = SdPolicyBuilder()
builder.owner(Sid.local_system())
builder.group(Sid.administrators())
builder.allow(admins, AccessMask.FILE_ALL_ACCESS)
policy = builder.build()
sd = policy.generate(model)
sd.validate()
Integrity levels and tokens
from win_sd import IntegrityLevel, IntegrityPolicy, AccessToken, Sid, Privilege
# Levels with natural ordering
assert IntegrityLevel.LOW != IntegrityLevel.HIGH
# Convert to/from SID
sid = IntegrityLevel.MEDIUM.to_sid()
level = IntegrityLevel.from_sid(sid)
assert level == IntegrityLevel.MEDIUM
# Build tokens with integrity and privileges
token = (AccessToken(Sid("S-1-5-21-100-200-300-1001"))
.with_groups([Sid.administrators(), Sid.everyone()])
.with_integrity(IntegrityLevel.HIGH)
.with_privilege(Privilege.SE_SECURITY_PRIVILEGE)
.with_privilege(Privilege.SE_TAKE_OWNERSHIP_PRIVILEGE))
assert token.integrity_level == IntegrityLevel.HIGH
print(token.all_sids())
Generic mapping
from win_sd import AccessMask, GenericMapping
# Expand generic rights to object-specific bits
mapping = GenericMapping.file()
expanded = mapping.map_mask(AccessMask.GENERIC_READ)
assert expanded.contains(AccessMask.FILE_READ_DATA)
# Pre-defined mappings
_ = GenericMapping.directory()
_ = GenericMapping.registry_key()
WinAcl operations
from win_sd import SecurityDescriptorBuilder, AccessMask, Sid
sd = (SecurityDescriptorBuilder()
.allow(Sid.everyone(), AccessMask.FILE_GENERIC_READ)
.deny(Sid.anonymous(), AccessMask.FILE_ALL_ACCESS)
.build())
dacl = sd.dacl
print(f"{dacl.ace_count()} ACEs, canonical: {dacl.is_canonical()}")
# Get a canonically sorted copy
sorted_dacl = dacl.canonicalized()
assert sorted_dacl.is_canonical()
Type stubs
The package ships PEP 561 type stubs (__init__.pyi) with full signatures
for all 21 classes and 4 module functions. Type checkers (mypy, pyright)
will see complete type information.
# mypy will check these types at analysis time
from win_sd import Sid, AccessMask, AccessToken, SecurityDescriptor
def check_write(sd: SecurityDescriptor, token: AccessToken) -> bool:
result = sd.check_access_full(token, AccessMask.FILE_WRITE_DATA)
return result.granted