Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ElementExamples
Attribute references@User.name, @Resource.name, @Device.name, @Local.name
Comparison==, !=, <, <=, >, >=
Set operatorsContains, Any_of
Logical&&, ||, !(...)
ExistenceExists @User.attr
MembershipMember_of{SID(BA)}, Not_Member_of{...}, Member_of_Any{...}, Device_Member_of{...} and 4 more variants
Literalsintegers, "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:

KeyValue typePurpose
"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"
MappingNamespaceBit 0x0010 meansBit 0x0020 means
file()"win"file_write_eafile_execute
ad()"ad"read_propwrite_prop
registry()"reg"notifycreate_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