pax_global_header 0000666 0000000 0000000 00000000064 15150254773 0014523 g ustar 00root root 0000000 0000000 52 comment=9c9b93b769dbc93439fda51d9554597ec75e1872
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/ 0000775 0000000 0000000 00000000000 15150254773 0021454 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/.gitignore 0000664 0000000 0000000 00000001100 15150254773 0023434 0 ustar 00root root 0000000 0000000 # If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
sddl
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# other tools
.devenv/
.devenv.*
.envrc
devenv.*
.direnv
.task
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/LICENSE 0000664 0000000 0000000 00000016744 15150254773 0022475 0 ustar 00root root 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/README.md 0000664 0000000 0000000 00000011136 15150254773 0022735 0 ustar 00root root 0000000 0000000 # sddl - Windows Security Descriptor Library and CLI Tool
A cross-platform Go library and command-line tool for working with Windows Security Descriptors, providing conversion between binary and SDDL (Security Descriptor Definition Language) string formats.
## Features
- Convert between binary and SDDL string formats
- Read security descriptors directly from files on Windows systems
- Support for all Security Descriptor components:
- Owner and Group SIDs
- DACLs and SACLs
- All standard ACE types
- Inheritance flags
- ACL control flags
- Translation of well-known SIDs to aliases (e.g., "SY" for SYSTEM)
- Translation of common access masks to symbolic form (e.g., "FA" for Full Access)
- Cross-platform library functionality
- Windows-specific features when available
- Pure Go implementation with minimal dependencies
## CLI Tool Usage
The command-line tool provides several modes of operation:
### Basic Usage
```bash
# Convert base64-encoded binary descriptor to SDDL string (reads from stdin, writes to stdout)
echo "AQAAgBQAAAAkAAAAAAAAABAAAAAQAAQACAAEABIAAAA=" | sddl -i binary -o string > output.txt
# Convert SDDL string to base64-encoded binary (reads from stdin, writes to stdout)
echo "O:SYG:SY" | sddl -i string -o binary > binary_output.txt
# Read security descriptors from files (Windows only, filenames from stdin)
echo "C:\Windows\notepad.exe" | sddl -file > security_descriptors.txt
```
### Input/Output Formats
- `-i format`: Input format, either 'binary' (base64 encoded) or 'string' (SDDL)
- `-o format`: Output format, either 'binary' (base64 encoded) or 'string' (SDDL)
- `-file`: Process input as filenames and read their security descriptors (Windows only)
- `-debug`: Prints the result in a human-readable format (applies only when `-o string` is used)
### Examples
```bash
# Convert binary to SDDL
echo "AQAAgBQAAAAkAAAAAAAAABAAAAAQAAQACAAEABIAAAA=" | sddl -i binary -o string
# Output: O:SYG:SY
# Convert SDDL to binary
echo "O:SYG:SY" | sddl -i string -o binary
# Output: AQAAgBQAAAAkAAAAAAAAABAAAAAQAAQACAAEABIAAAA=
# Get security descriptor from files (Windows only)
echo "C:\Windows\notepad.exe" | sddl -file -o string
# Output: O:SYG:BAD:(A;;FA;;;SY)
```
### Processing Rules
- Reads input line by line from stdin
- Each line should contain either a single security descriptor or filename
- Empty lines are ignored
- Processing continues even if some lines fail
- Errors are reported to stderr with line numbers
- Results are written to stdout, one per line
## Library Usage
### Installation
```bash
go get github.com/cloudsoda/sddl
```
### Basic Usage
```go
import "github.com/cloudsoda/sddl"
// Parse binary security descriptor
sd, err := sddl.FromBinary(binaryData)
if err != nil {
// Handle error
}
sddlString, err := sd.String()
// Parse SDDL string
sd, err := sddl.FromString("O:SYG:BAD:(A;;FA;;;SY)")
if err != nil {
// Handle error
}
binaryData, err := sd.Binary()
```
### Windows-Specific Features
```go
// Windows only: Get security descriptor from file
sddlString, err := GetFileSDString("C:\\Windows\\notepad.exe")
// Windows only: Get binary security descriptor from file
base64Data, err := GetFileSecurityBase64("C:\\Windows\\notepad.exe")
```
## SDDL Format
Security descriptors in SDDL format follow this structure:
```
O:owner_sidG:group_sidD:dacl_flagsS:sacl_flags
```
Components:
- Owner SID (`O:`): Specifies the owner
- Group SID (`G:`): Specifies the primary group
- DACL (`D:`): Discretionary Access Control List
- SACL (`S:`): System Access Control List
### ACL Format
ACLs contain flags and a list of ACEs (Access Control Entries):
```
D:flags(ace1)(ace2)...(aceN)
```
ACL Flags:
- `P`: Protected
- `AI`: Auto-inherited
- `AR`: Auto-inherit required
- `NO`: No propagate inherit
### ACE Format
Each ACE follows this format:
```
(ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid)
```
Components:
- `ace_type`: Type of ACE (e.g., "A" for Allow, "D" for Deny)
- `ace_flags`: Inheritance flags (e.g., "CI" for Container Inherit)
- `rights`: Access rights (e.g., "FA" for Full Access)
- `account_sid`: Security identifier for the trustee
## Error Handling
The library provides detailed error information for various scenarios:
- Invalid security descriptor structure
- Malformed SIDs
- Invalid ACL or ACE formats
- Base64 decoding errors
- File access errors (Windows-specific features)
Errors include context about where in the parsing process they occurred to aid in debugging.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/ 0000775 0000000 0000000 00000000000 15150254773 0022217 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/sddl/ 0000775 0000000 0000000 00000000000 15150254773 0023145 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/sddl/main.go 0000664 0000000 0000000 00000006411 15150254773 0024422 0 ustar 00root root 0000000 0000000 package main
import (
"bufio"
"encoding/base64"
"flag"
"fmt"
"os"
"strings"
"github.com/cloudsoda/sddl"
)
type config struct {
inputFormat string
outputFormat string
fileMode bool
debug bool
}
func main() {
cfg := parseFlags()
if err := processInput(cfg); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func parseFlags() config {
cfg := config{}
flag.StringVar(&cfg.inputFormat, "i", "binary", "Input format: 'binary' (base64 encoded) or 'string'")
flag.StringVar(&cfg.outputFormat, "o", "string", "Output format: 'binary' (base64 encoded) or 'string'")
flag.BoolVar(&cfg.fileMode, "file", false, "Process input as filenames and read their security descriptors using native Windows API calls")
flag.BoolVar(&cfg.debug, "debug", false, "Enable debugging output (applies only if -o string is set)")
flag.Parse()
// Validate input format
cfg.inputFormat = strings.ToLower(cfg.inputFormat)
if cfg.inputFormat != "binary" && cfg.inputFormat != "string" {
fmt.Fprintf(os.Stderr, "invalid input format: %s (must be 'binary' or 'string')\n", cfg.inputFormat)
flag.Usage()
os.Exit(1)
}
// Validate output format
cfg.outputFormat = strings.ToLower(cfg.outputFormat)
if cfg.outputFormat != "binary" && cfg.outputFormat != "string" {
fmt.Fprintf(os.Stderr, "invalid output format: %s (must be 'binary' or 'string')\n", cfg.outputFormat)
flag.Usage()
os.Exit(1)
}
// Input format is ignored in file mode
if cfg.fileMode && cfg.inputFormat != "binary" {
fmt.Fprintln(os.Stderr, "warning: input format is ignored in file mode")
}
return cfg
}
func processInput(cfg config) error {
scanner := bufio.NewScanner(os.Stdin)
lineNum := 0
for scanner.Scan() {
lineNum++
input := scanner.Text()
// Skip empty lines
if strings.TrimSpace(input) == "" {
continue
}
if cfg.fileMode {
// Process input as filename
var output string
var err error
if cfg.outputFormat == "binary" {
output, err = GetFileSecurityBase64(input)
} else {
output, err = GetFileSDString(input)
}
if err != nil {
fmt.Fprintf(os.Stderr, "line %d: error processing file %q: %v\n", lineNum, input, err)
continue
}
fmt.Println(output)
continue
}
// Process security descriptor input
var sd *sddl.SecurityDescriptor
var err error
// Parse input based on format
switch cfg.inputFormat {
case "binary":
data, err := base64.StdEncoding.DecodeString(input)
if err != nil {
fmt.Fprintf(os.Stderr, "line %d: error decoding base64: %v\n", lineNum, err)
continue
}
sd, err = sddl.FromBinary(data)
if err != nil {
fmt.Fprintf(os.Stderr, "line %d: error parsing security descriptor: %v\n", lineNum, err)
continue
}
case "string":
sd, err = sddl.FromString(input)
if err != nil {
fmt.Fprintf(os.Stderr, "line %d: error parsing security descriptor string: %v\n", lineNum, err)
continue
}
}
// Generate output based on format
switch cfg.outputFormat {
case "binary":
fmt.Println(base64.StdEncoding.EncodeToString(sd.Binary()))
case "string":
if cfg.debug {
fmt.Println(sd.StringIndent(0))
} else {
fmt.Println(sd.String())
}
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading input: %w", err)
}
return nil
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/sddl/main_linux.go 0000664 0000000 0000000 00000000720 15150254773 0025636 0 ustar 00root root 0000000 0000000 //go:build !windows
package main
import (
"errors"
)
// GetFileSecurityBase64 retrieves a file's security descriptor in base64-encoded format.
func GetFileSecurityBase64(filename string) (string, error) {
return "", errors.New("not implemented on this platform")
}
// GetFileSDString retrieves a file's security descriptor as a SDDL string.
func GetFileSDString(filename string) (string, error) {
return "", errors.New("not implemented on this platform")
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/sddl/main_windows.go 0000664 0000000 0000000 00000020222 15150254773 0026170 0 ustar 00root root 0000000 0000000 //go:build windows
package main
import (
"encoding/base64"
"fmt"
"os"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var (
advapi32 = windows.NewLazyDLL("advapi32.dll")
convertSecurityDescriptorToStringSecurityDescriptorW = advapi32.NewProc("ConvertSecurityDescriptorToStringSecurityDescriptorW")
convertStringSecurityDescriptorToSecurityDescriptorW = advapi32.NewProc("ConvertStringSecurityDescriptorToSecurityDescriptorW")
getSecurityInfo = advapi32.NewProc("GetSecurityInfo")
getSecurityDescriptorLength = advapi32.NewProc("GetSecurityDescriptorLength")
getSecurityDescriptorControl = advapi32.NewProc("GetSecurityDescriptorControl")
makeSelfRelativeSD = advapi32.NewProc("MakeSelfRelativeSD")
openProcessToken = advapi32.NewProc("OpenProcessToken")
lookupPrivilegeValueW = advapi32.NewProc("LookupPrivilegeValueW")
adjustTokenPrivileges = advapi32.NewProc("AdjustTokenPrivileges")
)
const (
OWNER_SECURITY_INFORMATION = 0x00000001
GROUP_SECURITY_INFORMATION = 0x00000002
DACL_SECURITY_INFORMATION = 0x00000004
SACL_SECURITY_INFORMATION = 0x00000008
SE_SECURITY_NAME = "SeSecurityPrivilege"
TOKEN_ADJUST_PRIVILEGES = 0x0020
TOKEN_QUERY = 0x0008
// Adding missing constants
READ_CONTROL = 0x00020000
ACCESS_SYSTEM_SECURITY = 0x01000000
// Security descriptor control flags
SE_SELF_RELATIVE = 0x8000
)
type LUID struct {
LowPart uint32
HighPart int32
}
type LUID_AND_ATTRIBUTES struct {
Luid LUID
Attributes uint32
}
type TOKEN_PRIVILEGES struct {
PrivilegeCount uint32
Privileges [1]LUID_AND_ATTRIBUTES
}
func enableSecurityPrivilege() error {
var token windows.Token
currentProcess := windows.CurrentProcess()
// Get process token
ret, _, err := openProcessToken.Call(
uintptr(currentProcess),
uintptr(TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY),
uintptr(unsafe.Pointer(&token)),
)
if ret == 0 {
return fmt.Errorf("OpenProcessToken failed: %v", err)
}
defer token.Close()
// Lookup the privilege value
var luid LUID
privName, err := syscall.UTF16PtrFromString(SE_SECURITY_NAME)
if err != nil {
return fmt.Errorf("UTF16PtrFromString failed: %v", err)
}
ret, _, err = lookupPrivilegeValueW.Call(
0,
uintptr(unsafe.Pointer(privName)),
uintptr(unsafe.Pointer(&luid)),
)
if ret == 0 {
return fmt.Errorf("LookupPrivilegeValue failed: %v", err)
}
// Prepare token privileges
var tp TOKEN_PRIVILEGES
tp.PrivilegeCount = 1
tp.Privileges[0].Luid = luid
tp.Privileges[0].Attributes = 0x00000002 // SE_PRIVILEGE_ENABLED
// Adjust token privileges
ret, _, err = adjustTokenPrivileges.Call(
uintptr(token),
0,
uintptr(unsafe.Pointer(&tp)),
0,
0,
0,
)
if ret == 0 {
return fmt.Errorf("AdjustTokenPrivileges failed: %v", err)
}
return nil
}
func getSecurityDescriptorPointerAndInfo(filename string) (uintptr, int, error) {
// Open the file to get a handle
pathPtr, err := syscall.UTF16PtrFromString(filename)
if err != nil {
return 0, 0, fmt.Errorf("Error converting filename: %w", err)
}
// Check if path is a directory
attrs, err := syscall.GetFileAttributes(pathPtr)
if err != nil {
return 0, 0, fmt.Errorf("Error getting file attributes: %w", err)
}
var fileFlags uint32 = syscall.FILE_ATTRIBUTE_NORMAL
if attrs&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
fileFlags = syscall.FILE_FLAG_BACKUP_SEMANTICS
}
handle, err := syscall.CreateFile(
pathPtr,
READ_CONTROL|ACCESS_SYSTEM_SECURITY,
syscall.FILE_SHARE_READ,
nil,
syscall.OPEN_EXISTING,
fileFlags,
0,
)
if err != nil {
return 0, 0, fmt.Errorf("Error opening file: %w", err)
}
defer syscall.CloseHandle(handle)
// Get the security descriptor
var pSD, pOwner, pGroup, pDacl, pSacl uintptr
secInfo := OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION
ret, _, err := getSecurityInfo.Call(
uintptr(handle),
uintptr(1), // SE_FILE_OBJECT
uintptr(secInfo),
uintptr(unsafe.Pointer(&pOwner)),
uintptr(unsafe.Pointer(&pGroup)),
uintptr(unsafe.Pointer(&pDacl)),
uintptr(unsafe.Pointer(&pSacl)),
uintptr(unsafe.Pointer(&pSD)),
)
// If failed, try without SACL
if ret != 0 {
fmt.Fprintf(os.Stderr, "Warning: Could not get full security info, trying without SACL...\n")
secInfo = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
ret, _, err = getSecurityInfo.Call(
uintptr(handle),
uintptr(1), // SE_FILE_OBJECT
uintptr(secInfo),
uintptr(unsafe.Pointer(&pOwner)),
uintptr(unsafe.Pointer(&pGroup)),
uintptr(unsafe.Pointer(&pDacl)),
0,
uintptr(unsafe.Pointer(&pSD)),
)
if ret != 0 {
return 0, 0, fmt.Errorf("GetSecurityInfo failed: %w", err)
}
}
return pSD, secInfo, nil
}
// GetFileSDBytes retrieves a file's security descriptor in binary form.
// It uses direct Windows API calls to get the raw SD bytes.
func GetFileSDBytes(filename string) ([]byte, error) {
// Try to enable security privilege
err := enableSecurityPrivilege()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Could not enable security privilege: %v\n", err)
fmt.Fprintf(os.Stderr, "Will try to continue with reduced privileges...\n")
}
pSD, _, err := getSecurityDescriptorPointerAndInfo(filename)
if err != nil {
return nil, err
}
// Check if the security descriptor is already self-relative
var control uint16
var revision uint32
ret, _, err := getSecurityDescriptorControl.Call(
pSD,
uintptr(unsafe.Pointer(&control)),
uintptr(unsafe.Pointer(&revision)),
)
if ret == 0 {
return nil, fmt.Errorf("GetSecurityDescriptorControl failed: %w", err)
}
var finalSD uintptr
var sdSize uint32
if control&SE_SELF_RELATIVE == 0 {
// First acll to get required buffer size
ret, _, err = makeSelfRelativeSD.Call(
pSD,
0,
uintptr(unsafe.Pointer(&sdSize)),
)
if ret == 0 {
return nil, fmt.Errorf("MakeSelfRelativeSD failed: %v", err)
}
// Allocate buffer
finalSD, err := windows.LocalAlloc(0, sdSize)
if finalSD == 0 {
return nil, fmt.Errorf("LocalAlloc failed: %v", err)
}
defer windows.LocalFree(windows.Handle(finalSD))
// Second call to actually convert
ret, _, err = makeSelfRelativeSD.Call(
pSD,
finalSD,
uintptr(unsafe.Pointer(&sdSize)),
)
if ret == 0 {
return nil, fmt.Errorf("MakeSelfRelativeSD failed (2): %v", err)
}
} else {
finalSD = pSD
length, _, _ := getSecurityDescriptorLength.Call(pSD)
sdSize = uint32(length)
}
// Copy to byte slice and encode
sdBytes := make([]byte, sdSize)
for i := uint32(0); i < sdSize; i++ {
sdBytes[i] = *(*byte)(unsafe.Pointer(finalSD + uintptr(i)))
}
return sdBytes, nil
}
// GetFileSDString retrieves a file's security descriptor as a SDDL string.
// It tries to use the ConvertSecurityDescriptorToStringSecurityDescriptor API
// first for accuracy, but falls back to our SDDL package if that fails.
func GetFileSDString(filename string) (string, error) {
// Try to enable security privilege
err := enableSecurityPrivilege()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Could not enable security privilege: %v\n", err)
fmt.Fprintf(os.Stderr, "Will try to continue with reduced privileges...\n")
}
pSD, secInfo, err := getSecurityDescriptorPointerAndInfo(filename)
if err != nil {
return "", err
}
// Convert to string format (SDDL)
var strPtr *uint16
ret, _, err := convertSecurityDescriptorToStringSecurityDescriptorW.Call(
pSD,
uintptr(1),
uintptr(secInfo),
uintptr(unsafe.Pointer(&strPtr)),
0,
)
if ret == 0 {
return "", fmt.Errorf("ConvertSecurityDescriptorToString failed: %v", err)
}
defer windows.LocalFree(windows.Handle(unsafe.Pointer(strPtr)))
// Convert UTF16 to string and print SDDL
sddl := windows.UTF16PtrToString(strPtr)
return sddl, nil
}
// GetFileSecurityBase64 retrieves a file's security descriptor in base64-encoded format.
func GetFileSecurityBase64(filename string) (string, error) {
sd, err := GetFileSDBytes(filename)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(sd), nil
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/from_binary.go 0000664 0000000 0000000 00000013324 15150254773 0024315 0 ustar 00root root 0000000 0000000 package sddl
import (
"encoding/binary"
"fmt"
)
// FromBinary takes a binary security descriptor in relative format (contiguous memory with offsets)
func FromBinary(data []byte) (*SecurityDescriptor, error) {
dataLen := uint32(len(data))
if dataLen < 20 {
return nil, fmt.Errorf("invalid security descriptor: it must be 20 bytes length at minimum")
}
revision := data[0]
sbzl := data[1]
control := binary.LittleEndian.Uint16(data[2:4])
ownerOffset := binary.LittleEndian.Uint32(data[4:8])
groupOffset := binary.LittleEndian.Uint32(data[8:12])
saclOffset := binary.LittleEndian.Uint32(data[12:16])
daclOffset := binary.LittleEndian.Uint32(data[16:20])
if ownerOffset > 0 && ownerOffset >= dataLen {
return nil, fmt.Errorf("invalid security descriptor: Owner offset 0x%x exceeds data length 0x%x", ownerOffset, dataLen)
}
if groupOffset > 0 && groupOffset >= dataLen {
return nil, fmt.Errorf("invalid security descriptor: Group offset 0x%x exceeds data length 0x%x", groupOffset, dataLen)
}
if saclOffset > 0 && saclOffset >= dataLen {
return nil, fmt.Errorf("invalid security descriptor: SACL offset 0x%x exceeds data length 0x%x", saclOffset, dataLen)
}
if daclOffset > 0 && daclOffset >= dataLen {
return nil, fmt.Errorf("invalid security descriptor: DACL offset 0x%x exceeds data length 0x%x", daclOffset, dataLen)
}
// Parse Owner SID if present
var ownerSID *sid
if ownerOffset > 0 {
sid, err := parseSIDBinary(data[ownerOffset:])
if err != nil {
return nil, fmt.Errorf("error parsing owner SID: %w", err)
}
ownerSID = sid
}
// Parse Group SID if present
var groupSID *sid
if groupOffset > 0 {
sid, err := parseSIDBinary(data[groupOffset:])
if err != nil {
return nil, fmt.Errorf("error parsing group SID: %w", err)
}
groupSID = sid
}
// Parse DACL if present
var dacl *acl
if daclOffset > 0 {
acl, err := parseACLBinary(data[daclOffset:], "D", control)
if err != nil {
return nil, fmt.Errorf("error parsing DACL: %w", err)
}
dacl = acl
}
// Parse SACL if present
var sacl *acl
if saclOffset > 0 {
acl, err := parseACLBinary(data[saclOffset:], "S", control)
if err != nil {
return nil, fmt.Errorf("error parsing SACL: %w", err)
}
sacl = acl
}
return &SecurityDescriptor{
revision: revision,
sbzl: sbzl,
control: control,
ownerOffset: ownerOffset,
groupOffset: groupOffset,
saclOffset: saclOffset,
daclOffset: daclOffset,
ownerSID: ownerSID,
groupSID: groupSID,
dacl: dacl,
sacl: sacl,
}, nil
}
// parseACEBinary takes a binary ACE and returns an ACE struct
func parseACEBinary(data []byte) (*ace, error) {
dataLen := uint16(len(data))
if dataLen < 16 {
return nil, fmt.Errorf("invalid ACE: too short, got %d bytes but need at least 16 (4 for header + 4 for access mask + 8 for SID)", dataLen)
}
aceType := data[0]
aceFlags := data[1]
aceSize := binary.LittleEndian.Uint16(data[2:4])
// Validate full ACE size fits in data provided
if dataLen < aceSize {
return nil, fmt.Errorf("invalid ACE: data length %d doesn't match ACE size %d", dataLen, aceSize)
}
accessMask := binary.LittleEndian.Uint32(data[4:8])
sid, err := parseSIDBinary(data[8:])
if err != nil {
return nil, fmt.Errorf("error parsing ACE SID: %w", err)
}
return &ace{
header: &aceHeader{
aceType: aceType,
aceFlags: aceFlags,
aceSize: aceSize,
},
accessMask: accessMask,
sid: sid,
}, nil
}
// parseACLBinary takes a binary ACL and returns an ACL struct
func parseACLBinary(data []byte, aclType string, control uint16) (*acl, error) {
dataLength := uint16(len(data))
if dataLength < 8 {
return nil, fmt.Errorf("invalid ACL: too short")
}
aclRevision := data[0]
sbzl := data[1]
aclSize := binary.LittleEndian.Uint16(data[2:4])
aceCount := binary.LittleEndian.Uint16(data[4:6])
sbz2 := binary.LittleEndian.Uint16(data[6:8])
var aces []ace
offset := uint16(8)
// Parse each ACE
for i := uint16(0); i < aceCount; i++ {
if offset >= aclSize {
return nil, fmt.Errorf("invalid ACL: offset is bigger than AclSize: offset 0x%x (ACL Size: 0x%x)", offset, aclSize)
}
ace, err := parseACEBinary(data[offset:])
if err != nil {
return nil, fmt.Errorf("error parsing ACE: %w", err)
}
aces = append(aces, *ace)
offset += uint16(ace.header.aceSize)
}
return &acl{
aclRevision: aclRevision,
sbzl: sbzl,
aclSize: aclSize,
aceCount: aceCount,
sbz2: sbz2,
aclType: aclType,
control: control,
aces: aces,
}, nil
}
// parseSIDBinary takes a binary SID and returns a SID struct
func parseSIDBinary(data []byte) (*sid, error) {
if len(data) < 8 {
return nil, fmt.Errorf("invalid SID: it must be at least 8 bytes long")
}
revision := data[0]
subAuthorityCount := int(data[1])
neededLen := 8 + (4 * subAuthorityCount)
if len(data) < neededLen {
return nil, fmt.Errorf("invalid SID: truncated data, got %d bytes but need %d bytes for %d sub-authorities",
len(data), neededLen, subAuthorityCount)
}
if subAuthorityCount > 15 { // Maximum sub-authorities in a valid SID
return nil, fmt.Errorf("invalid SID: too many sub-authorities (%d), maximum is 15", subAuthorityCount)
}
if len(data) < 8+4*subAuthorityCount {
return nil, fmt.Errorf("invalid SID: data too short for sub-authority count")
}
// Parse authority (48 bits)
authority := uint64(0)
for i := 2; i < 8; i++ {
authority = authority<<8 | uint64(data[i])
}
// Parse sub-authorities
subAuthorities := make([]uint32, subAuthorityCount)
for i := 0; i < subAuthorityCount; i++ {
offset := 8 + 4*i
subAuthorities[i] = binary.LittleEndian.Uint32(data[offset : offset+4])
}
return &sid{
revision: revision,
identifierAuthority: authority,
subAuthority: subAuthorities,
}, nil
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/from_binary_test.go 0000664 0000000 0000000 00000061225 15150254773 0025357 0 ustar 00root root 0000000 0000000 package sddl
import (
"testing"
)
func TestParseSIDBinary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
want string
wantErr bool
}{
{
name: "Invalid data - too short",
data: []byte{0x01, 0x02}, // Not enough bytes for a valid SID
want: "",
wantErr: true,
},
{
name: "Invalid data - nil",
data: nil,
want: "",
wantErr: true,
},
{
name: "Invalid data - mismatched length for sub-authorities",
data: []byte{
0x01, // Revision
0x02, // SubAuthorityCount (claims 2 but only has data for 1)
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x01, 0x00, 0x00, 0x00, // One SubAuthority only
},
want: "",
wantErr: true,
},
{
name: "Valid minimal SID",
data: []byte{
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority (NT Authority)
0x01, 0x00, 0x00, 0x00, // SubAuthority[0] = 1 (DIALUP)
},
want: "DU", // Well-known SID for DIALUP
wantErr: false,
},
{
name: "Valid SID with multiple authorities",
data: []byte{
0x01, // Revision
0x02, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority (NT Authority)
0x20, 0x00, 0x00, 0x00, // SubAuthority[0] = 32 (BUILTIN)
0x20, 0x02, 0x00, 0x00, // SubAuthority[1] = 544 (Administrators)
},
want: "BA", // Well-known SID for BUILTIN\Administrators
wantErr: false,
},
{
name: "Non-well-known SID",
data: []byte{
0x01, // Revision
0x05, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x21, 0x00, 0x00, 0x00, // SubAuthority[0]
0x22, 0x00, 0x00, 0x00, // SubAuthority[1]
0x23, 0x00, 0x00, 0x00, // SubAuthority[2]
0x24, 0x00, 0x00, 0x00, // SubAuthority[3]
0x25, 0x00, 0x00, 0x00, // SubAuthority[4]
},
want: "S-1-5-33-34-35-36-37", // Regular SID format
wantErr: false,
},
{
name: "Well-known NT AUTHORITY\\SYSTEM SID",
data: []byte{
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority (NT Authority)
0x12, 0x00, 0x00, 0x00, // SubAuthority[0] = 18 (SYSTEM)
},
want: "SY", // Well-known SID for Local System
wantErr: false,
},
{
name: "Well-known Everyone SID",
data: []byte{
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // World Authority
0x00, 0x00, 0x00, 0x00, // SubAuthority[0] = 0
},
want: "WD", // Well-known SID for Everyone
wantErr: false,
},
{
name: "Maximum sub-authorities",
data: []byte{
0x01, // Revision
0x0F, // SubAuthorityCount (15 is max)
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x01, 0x00, 0x00, 0x00, // SubAuthority[0]
0x02, 0x00, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00,
0x06, 0x00, 0x00, 0x00,
0x07, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00,
0x09, 0x00, 0x00, 0x00,
0x0A, 0x00, 0x00, 0x00,
0x0B, 0x00, 0x00, 0x00,
0x0C, 0x00, 0x00, 0x00,
0x0D, 0x00, 0x00, 0x00,
0x0E, 0x00, 0x00, 0x00,
0x0F, 0x00, 0x00, 0x00,
},
want: "S-1-5-1-2-3-4-5-6-7-8-9-10-11-12-13-14-15",
wantErr: false,
},
{
name: "Too many sub-authorities",
data: []byte{
0x01, // Revision
0x10, // SubAuthorityCount (16 is too many)
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x01, 0x00, 0x00, 0x00, // SubAuthority data...
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sid, err := parseSIDBinary(tt.data)
if tt.wantErr {
if err == nil {
t.Errorf("parseSIDBinary() error = %v, wantErr %v", err, tt.wantErr)
}
if sid != nil {
t.Errorf("parseSIDBinary() sid = %#v, want nil", sid)
}
return
}
if err != nil {
t.Errorf("parseSIDBinary() error = %v, wantErr %v", err, tt.wantErr)
}
if sid == nil {
t.Errorf("parseSIDBinary() sid = nil, want non-nil, wantErr %v", tt.wantErr)
return
}
if sidStr := sid.String(); sidStr != tt.want {
t.Errorf("parseSIDBinary() = %v, want %v, (sid = %#v)", sidStr, tt.want, sid)
}
})
}
}
func TestParseACEBinary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
want string
wantErr bool
}{
{
name: "Invalid data - too short",
data: []byte{0x00, 0x00, 0x14, 0x00}, // Only header size, no mask or SID
want: "",
wantErr: true,
},
{
name: "Invalid data - mismatched size",
data: []byte{
0x00, // Type
0x00, // Flags
0xFF, 0x00, // Size (larger than actual data)
0x00, 0x00, 0x00, 0x00, // Mask
// Minimal SID
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
},
want: "",
wantErr: true,
},
{
name: "Basic Allow ACE",
data: []byte{
// ACE Header
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags (none)
0x14, 0x00, // Size (20 bytes)
// Access mask
0xFF, 0x01, 0x1F, 0x00, // 0x1F01FF - Full Access
// SID (SYSTEM)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
},
want: "(A;;FA;;;SY)",
wantErr: false,
},
{
name: "Basic Deny ACE",
data: []byte{
0x01, // Type (ACCESS_DENIED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size
0x89, 0x00, 0x12, 0x00, // 0x120089 - File Read
// SID (Everyone)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00,
},
want: "(D;;FR;;;WD)",
wantErr: false,
},
{
name: "Audit ACE",
data: []byte{
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x00, // Flags
0x18, 0x00, // Size
0x16, 0x01, 0x12, 0x00, // 0x00120116 - File Write
// SID (BUILTIN\Administrators)
0x01, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x20, 0x00, 0x00, 0x00,
0x20, 0x02, 0x00, 0x00,
},
want: "(AU;;FW;;;BA)",
wantErr: false,
},
{
name: "ACE with inheritance flags",
data: []byte{
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x0B, // Flags (CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE | INHERIT_ONLY_ACE)
0x14, 0x00, // Size
0xA9, 0x00, 0x12, 0x00, // 0x1200A9 - Read and Execute Access
// SID (Authenticated Users)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x0B, 0x00, 0x00, 0x00,
},
want: "(A;OICIIO;CCSWWPLORCSY;;;AU)",
wantErr: false,
},
{
name: "ACE with custom access mask",
data: []byte{
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size
0x34, 0x12, 0x56, 0x78, // Custom access mask
// SID (SYSTEM)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
},
want: "(A;;0x78561234;;;SY)",
wantErr: false,
},
{
name: "ACE with inherited flag",
data: []byte{
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x10, // Flags (INHERITED_ACE)
0x18, 0x00, // Size
0x89, 0x00, 0x12, 0x00, // File Read
// SID (BUILTIN\Users)
0x01, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x20, 0x00, 0x00, 0x00,
0x21, 0x02, 0x00, 0x00,
},
want: "(A;ID;FR;;;BU)",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ace, err := parseACEBinary(tt.data)
if tt.wantErr {
if err == nil {
t.Errorf("parseACEBinary() expected error, got nil")
}
if ace != nil {
t.Errorf("parseACEBinary() expected nil, got %v", ace)
}
return
}
if err != nil {
t.Errorf("parseACEBinary() error = %v, expected nil", err)
return
}
if ace == nil {
t.Errorf("parseACEBinary() expected non-nil, got nil")
return
}
if aceStr := ace.String(); aceStr != tt.want {
t.Errorf("parseACEBinary() = %v, want %v", aceStr, tt.want)
}
})
}
}
func TestParseACLBinary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
aclType string
control uint16
want *acl
wantStr string
wantErr bool
}{
{
name: "Invalid data - too short",
data: []byte{0x02, 0x00}, // Not enough bytes for ACL header
aclType: "D",
control: 0,
wantStr: "",
wantErr: true,
},
{
name: "Invalid data - size mismatch",
data: []byte{
0x02, // Revision
0x00, // Sbz1
0xFF, 0x00, // Size (too large)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
},
aclType: "D",
control: 0,
wantStr: "",
wantErr: true,
},
{
name: "Empty ACL",
data: []byte{
0x02, // Revision
0x00, // Sbz1
0x08, 0x00, // Size (8 bytes - just header)
0x00, 0x00, // AceCount
0x00, 0x00, // Sbz2
},
aclType: "D",
control: 0,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x8,
sbz2: 0,
aclType: "D",
control: 0,
aces: nil,
},
wantStr: "",
wantErr: false,
},
{
name: "Protected empty ACL",
data: []byte{
0x02, // Revision
0x00, // Sbz1
0x08, 0x00, // Size
0x00, 0x00, // AceCount
0x00, 0x00, // Sbz2
},
aclType: "D",
control: seDACLProtected,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x8,
sbz2: 0,
aclType: "D",
control: seDACLProtected,
},
wantStr: "P",
wantErr: false,
},
{
name: "Auto-inherited empty ACL",
data: []byte{
0x02, // Revision
0x00, // Sbz1
0x08, 0x00, // Size
0x00, 0x00, // AceCount
0x00, 0x00, // Sbz2
},
aclType: "D",
control: seDACLAutoInherited,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x8,
sbz2: 0,
aclType: "D",
control: seDACLAutoInherited,
},
wantStr: "AI",
wantErr: false,
},
{
name: "Protected and auto-inherited empty ACL",
data: []byte{
0x02, // Revision
0x00, // Sbz1
0x08, 0x00, // Size
0x00, 0x00, // AceCount
0x00, 0x00, // Sbz2
},
aclType: "D",
control: seDACLProtected | seDACLAutoInherited,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x8,
sbz2: 0,
aclType: "D",
control: seDACLProtected | seDACLAutoInherited,
},
wantStr: "PAI",
wantErr: false,
},
{
name: "ACL with one ACE",
data: []byte{
// ACL Header
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes = 8 header + 20 ACE)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
// SID (SYSTEM)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (NT Authority)
0x12, 0x00, 0x00, 0x00, // SubAuthority (SYSTEM)
},
aclType: "D",
control: 0,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x1C, // 28 bytes = 8 header + 20 ACE
aceCount: 1,
sbz2: 0,
aclType: "D",
control: 0,
aces: []ace{
{
header: &aceHeader{
aceType: 0,
aceFlags: 0,
aceSize: 0x14, // 20 Bytes
},
accessMask: 0x001F01FF, // Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5, // NT Authority
subAuthority: []uint32{0x12}, // SYSTEM
},
},
},
},
wantStr: "(A;;FA;;;SY)",
wantErr: false,
},
{
name: "ACL with multiple ACEs",
data: []byte{
// ACL Header
0x02, // Revision
0x00, // Sbz1
0x38, 0x00, // Size (56 bytes = 8 header + 20 first ACE + 28 second ACE)
0x02, 0x00, // AceCount
0x00, 0x00, // Sbz2
// First ACE - Allow System Full Access
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01, // SID - Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authoruty
0x12, 0x00, 0x00, 0x00, // SubAuthority - SYSTEM
// Second ACE - Allow Administrators Read
0x00, // Type
0x00, // Flags
0x18, 0x00, // Size (24 bytes - larger to accommodate full Administrators SID)
0x89, 0x00, 0x12, 0x00, // Access mask (File Read)
0x01, 0x02, // SID: Rev=1, Count=2
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (NT Authority)
0x20, 0x00, 0x00, 0x00, // SubAuth1 = 32 (BUILTIN)
0x20, 0x02, 0x00, 0x00, // SubAuth2 = 544 (Administrators)
},
aclType: "D",
control: 0,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x38, // 56 bytes = 8 header + 20 first ACE + 28 second ACE
aceCount: 2,
sbz2: 0,
aclType: "D",
control: 0,
aces: []ace{
{
header: &aceHeader{
aceType: 0,
aceFlags: 0,
aceSize: 0x14, // 20 Bytes
},
accessMask: 0x001F01FF, // Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{0x12},
},
},
{
header: &aceHeader{
aceType: 0,
aceFlags: 0,
aceSize: 0x18, // 24 Bytes
},
accessMask: 0x00120089, // File Read
sid: &sid{
revision: 1,
identifierAuthority: 5, // NT Authority
subAuthority: []uint32{0x20, 0x0220}, // BUILTIN, Administrators
},
},
},
},
wantStr: "(A;;FA;;;SY)(A;;FR;;;BA)",
wantErr: false,
},
{
name: "SACL with audit ACEs",
data: []byte{
// ACL Header
0x02, // Revision
0x00, // Sbz1
0x28, 0x00, // Size (40 bytes = 8 header + 2 ACEs of 16 bytes each)
0x02, 0x00, // AceCount
0x00, 0x00, // Sbz2
// First ACE - Audit System Success
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x40, // Flags (SUCCESSFUL_ACCESS_ACE)
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00, // SYSTEM
// Second ACE - Audit System Failure
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x80, // Flags (FAILED_ACCESS_ACE)
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00, // SYSTEM
},
aclType: "S",
control: seSACLPresent,
want: &acl{
aclRevision: 0x02,
sbzl: 0,
aclSize: 0x28, // 40 bytes = 8 header + 2 ACEs of 16 bytes each
aceCount: 2,
sbz2: 0,
aclType: "S",
control: seSACLPresent,
aces: []ace{
{
header: &aceHeader{
aceType: 2, // SYSTEM_AUDIT_ACE_TYPE
aceFlags: 0x40, // SUCCESSFUL_ACCESS_ACE
aceSize: 0x14, // 20 Bytes
},
accessMask: 0x001F01FF, // Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5, // NT Authority
subAuthority: []uint32{0x12}, // SYSTEM
},
},
{
header: &aceHeader{
aceType: 2, // SYSTEM_AUDIT_ACE_TYPE
aceFlags: 0x80, // FAILED_ACCESS_ACE
aceSize: 0x14, // 20 Bytes
},
accessMask: 0x001F01FF, // Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5, // NT Authority
subAuthority: []uint32{0x12}, // SYSTEM
},
},
},
},
wantStr: "(AU;SA;FA;;;SY)(AU;FA;FA;;;SY)",
wantErr: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
acl, err := parseACLBinary(tt.data, tt.aclType, tt.control)
if tt.wantErr {
if err == nil {
t.Errorf("parseACLBinary() = %v, wantErr %v", acl, tt.wantErr)
}
if acl != nil {
t.Errorf("parseACLBinary() = %v, wantErr %v", acl, tt.wantErr)
}
return
}
if err != nil {
t.Errorf("parseACLBinary() error = %v, wantErr %v", err, tt.wantErr)
return
}
if acl == nil {
t.Errorf("parseACLBinary() = %v, wantErr %v", acl, tt.wantErr)
return
}
compareACLs(t, "acl", acl, tt.want)
if aclStr := acl.String(); aclStr != tt.wantStr {
t.Errorf("parseACLBinary() = %v, want %v", aclStr, tt.wantStr)
}
})
}
}
func TestFromBinary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
want string
wantErr bool
}{
{
name: "Invalid data - too short",
data: []byte{0x01, 0x00, 0x04},
want: "",
wantErr: true,
},
{
name: "Invalid data - nil",
data: nil,
want: "",
wantErr: true,
},
{
name: "Empty self-relative security descriptor",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x00, 0x80, // Control (SE_SELF_RELATIVE)
0x00, 0x00, 0x00, 0x00, // Owner
0x00, 0x00, 0x00, 0x00, // Group
0x00, 0x00, 0x00, 0x00, // Sacl
0x00, 0x00, 0x00, 0x00, // Dacl
},
want: "",
wantErr: false,
},
{
name: "Security descriptor with owner only",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x00, 0x80, // Control (SE_SELF_RELATIVE)
0x14, 0x00, 0x00, 0x00, // Owner offset
0x00, 0x00, 0x00, 0x00, // Group
0x00, 0x00, 0x00, 0x00, // Sacl
0x00, 0x00, 0x00, 0x00, // Dacl
// Owner SID (SYSTEM)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority
0x12, 0x00, 0x00, 0x00, // SubAuthority
},
want: "O:SY",
wantErr: false,
},
{
name: "Security descriptor with owner and group",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x00, 0x80, // Control (SE_SELF_RELATIVE)
0x14, 0x00, 0x00, 0x00, // Owner offset (20 bytes from start)
0x20, 0x00, 0x00, 0x00, // Group offset (32 bytes from start)
0x00, 0x00, 0x00, 0x00, // Sacl
0x00, 0x00, 0x00, 0x00, // Dacl
// Owner SID (SYSTEM)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority
0x12, 0x00, 0x00, 0x00, // SubAuthority
// Group SID (Everyone - S-1-1-0)
0x01, // Revision (1)
0x01, // SubAuthorityCount (1)
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // IdentifierAuthority (SECURITY_WORLD_SID_AUTHORITY = 1)
0x00, 0x00, 0x00, 0x00, // SubAuthority[0] = 0 (final component of S-1-1-0)
// Owner SID (SYSTEM)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
// Group SID (Everyone)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00,
},
want: "O:SYG:WD",
wantErr: false,
},
{
name: "Security descriptor with DACL",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x04, 0x80, // Control (SE_SELF_RELATIVE | SE_DACL_PRESENT)
0x00, 0x00, 0x00, 0x00, // Owner
0x00, 0x00, 0x00, 0x00, // Group
0x00, 0x00, 0x00, 0x00, // Sacl
0x14, 0x00, 0x00, 0x00, // Dacl offset
// DACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
// SID (SYSTEM)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
},
want: "D:(A;;FA;;;SY)",
wantErr: false,
},
{
name: "Security descriptor with SACL",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x10, 0x80, // Control (SE_SELF_RELATIVE | SE_SACL_PRESENT)
0x00, 0x00, 0x00, 0x00, // Owner
0x00, 0x00, 0x00, 0x00, // Group
0x14, 0x00, 0x00, 0x00, // Sacl offset
0x00, 0x00, 0x00, 0x00, // Dacl
// SACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// ACE
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x40, // Flags (SUCCESSFUL_ACCESS_ACE)
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
// SID (SYSTEM)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
},
want: "S:(AU;SA;FA;;;SY)",
wantErr: false,
},
{
name: "Complete security descriptor with all components",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x14, 0x80, // Control (SE_SELF_RELATIVE | SE_DACL_PRESENT | SE_SACL_PRESENT)
0x4C, 0x00, 0x00, 0x00, // Owner offset
0x58, 0x00, 0x00, 0x00, // Group offset
0x14, 0x00, 0x00, 0x00, // Sacl offset
0x30, 0x00, 0x00, 0x00, // Dacl offset
// SACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// SACL ACE
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x40, // Flags (SUCCESSFUL_ACCESS_ACE)
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (NT)
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
// DACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// DACL ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (NT)
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
// Owner SID (SYSTEM)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (NT)
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
// Group SID (Everyone)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // Authority (WORLD)
0x00, 0x00, 0x00, 0x00, // SubAuthority (0)
},
want: "O:SYG:WDD:(A;;FA;;;SY)S:(AU;SA;FA;;;SY)",
wantErr: false,
},
{
name: "Invalid owner offset",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x00, 0x80, // Control
0xFF, 0xFF, 0xFF, 0xFF, // Owner (invalid offset)
0x00, 0x00, 0x00, 0x00, // Group
0x00, 0x00, 0x00, 0x00, // Sacl
0x00, 0x00, 0x00, 0x00, // Dacl
},
want: "",
wantErr: true,
},
{
name: "Invalid group offset",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x00, 0x80, // Control
0x00, 0x00, 0x00, 0x00, // Owner
0xFF, 0xFF, 0xFF, 0xFF, // Group (invalid offset)
0x00, 0x00, 0x00, 0x00, // Sacl
0x00, 0x00, 0x00, 0x00, // Dacl
},
want: "",
wantErr: true,
},
{
name: "Invalid SACL offset",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x10, 0x80, // Control (SE_SELF_RELATIVE | SE_SACL_PRESENT)
0x00, 0x00, 0x00, 0x00, // Owner
0x00, 0x00, 0x00, 0x00, // Group
0xFF, 0xFF, 0xFF, 0xFF, // Sacl (invalid offset)
0x00, 0x00, 0x00, 0x00, // Dacl
},
want: "",
wantErr: true,
},
{
name: "Invalid DACL offset",
data: []byte{
0x01, // Revision
0x00, // Sbz1
0x04, 0x80, // Control (SE_SELF_RELATIVE | SE_DACL_PRESENT)
0x00, 0x00, 0x00, 0x00, // Owner
0x00, 0x00, 0x00, 0x00, // Group
0x00, 0x00, 0x00, 0x00, // Sacl
0xFF, 0xFF, 0xFF, 0xFF, // Dacl (invalid offset)
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sd, err := FromBinary(tt.data)
if tt.wantErr {
if err == nil {
t.Errorf("ParseSecurityDescriptorToStruct() error = %v, wantErr %v", err, tt.wantErr)
}
if sd != nil {
t.Errorf("ParseSecurityDescriptorToStruct() = %v, want nil", sd)
}
return
}
if err != nil {
t.Errorf("ParseSecurityDescriptorToStruct() error = %v, wantErr %v", err, tt.wantErr)
return
}
if sd == nil {
t.Errorf("ParseSecurityDescriptorToStruct() = nil, want not nil")
return
}
sdStr := sd.String()
if sdStr != tt.want {
t.Errorf("ParseSecurityDescriptor() = %v, want %v", sdStr, tt.want)
}
})
}
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/from_string.go 0000664 0000000 0000000 00000066103 15150254773 0024342 0 ustar 00root root 0000000 0000000 package sddl
import (
"fmt"
"strconv"
"strings"
)
// wellKnownRIDs maps short names to Relative Identifiers (RIDs) for well-known security principals
// as defined in [MS-DTYP] section 2.4.2.4 Well-known SID Structures.
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab
var wellKnownRIDs = map[string]rid{
"LA": 500, // DOMAIN_USER_RID_ADMIN (Local Administrator)
"LG": 501, // DOMAIN_USER_RID_GUEST (Local Guest)
}
// sidHolder represents any structure capable of containing zero or more Security Identifiers (SIDs).
//
// This interface is necessary for two main reasons:
// 1. Parsing SDDL components may result in incomplete SID parsing results.
// 2. At some point, we need to extract all complete SIDs from existing structures
// to build the incomplete SIDs (using the domain information from complete SIDs).
//
// Implementations of this interface should provide a method to access all contained SIDs.
type sidHolder interface {
// sids returns a slice of all SIDs contained within the implementing structure.
sids() []sid
}
// making existing structures implement sidHolder
var _ sidHolder = &sid{}
func (s *sid) sids() []sid { // implements sidHolder
return []sid{*s}
}
var _ sidHolder = &ace{}
func (a *ace) sids() []sid { // implements sidHolder
return []sid{*a.sid}
}
var _ sidHolder = &acl{}
func (a *acl) sids() []sid { // implements sidHolder
var sids []sid
for _, ace := range a.aces {
sids = append(sids, ace.sids()...)
}
return sids
}
// parseSIDStringResult represents the outcome of a SID parsing operation.
//
// This interface can represent either:
// - A complete SID structure
// - An incomplete SID for domain-specific Relative Identifiers (RIDs)
// where domain information is missing (e.g., S-1-5-21--)
//
// Implementations must provide a method to convert the result into a full SID,
// potentially using contextual information from previously parsed SIDs.
type parseSIDStringResult interface {
sidHolder // parseSIDStringResult implements sidHolder, incomplete results return empty slice
// toSID converts the result into a full SID.
//
// It uses contextual information from previously parsed SIDs if necessary.
// For incomplete SIDs (e.g., RIDs without domain information), it attempts to
// extract domain information from previousSIDs. If previousSIDs is empty and
// the SID is incomplete, this method will return an error.
//
// Parameters:
// - previousSIDs: A slice of previously parsed SIDs to provide context
//
// Returns:
// - *sid: A pointer to the complete SID structure
// - error: An error if the conversion fails
toSID(previousSIDs []sid) (*sid, error)
}
func (s *sid) toSID(previousSIDs []sid) (*sid, error) {
// sid structure is a valid parseSIDStringResult and represents a complete SID
return s, nil
}
// rid represents a Relative Identifier (RID), which is the last sub-authority of a Security Identifier (SID).
// It is incomplete on its own and requires domain information from a complete SID to form a full SID.
// RIDs are typically used in domain environments to uniquely identify users, groups, or other security principals.
type rid uint32
func (r rid) toSID(previousSIDs []sid) (*sid, error) {
if len(previousSIDs) == 0 {
return nil, ErrMissingDomainInformation
}
s, err := r.complete(previousSIDs[0])
if err != nil {
return nil, err
}
return s, nil
}
func (r rid) sids() []sid {
return []sid{}
}
// complete converts a Relative Identifier (RID) into a complete SID by combining it with the information from an existing SID.
// It uses the domain information from the provided SID and appends the RID as the last sub-authority.
//
// Parameters:
// - s: An existing SID to provide the domain information
//
// Returns:
// - *sid: A pointer to a new, complete SID that includes the RID
// - error: If the sid does not contain sub authorities (first sub-authority is required)
func (r rid) complete(s sid) (*sid, error) {
if len(s.subAuthority) == 0 {
return nil, ErrMissingSubAuthorities
}
firstSubAuthority := s.subAuthority[0]
domain := s.Domain()
var subAuthorities []uint32
subAuthorities = append(subAuthorities, firstSubAuthority)
subAuthorities = append(subAuthorities, domain...)
subAuthorities = append(subAuthorities, uint32(r))
return &sid{
revision: s.revision,
identifierAuthority: s.identifierAuthority,
subAuthority: subAuthorities,
}, nil
}
// parseACEStringResult represents the outcome of an ACE parsing operation.
// It mimics the ACE structure (ace) but instead of a sid, it contains a parseSIDStringResult.
type parseACEStringResult struct {
// header contains the ACE header information
header *aceHeader
// accessMask specifies the access rights controlled by the ACE
accessMask uint32
// sid represents the Security Identifier (SID) associated with this ACE
sid parseSIDStringResult
}
func (a *parseACEStringResult) sids() []sid {
return a.sid.sids()
}
// toACE converts a parseACEStringResult to a complete ACE structure.
// It resolves any incomplete SID information using the provided previousSIDs.
//
// Parameters:
// - previousSIDs: A slice of previously parsed SIDs to provide context for incomplete SIDs
//
// Returns:
// - *ace: A pointer to the complete ACE structure
// - error: An error if the conversion fails, particularly if SID resolution fails
func (a *parseACEStringResult) toACE(previousSIDs []sid) (*ace, error) {
sid, err := a.sid.toSID(previousSIDs)
if err != nil {
return nil, err
}
// Calculate the total size of the ACE
// Size = sizeof(ACE_HEADER) + sizeof(ACCESS_MASK) + size of the SID
// SID size = 8 + (4 * number of sub-authorities)
sidSize := 8 + (4 * len(sid.subAuthority))
aceSize := 4 + 4 + sidSize // 4 (header) + 4 (access mask) + sidSize
a.header.aceSize = uint16(aceSize)
return &ace{
header: a.header,
accessMask: a.accessMask,
sid: sid,
}, nil
}
// parseACLStringResult represents the outcome of an ACL parsing operation.
// It mimics the ACL structure (acl) but instead of a slice of aces, it contains a slice of parseACEStringResult.
type parseACLStringResult struct {
// aclRevision is the revision level of the ACL structure
aclRevision byte
// sbzl is a reserved field (should be zero)
sbzl byte
// aclSize is the size, in bytes, of the ACL structure
aclSize uint16
// aceCount is the number of ACEs in the ACL
aceCount uint16
// sbz2 is a reserved field (should be zero)
sbz2 uint16
// aclType indicates whether this is a DACL or SACL
aclType string
// control contains ACL control flags
control uint16
// aces is a slice of parsed ACE results
aces []parseACEStringResult
}
func (a *parseACLStringResult) sids() []sid {
var sids []sid
for _, ace := range a.aces {
sids = append(sids, ace.sids()...)
}
return sids
}
// toACL converts a parseACLStringResult to a complete ACL structure.
// It resolves any incomplete SID information in the ACEs using the provided previousSIDs.
//
// Parameters:
// - previousSIDs: A slice of previously parsed SIDs to provide context for incomplete SIDs in ACEs
//
// Returns:
// - *acl: A pointer to the complete ACL structure
// - error: An error if the conversion fails, particularly if SID resolution fails in any ACE
func (a *parseACLStringResult) toACL(previousSIDs []sid) (*acl, error) {
var aces []ace
for _, ace := range a.aces {
ace, err := ace.toACE(previousSIDs)
if err != nil {
return nil, err
}
aces = append(aces, *ace)
}
// Calculate total ACL size
totalSize := 8 // ACL header size
for _, ace := range aces {
totalSize += int(ace.header.aceSize)
}
a.aclSize = uint16(totalSize)
return &acl{
aclRevision: a.aclRevision,
sbzl: a.sbzl,
aclSize: a.aclSize,
aceCount: a.aceCount,
sbz2: a.sbz2,
aclType: a.aclType,
control: a.control,
aces: aces,
}, nil
}
// FromString parses a security descriptor string in SDDL format.
// The format is: "O:owner_sidG:group_sidD:dacl_flagsS:sacl_flags"
// where each component is optional.
//
// Examples:
// - "O:SYG:BAD:(A;;FA;;;SY)" - Owner: SYSTEM, Group: BUILTIN\Administrators, DACL with full access for SYSTEM
// - "O:SYG:SYD:PAI(A;;FA;;;SY)" - Protected auto-inherited DACL
// - "O:SYG:SYD:(A;;FA;;;SY)S:(AU;SA;FA;;;SY)" - With both DACL and SACL
func FromString(s string) (*SecurityDescriptor, error) {
// Initialize security descriptor with self-relative flag
sd := &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seDACLDefaulted | seSACLDefaulted, // All components are defaulted unless they are present
}
// Empty string is valid - returns a security descriptor with defaults set
if s == "" {
return sd, nil
}
remaining := s
var err error
// parsing results
var (
completeSIDs []sid
ownerSID parseSIDStringResult
groupSID parseSIDStringResult
dacl *parseACLStringResult
sacl *parseACLStringResult
)
// Parse each component in order if present
// The order doesn't technically matter, so, we are going to keep a list of pending components to parse
// and remove them as we go
pendingComponents := []string{"O:", "G:", "D:", "S:"}
removePendingComponent := func(component string) {
for i, c := range pendingComponents {
if c == component {
pendingComponents = append(pendingComponents[:i], pendingComponents[i+1:]...)
break
}
}
}
// If there is data, then, at least one component must be present
if findNextComponent(remaining, pendingComponents...) == -1 {
return nil, fmt.Errorf("no components found in security descriptor")
}
// Parse each component regardless of their order, as long as there are remaining characters and pending components
for len(pendingComponents) > 0 && len(remaining) > 0 {
switch {
case strings.HasPrefix(remaining, "O:"):
// remove O: prefix
remaining = remaining[2:]
removePendingComponent("O:")
ownerSID, remaining, err = parseSIDComponent(remaining, pendingComponents...)
if err != nil {
return nil, fmt.Errorf("error parsing owner SID: %w", err)
}
sd.control ^= seOwnerDefaulted
case strings.HasPrefix(remaining, "G:"):
// remove G: prefix
remaining = remaining[2:]
removePendingComponent("G:")
groupSID, remaining, err = parseSIDComponent(remaining, pendingComponents...)
if err != nil {
return nil, fmt.Errorf("error parsing group SID: %w", err)
}
sd.control ^= seGroupDefaulted
case strings.HasPrefix(remaining, "D:"):
// remove D: prefix
remaining = remaining[2:]
removePendingComponent("D:")
dacl, remaining, err = parseACLComponent("D", remaining, pendingComponents...)
if err != nil {
return nil, fmt.Errorf("error parsing DACL: %w", err)
}
sd.control ^= seDACLDefaulted
sd.control |= seDACLPresent
case strings.HasPrefix(remaining, "S:"):
// remove S: prefix
remaining = remaining[2:]
removePendingComponent("S:")
sacl, remaining, err = parseACLComponent("S", remaining, pendingComponents...)
if err != nil {
return nil, fmt.Errorf("error parsing SACL: %w", err)
}
sd.control ^= seSACLDefaulted
sd.control |= seSACLPresent
}
}
// If there's anything left unparsed, it's an error
if remaining != "" {
return nil, fmt.Errorf("unexpected content after parsing: %s", remaining)
}
// convert parsed result components into final structures
if ownerSID != nil {
completeSIDs = append(completeSIDs, ownerSID.sids()...)
}
if groupSID != nil {
completeSIDs = append(completeSIDs, groupSID.sids()...)
}
if dacl != nil {
completeSIDs = append(completeSIDs, dacl.sids()...)
}
if sacl != nil {
completeSIDs = append(completeSIDs, sacl.sids()...)
}
// Remove generic (well-known) SIDs from completeSIDs because they do not give the appropriate domain
for i := len(completeSIDs) - 1; i >= 0; i-- {
if completeSIDs[i].isGeneric() {
completeSIDs = append(completeSIDs[:i], completeSIDs[i+1:]...)
}
}
// Resolve incomplete SIDs in the DACL and SACL
if dacl != nil {
sd.dacl, err = dacl.toACL(completeSIDs)
if err != nil {
return nil, err
}
}
if sacl != nil {
sd.sacl, err = sacl.toACL(completeSIDs)
if err != nil {
return nil, err
}
}
if ownerSID != nil {
sd.ownerSID, err = ownerSID.toSID(completeSIDs)
if err != nil {
return nil, err
}
}
if groupSID != nil {
sd.groupSID, err = groupSID.toSID(completeSIDs)
if err != nil {
return nil, err
}
}
// update control flags based on ACLs
if sd.dacl != nil {
// Update control flags based on DACL flags
if sd.dacl.control&seDACLProtected != 0 {
sd.control |= seDACLProtected
}
if sd.dacl.control&seDACLAutoInherited != 0 {
sd.control |= seDACLAutoInherited
}
if sd.dacl.control&seDACLAutoInheritRe != 0 {
sd.control |= seDACLAutoInheritRe
}
}
if sd.sacl != nil {
// Update control flags based on SACL flags
if sd.sacl.control&seSACLProtected != 0 {
sd.control |= seSACLProtected
}
if sd.sacl.control&seSACLAutoInherited != 0 {
sd.control |= seSACLAutoInherited
}
if sd.sacl.control&seSACLAutoInheritRe != 0 {
sd.control |= seSACLAutoInheritRe
}
}
// Adjust ACL's control flags once they are fully computed
if sd.dacl != nil {
sd.dacl.control = sd.control
}
if sd.sacl != nil {
sd.sacl.control = sd.control
}
return sd, nil
}
func parseSIDComponent(s string, nextMarkers ...string) (sid parseSIDStringResult, remaining string, err error) {
// Find the next component marker (G:, D:, or S:)
sidEnd := findNextComponent(s, nextMarkers...)
if sidEnd == -1 {
sidEnd = len(s)
}
// Parse the SID string
sid, err = parseSIDString(s[:sidEnd])
if err != nil {
return nil, "", fmt.Errorf("invalid SID: %w", err)
}
return sid, s[sidEnd:], nil
}
func parseACLComponent(aclType, s string, nextMarkers ...string) (aclr *parseACLStringResult, remaining string, err error) {
// Find the next marker (if any)
aclEnd := len(s)
if len(nextMarkers) > 0 {
nextMarkerIndex := findNextComponent(s, nextMarkers...)
if nextMarkerIndex != -1 {
aclEnd = nextMarkerIndex
}
}
// Parse the ACL string
aclr, err = parseACLString(aclType, s[:aclEnd])
if err != nil {
return nil, "", fmt.Errorf("invalid ACL: %w", err)
}
return aclr, s[aclEnd:], nil
}
// findNextComponent looks for the next component marker given in arguments
// Returns the index of the next component or -1 if none found
func findNextComponent(s string, markers ...string) int {
minIndex := -1
for _, marker := range markers {
if idx := strings.Index(s, marker); idx != -1 {
if minIndex == -1 || idx < minIndex {
minIndex = idx
}
}
}
return minIndex
}
// parseAccessMask converts an access mask string to its corresponding uint32 value
func parseAccessMask(maskStr string) (uint32, error) {
// Check well-known access masks first
if value, ok := reverseWellKnownAccessMasks[maskStr]; ok {
return value, nil
}
// If not a well-known mask, try to parse as hexadecimal
if strings.HasPrefix(maskStr, "0x") {
value, err := strconv.ParseUint(maskStr[2:], 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid hexadecimal access mask: %s", maskStr)
}
return uint32(value), nil
}
// If not a hexadecimal, try to use two-letter codes
var components []string
var idx int
for idx < len(maskStr) {
components = append(components, maskStr[idx:idx+2])
idx += 2
}
mask, remaining := composeAccessMask(components)
if len(remaining) == 0 {
return mask, nil
}
return 0, fmt.Errorf("unknown access mask: %s", maskStr)
}
// parseACEString parses an ACE string in the format "(type;flags;rights;;;sid)" into an ACE structure
// Example: "(A;;FA;;;SY)" which represents:
// - Type: A (ACCESS_ALLOWED_ACE_TYPE)
// - Flags: (none)
// - Rights: FA (Full Access)
// - SID: SY (Local System)
func parseACEString(aceStr string) (*parseACEStringResult, error) {
// Validate basic string format
if len(aceStr) < 2 || !strings.HasPrefix(aceStr, "(") || !strings.HasSuffix(aceStr, ")") {
return nil, fmt.Errorf("invalid ACE string format: must be enclosed in parentheses")
}
// Remove parentheses and split into components
parts := strings.Split(aceStr[1:len(aceStr)-1], ";")
if len(parts) != 6 {
return nil, fmt.Errorf("invalid ACE string format: expected 6 components separated by semicolons")
}
// Parse ACE type
aceType, err := parseACEType(parts[0])
if err != nil {
return nil, fmt.Errorf("invalid ACE type: %w", err)
}
// Parse ACE flags with type validation
aceFlags, err := parseFlagsForACEType(parts[1], aceType)
if err != nil {
return nil, fmt.Errorf("invalid ACE flags: %w", err)
}
// Parse access mask
accessMask, err := parseAccessMask(parts[2])
if err != nil {
return nil, fmt.Errorf("invalid access mask: %w", err)
}
// Parse SID (parts[3] and parts[4] are object type and inherited object type, which we ignore)
sid, err := parseSIDString(parts[5])
if err != nil {
return nil, fmt.Errorf("invalid SID: %w", err)
}
ace := &parseACEStringResult{
header: &aceHeader{
aceType: aceType,
aceFlags: aceFlags,
},
accessMask: accessMask,
sid: sid,
}
return ace, nil
}
// parseACEType converts an ACE type string to its corresponding byte value
// The valid types are:
// - A (ACCESS_ALLOWED_ACE_TYPE): allows access to the object
// - D (ACCESS_DENIED_ACE_TYPE): denies access to the object
// - AU (SYSTEM_AUDIT_ACE_TYPE): specifies a system audit ACE
// - AL (SYSTEM_ALARM_ACE_TYPE): specifies a system alarm ACE
// - OA (ACCESS_ALLOWED_OBJECT_ACE_TYPE): specifies an object-specific access ACE
func parseACEType(typeStr string) (byte, error) {
// First check well-known string representations
switch typeStr {
case "A":
return accessAllowedACEType, nil
case "D":
return accessDeniedACEType, nil
case "AU":
return systemAuditACEType, nil
case "AL":
return systemAlarmACEType, nil
case "OA":
return accessAllowedObjectACEType, nil
}
// If not a well-known type, try to parse as hexadecimal
// The format should be "0xNN" where NN is a hex number
if strings.HasPrefix(typeStr, "0x") {
value, err := strconv.ParseUint(typeStr[2:], 16, 8)
if err != nil {
return 0, fmt.Errorf("invalid hexadecimal ACE type: %s", typeStr)
}
return byte(value), nil
}
return 0, fmt.Errorf("invalid ACE type: %s (must be a known type or hexadecimal value)", typeStr)
}
// parseACLFlags splits a flag string into individualn ACL flags
// Example: "PAI" becomes []string{"P", "AI"}
//
// The ACL Control Flags in SDDL String Format are:
//
// Single-letter flags:
//
// P - Protected
// Prevents the ACL from being modified by inheritable ACEs.
// The ACL is protected from inheritance flowing down from parent containers.
// R - Read-Only
// Marks the ACL as read-only, preventing any modifications.
// This is often used for system-managed ACLs.
//
// Two-letter flags:
//
// AI - Auto-Inherited
// Indicates the ACL was created through inheritance.
// Appears when the ACL contains entries inherited from a parent object.
// AR - Auto-Inherit Required
// Forces child objects to inherit this ACL.
// When set, ensures all child objects must process inherited permissions.
// NO - No Inheritance
// Explicitly excludes inheritable ACEs from being considered.
// Blocks inheritance without changing the inherited ACEs themselves.
// IO - Inherit Only
// Specifies the ACL should only be used for inheritance purposes.
// The ACL is not used for access checks on the current object.
//
// These flags can be combined in any order after the ACL type identifier:
// - For DACLs: "D:[flags]", e.g., "D:PAI", "D:AINO"
// - For SACLs: "S:[flags]", e.g., "S:PAR", "S:ARNO"
//
// The ordering of combined flags does not affect their meaning:
// "D:AINO" is equivalent to "D:NOAI"
func parseACLFlags(s string) ([]string, error) {
var flags []string
for i := 0; i < len(s); {
code1 := s[i : i+1]
code2 := ""
if i+1 < len(s) {
code2 = s[i : i+2]
}
// Check for two-character flags first
switch code2 {
case "AI", "AR", "NO", "IO":
flags = append(flags, code2)
i += 2
default:
// Check for single-character flags
switch code1 {
case "P", "R":
flags = append(flags, code1)
i++
default:
return nil, fmt.Errorf("invalid flag: %q", s[i])
}
}
}
return flags, nil
}
// parseACLString parses an ACL string representation into an ACL structure.
// The ACL string format follows the Security Descriptor String Format (SDDL).
// Parameters:
// - aclType: Either "D" for DACL or "S" for SACL
// - s: The ACL string to parse, which may include:
// - Optional flags (e.g., "PAI" for Protected and AutoInherited)
// - One or more ACEs enclosed in parentheses
//
// Examples:
// - "D:(A;;FA;;;SY)" // DACL with a single ACE
// - "S:PAI(AU;SA;FA;;;SY)" // Protected auto-inherited SACL with an audit ACE
// - "D:(A;;FA;;;SY)(D;;FR;;;WD)" // DACL with two ACEs
func parseACLString(aclType, s string) (*parseACLStringResult, error) {
// Determine ACL type from prefix
var baseControl uint16
switch aclType {
case "D":
baseControl = seDACLPresent
case "S":
baseControl = seSACLPresent
default:
return nil, fmt.Errorf("invalid ACL type: must be either 'D' or 'S'")
}
// Parse flags if present (before the first ACE)
var control uint16 = baseControl
var flags []string
aceStart := 0
// Look for flags section (between : and first parenthesis)
if len(s) > 0 && s[0] != '(' {
flagEnd := strings.Index(s, "(")
if flagEnd == -1 {
if strings.Contains(s, ")") {
return nil, fmt.Errorf("invalid ACL format: missing opening parenthesis")
}
flagEnd = len(s)
}
ff, err := parseACLFlags(s[:flagEnd])
if err != nil {
return nil, fmt.Errorf("error parsing flags: %w", err)
}
flags = ff
aceStart = flagEnd
}
// Update control flags based on parsed flags
// Note: other flags such as NO, IO, etc. are ignored because they do not have a corresponding control flag
for _, flag := range flags {
switch flag {
case "P":
if aclType == "D" {
control |= seDACLProtected
} else {
control |= seSACLProtected
}
case "AI":
if aclType == "D" {
control |= seDACLAutoInherited
} else {
control |= seSACLAutoInherited
}
case "AR":
if aclType == "D" {
control |= seDACLAutoInheritRe
} else {
control |= seSACLAutoInheritRe
}
case "R":
if aclType == "D" {
control |= seDACLDefaulted
} else {
control |= seSACLDefaulted
}
}
}
// Parse ACEs
var aces []parseACEStringResult
remaining := s[aceStart:]
// Handle empty ACL (no ACEs)
if len(remaining) == 0 {
return &parseACLStringResult{
aclRevision: 2,
aclSize: 8, // Size of empty ACL (just header)
aclType: aclType,
control: control,
}, nil
}
// Extract each ACE string (enclosed in parentheses)
for len(remaining) > 0 {
if remaining[0] != '(' {
return nil, fmt.Errorf("invalid ACE format: expected '(' but got %q", remaining[0])
}
// Find closing parenthesis
closePos := strings.Index(remaining, ")")
if closePos == -1 {
return nil, fmt.Errorf("invalid ACE format: missing closing parenthesis")
}
// Parse individual ACE
aceStr := remaining[:closePos+1]
ace, err := parseACEString(aceStr)
if err != nil {
return nil, fmt.Errorf("error parsing ACE %q: %w", aceStr, err)
}
aces = append(aces, *ace)
remaining = remaining[closePos+1:]
}
// Create and return the ACL structure
return &parseACLStringResult{
aclRevision: 2,
sbzl: 0,
aceCount: uint16(len(aces)),
sbz2: 0,
aclType: aclType,
control: control,
aces: aces,
}, nil
}
// parseFlagsForACEType converts an ACE flags string to its corresponding byte value,
// validating that the flags are appropriate for the given ACE type
func parseFlagsForACEType(flagsStr string, aceType byte) (byte, error) {
if flagsStr == "" {
return 0, nil
}
var flags byte
var hasAuditFlags bool
// Process flags in pairs (each flag is 2 characters)
for i := 0; i < len(flagsStr); i += 2 {
if i+2 > len(flagsStr) {
return 0, fmt.Errorf("invalid flag format at position %d", i)
}
flag := flagsStr[i : i+2]
switch flag {
// Inheritance flags - valid for all ACE types
case "CI":
flags |= containerInheritACE
case "OI":
flags |= objectInheritACE
case "NP":
flags |= noPropagateInheritACE
case "IO":
flags |= inheritOnlyACE
case "ID":
flags |= inheritedACE
// Audit flags - only valid for SYSTEM_AUDIT_ACE_TYPE
case "SA", "FA":
hasAuditFlags = true
if aceType != systemAuditACEType {
return 0, fmt.Errorf("audit flags (SA/FA) are only valid for audit ACEs")
}
if flag == "SA" {
flags |= successfulAccessACE
} else {
flags |= failedAccessACE
}
default:
return 0, fmt.Errorf("unknown flag: %s", flag)
}
}
// Validate that audit ACEs have at least one audit flag
if aceType == systemAuditACEType && !hasAuditFlags {
return 0, fmt.Errorf("audit ACEs must specify at least one audit flag (SA/FA)")
}
return flags, nil
}
// parseSIDString parses a string SID representation into a SID structure
func parseSIDString(s string) (parseSIDStringResult, error) {
// First, check if it's a well-known RID abbreviation
// hence this parsing will result in an incomplete SID
if r, ok := wellKnownRIDs[s]; ok {
return r, nil
}
// Second, check if it's a well-known SID abbreviation
if fullSid, ok := reverseWellKnownSids[s]; ok {
s = fullSid
}
// If it doesn't start with "S-", it's invalid
if !strings.HasPrefix(s, "S-") {
return nil, fmt.Errorf("%w: must start with S-", ErrInvalidSIDFormat)
}
// Split the SID string into components
parts := strings.Split(s[2:], "-") // Skip "S-" prefix
if len(parts) < 2 {
return nil, fmt.Errorf("%w: insufficient components", ErrInvalidSIDFormat)
}
// Parse revision
revision, err := strconv.ParseUint(parts[0], 10, 8)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidRevision, err)
}
if revision != 1 {
return nil, fmt.Errorf("%w: got %d, want 1", ErrInvalidRevision, revision)
}
// Parse authority - can be decimal or hex (with 0x prefix)
var authority uint64
authStr := parts[1]
if strings.HasPrefix(strings.ToLower(authStr), "0x") {
// Parse hexadecimal authority
authority, err = strconv.ParseUint(authStr[2:], 16, 48)
if err != nil {
return nil, fmt.Errorf("%w: invalid hex value %v", ErrInvalidAuthority, err)
}
} else {
// Parse decimal authority
authority, err = strconv.ParseUint(authStr, 10, 48)
if err != nil {
return nil, fmt.Errorf("%w: invalid decimal value %v", ErrInvalidAuthority, err)
}
}
// Additional validation for authority value
if authority >= 1<<48 {
return nil, fmt.Errorf("%w: value %d exceeds maximum of 2^48-1", ErrInvalidAuthority, authority)
}
// Parse sub-authorities
subAuthCount := len(parts) - 2 // Subtract revision and authority parts
if subAuthCount > 15 {
return nil, fmt.Errorf("%w: got %d, maximum is 15", ErrTooManySubAuthorities, subAuthCount)
}
subAuthorities := make([]uint32, subAuthCount)
for i := 0; i < subAuthCount; i++ {
sa, err := strconv.ParseUint(parts[i+2], 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid sub-authority at position %d: %v",
ErrInvalidSubAuthority, i, err)
}
subAuthorities[i] = uint32(sa)
}
return &sid{
revision: byte(revision),
identifierAuthority: authority,
subAuthority: subAuthorities,
}, nil
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/from_string_test.go 0000664 0000000 0000000 00000110274 15150254773 0025400 0 ustar 00root root 0000000 0000000 package sddl
import (
"errors"
"fmt"
"reflect"
"sort"
"strings"
"testing"
)
func TestParseACEString(t *testing.T) {
// Helper function to create a SID for testing
createTestSID := func(revision byte, authority uint64, subAuth ...uint32) *sid {
return &sid{
revision: revision,
identifierAuthority: authority,
subAuthority: subAuth,
}
}
tests := []struct {
name string
aceStr string
want *ace
wantErr bool
}{
{
name: "Basic allow ACE",
aceStr: "(A;;FA;;;SY)",
want: &ace{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20, // 4 (header) + 4 (mask) + 12 (SID with 1 sub-authority)
},
accessMask: 0x1F01FF, // FA - Full Access
sid: createTestSID(1, 5, 18), // SY - Local System
},
wantErr: false,
},
{
name: "Deny ACE with inheritance flags",
aceStr: "(D;OICI;FR;;;BA)",
want: &ace{
header: &aceHeader{
aceType: accessDeniedACEType,
aceFlags: objectInheritACE | containerInheritACE,
aceSize: 24, // 4 (header) + 4 (mask) + 16 (SID with 2 sub-authorities)
},
accessMask: 0x120089, // FR - File Read
sid: createTestSID(1, 5, 32, 544), // BA - Builtin Administrators
},
wantErr: false,
},
{
name: "Audit ACE with success audit",
aceStr: "(AU;SA;FA;;;WD)",
want: &ace{
header: &aceHeader{
aceType: systemAuditACEType,
aceFlags: successfulAccessACE,
aceSize: 20, // 4 (header) + 4 (mask) + 12 (SID with 1 sub-authority)
},
accessMask: 0x1F01FF, // FA
sid: createTestSID(1, 1, 0), // WD - Everyone
},
wantErr: false,
},
{
name: "Audit ACE with both success and failure",
aceStr: "(AU;SAFA;FA;;;SY)",
want: &ace{
header: &aceHeader{
aceType: systemAuditACEType,
aceFlags: successfulAccessACE | failedAccessACE,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: createTestSID(1, 5, 18),
},
wantErr: false,
},
{
name: "Complex inheritance flags",
aceStr: "(A;OICIIONP;FA;;;AU)",
want: &ace{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: objectInheritACE | containerInheritACE | inheritOnlyACE | noPropagateInheritACE,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: createTestSID(1, 5, 11), // AU - Authenticated Users
},
wantErr: false,
},
{
name: "Directory operations access mask",
aceStr: "(A;;DCLCRPCR;;;SY)",
want: &ace{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20, // 4 (header) + 4 (access mask) + 12 (SID with 1 sub-authority)
},
accessMask: 0x000116, // Directory Create/List/Read/Pass through/Child rename/Child delete
sid: createTestSID(1, 5, 18),
},
wantErr: false,
},
{
name: "Custom access mask",
aceStr: "(A;;0x1234ABCD;;;SY)",
want: &ace{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20, // 4 (header) + 4 (mask) + 12 (SID with 1 sub-authority)
},
accessMask: 0x1234ABCD,
sid: createTestSID(1, 5, 18),
},
wantErr: false,
},
{
name: "Custom ACE type",
aceStr: "(0x15;;FA;;;SY)", // SYSTEM_ACCESS_FILTER_ACE_TYPE
want: &ace{
header: &aceHeader{
aceType: 0x15,
aceFlags: 0,
aceSize: 20, // 4 (header) + 4 (access mask) + 12 (SID with 1 sub-authority)
},
accessMask: 0x1F01FF,
sid: createTestSID(1, 5, 18),
},
wantErr: false,
},
// Error cases
{
name: "Invalid format - missing parentheses",
aceStr: "A;;FA;;;SY",
wantErr: true,
},
{
name: "Invalid format - wrong number of components",
aceStr: "(A;FA;;;SY)",
wantErr: true,
},
{
name: "Invalid ACE type",
aceStr: "(X;;FA;;;SY)",
wantErr: true,
},
{
name: "Invalid hex ACE type",
aceStr: "(0xZZ;;FA;;;SY)",
wantErr: true,
},
{
name: "Invalid flags format",
aceStr: "(A;OIC;FA;;;SY)", // Incomplete flag pair
wantErr: true,
},
{
name: "Unknown flag",
aceStr: "(A;XXXX;FA;;;SY)",
wantErr: true,
},
{
name: "Audit flags on non-audit ACE",
aceStr: "(A;SAFA;FA;;;SY)",
wantErr: true,
},
{
name: "Audit ACE without audit flags",
aceStr: "(AU;OICI;FA;;;SY)",
wantErr: true,
},
{
name: "Invalid access mask",
aceStr: "(A;;XX;;;SY)",
wantErr: true,
},
{
name: "Invalid hex access mask",
aceStr: "(A;;0xZZZZ;;;SY)",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotR, err := parseACEString(tt.aceStr)
if (err != nil) != tt.wantErr {
t.Errorf("ParseACEString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
if gotR == nil {
t.Errorf("ParseACEString() returned nil, want non-nil")
return
}
got, err := gotR.toACE(tt.want.sids())
if err != nil {
t.Errorf("toACE() error = %v", err)
return
}
// Compare Header fields
if got.header.aceType != tt.want.header.aceType {
t.Errorf("ACE Type = %v, want %v", got.header.aceType, tt.want.header.aceType)
}
if got.header.aceFlags != tt.want.header.aceFlags {
t.Errorf("ACE Flags = %v, want %v", got.header.aceFlags, tt.want.header.aceFlags)
}
if got.header.aceSize != tt.want.header.aceSize {
t.Errorf("ACE Size = %v, want %v", got.header.aceSize, tt.want.header.aceSize)
}
// Compare AccessMask
if got.accessMask != tt.want.accessMask {
t.Errorf("AccessMask = %v, want %v", got.accessMask, tt.want.accessMask)
}
// Compare SID fields
if got.sid.revision != tt.want.sid.revision {
t.Errorf("SID Revision = %v, want %v", got.sid.revision, tt.want.sid.revision)
}
if got.sid.identifierAuthority != tt.want.sid.identifierAuthority {
t.Errorf("SID Authority = %v, want %v", got.sid.identifierAuthority, tt.want.sid.identifierAuthority)
}
if !reflect.DeepEqual(got.sid.subAuthority, tt.want.sid.subAuthority) {
t.Errorf("SID SubAuthority = %v, want %v", got.sid.subAuthority, tt.want.sid.subAuthority)
}
})
}
}
func TestParseACLString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
aclType string
input string
want *acl
wantErr bool
errString string
}{
{
name: "Invalid ACL type",
aclType: "X",
input: "(A;;FA;;;SY)",
wantErr: true,
errString: "invalid ACL type: must be either 'D' or 'S'",
},
{
name: "Empty DACL",
aclType: "D",
input: "",
want: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "D",
control: seDACLPresent,
},
},
{
name: "Empty SACL",
aclType: "S",
input: "",
want: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "S",
control: seSACLPresent,
},
},
{
name: "Basic DACL with single ACE",
aclType: "D",
input: "(A;;FA;;;SY)",
want: &acl{
aclRevision: 2,
aclSize: 28, // 8 (header) + 20 (ACE size)
aceCount: 1,
aclType: "D",
control: seDACLPresent,
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF, // FA - Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18}, // SYSTEM
},
},
},
},
},
{
name: "DACL with multiple ACEs",
aclType: "D",
input: "(A;;FA;;;SY)(D;;FR;;;WD)",
want: &acl{
aclRevision: 2,
aclSize: 48, // 8 (header) + 20 (first ACE) + 20 (second ACE)
aceCount: 2,
aclType: "D",
control: seDACLPresent,
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF, // FA
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18}, // SYSTEM
},
},
{
header: &aceHeader{
aceType: accessDeniedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x120089, // FR
sid: &sid{
revision: 1,
identifierAuthority: 1,
subAuthority: []uint32{0}, // Everyone
},
},
},
},
},
{
name: "SACL with audit ACE",
aclType: "S",
input: "(AU;SA;FA;;;SY)",
want: &acl{
aclRevision: 2,
aclSize: 28,
aceCount: 1,
aclType: "S",
control: seSACLPresent,
aces: []ace{
{
header: &aceHeader{
aceType: systemAuditACEType,
aceFlags: successfulAccessACE,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
},
},
{
name: "DACL with protected flag",
aclType: "D",
input: "P(A;;FA;;;SY)",
want: &acl{
aclRevision: 2,
aclSize: 28,
aceCount: 1,
aclType: "D",
control: seDACLPresent | seDACLProtected,
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
},
},
{
name: "DACL with auto-inherited flag",
aclType: "D",
input: "AI(A;;FA;;;SY)",
want: &acl{
aclRevision: 2,
aclSize: 28,
aceCount: 1,
aclType: "D",
control: seDACLPresent | seDACLAutoInherited,
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
},
},
{
name: "SACL with multiple flags",
aclType: "S",
input: "PAI(AU;SA;FA;;;SY)",
want: &acl{
aclRevision: 2,
aclSize: 28,
aceCount: 1,
aclType: "S",
control: seSACLPresent | seSACLProtected | seSACLAutoInherited,
aces: []ace{
{
header: &aceHeader{
aceType: systemAuditACEType,
aceFlags: successfulAccessACE,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
},
},
{
name: "Invalid ACE format",
aclType: "D",
input: "A;;FA;;;SY)", // Missing opening parenthesis
wantErr: true,
errString: "invalid ACL format: missing opening parenthesis",
},
{
name: "Missing closing parenthesis",
aclType: "D",
input: "(A;;FA;;;SY",
wantErr: true,
errString: "invalid ACE format: missing closing parenthesis",
},
{
name: "Empty DACL with flags",
aclType: "D",
input: "PAI",
want: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "D",
control: seDACLPresent | seDACLProtected | seDACLAutoInherited,
},
},
}
for _, tt := range tests {
tt := tt // Capture range variable for parallel testing
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotR, err := parseACLString(tt.aclType, tt.input)
// Check error cases
if tt.wantErr {
if err == nil {
t.Errorf("parseACLFromString() error= nil, wantErr = true")
return /* */
}
if tt.errString != "" && err.Error() != tt.errString {
t.Errorf("parseACLFromString() error = %v, wantErr = %v", err, tt.errString)
}
return
}
// Check non-error cases
if err != nil {
t.Errorf("parseACLFromString() unexpected error = %v", err)
return
}
if gotR == nil {
t.Fatal("parseACLFromString() = nil, want non-nil")
}
got, err := gotR.toACL(tt.want.sids())
if err != nil {
t.Errorf("toACL() unexpected error = %v", err)
return
}
// Compare ACL fields
if got.aclRevision != tt.want.aclRevision {
t.Errorf("AclRevision = %v, want %v", got.aclRevision, tt.want.aclRevision)
}
if got.aclSize != tt.want.aclSize {
t.Errorf("AclSize = %v, want %v", got.aclSize, tt.want.aclSize)
}
if got.aceCount != tt.want.aceCount {
t.Errorf("AceCount = %v, want %v", got.aceCount, tt.want.aceCount)
}
if got.aclType != tt.want.aclType {
t.Errorf("AclType = %v, want %v", got.aclType, tt.want.aclType)
}
if got.control != tt.want.control {
t.Errorf("Control = %v, want %v", got.control, tt.want.control)
}
// Compare ACEs
if len(got.aces) != len(tt.want.aces) {
t.Errorf("len(ACEs) = %v, want %v", len(got.aces), len(tt.want.aces))
return
}
for i := range got.aces {
// Compare ACE Header
if got.aces[i].header.aceType != tt.want.aces[i].header.aceType {
t.Errorf("ACE[%d].Header.AceType = %v, want %v",
i, got.aces[i].header.aceType, tt.want.aces[i].header.aceType)
}
if got.aces[i].header.aceFlags != tt.want.aces[i].header.aceFlags {
t.Errorf("ACE[%d].Header.AceFlags = %v, want %v",
i, got.aces[i].header.aceFlags, tt.want.aces[i].header.aceFlags)
}
if got.aces[i].header.aceSize != tt.want.aces[i].header.aceSize {
t.Errorf("ACE[%d].Header.AceSize = %v, want %v",
i, got.aces[i].header.aceSize, tt.want.aces[i].header.aceSize)
}
// Compare ACE AccessMask
if got.aces[i].accessMask != tt.want.aces[i].accessMask {
t.Errorf("ACE[%d].AccessMask = %v, want %v",
i, got.aces[i].accessMask, tt.want.aces[i].accessMask)
}
// Compare ACE SID
if !reflect.DeepEqual(got.aces[i].sid, tt.want.aces[i].sid) {
t.Errorf("ACE[%d].SID = %v, want %v",
i, got.aces[i].sid, tt.want.aces[i].sid)
}
}
})
}
}
func TestFromString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want *SecurityDescriptor
wantErr bool
}{
{
name: "Empty string",
input: "",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seDACLDefaulted | seSACLDefaulted,
},
wantErr: false,
},
{
name: "Owner only",
input: "O:SY",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seGroupDefaulted | seDACLDefaulted | seSACLDefaulted,
ownerSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
wantErr: false,
},
{
name: "Group only",
input: "G:BA",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seDACLDefaulted | seSACLDefaulted,
groupSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{32, 544},
},
},
wantErr: false,
},
{
name: "Owner and Group only",
input: "O:SYG:BA",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seDACLDefaulted | seSACLDefaulted,
ownerSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
groupSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{32, 544},
},
},
wantErr: false,
},
{
name: "Only Empty DACL",
input: "D:",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seSACLDefaulted | seDACLPresent,
dacl: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "D",
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seSACLDefaulted | seDACLPresent, // This field is a copy of SD.Control
},
},
wantErr: false,
},
{
name: "Only Empty SACL",
input: "S:",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seDACLDefaulted | seSACLPresent,
sacl: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "S",
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seDACLDefaulted | seSACLPresent, // This field is a copy of SD.Control
},
},
wantErr: false,
},
{
name: "Protected DACL",
input: "D:P(A;;FA;;;SY)",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seSACLDefaulted | seDACLPresent | seDACLProtected,
dacl: &acl{
aclRevision: 2,
aclSize: 28,
aceCount: 1,
aclType: "D",
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seSACLDefaulted | seDACLPresent | seDACLProtected, // This field is a copy of SD.Control
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
},
},
wantErr: false,
},
{
name: "Complete security descriptor",
input: "O:SYG:BAD:PAI(A;;FA;;;SY)(D;;FR;;;WD)S:AI(AU;SA;FA;;;BA)",
want: &SecurityDescriptor{
revision: 1,
control: seDACLAutoInherited | seDACLPresent | seDACLProtected | seSACLAutoInherited | seSACLPresent | seSelfRelative,
ownerSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
groupSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{32, 544},
},
dacl: &acl{
aclRevision: 2,
aclSize: 48, // 4 bytes for AceCount and Sbz1, 40 bytes for the two ACEs, 4 bytes for Sbz2
aceCount: 2,
aclType: "D",
control: seDACLAutoInherited | seDACLPresent | seDACLProtected |
seSACLAutoInherited | seSACLPresent | seSelfRelative, // This field is a copy of SD.Control
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20, // 4 bytes for ACE header + 4 bytes for mask + 12 bytes for SID
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
{
header: &aceHeader{
aceType: accessDeniedACEType,
aceFlags: 0,
aceSize: 20, // 4 bytes for ACE header + 4 bytes for mask + 12 bytes for SID
},
accessMask: 0x120089,
sid: &sid{
revision: 1,
identifierAuthority: 1,
subAuthority: []uint32{0},
},
},
},
},
sacl: &acl{
aclRevision: 2,
aclSize: 32, // 4 bytes for AceCount and Sbz1, 24 bytes for the single ACE, 4 bytes for Sbz2
aceCount: 1,
aclType: "S",
control: seDACLAutoInherited | seDACLPresent | seDACLProtected |
seSACLAutoInherited | seSACLPresent | seSelfRelative, // This field is a copy of SD.Control
aces: []ace{
{
header: &aceHeader{
aceType: systemAuditACEType,
aceFlags: successfulAccessACE,
aceSize: 24, // 4 bytes for ACE header, 4 bytes for access mask, 8 bytes for SID header, 4 bytes for 1 sub-authority
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{32, 544},
},
},
},
},
},
wantErr: false,
},
{
name: "Invalid format - no separator",
input: "O-SY",
wantErr: true,
},
{
name: "Invalid SID format",
input: "O:INVALID",
wantErr: true,
},
{
name: "Invalid DACL format",
input: "D:X",
wantErr: true,
},
{
name: "Invalid ACE format",
input: "D:(A;FR;;;SY", // Missing closing parenthesis
wantErr: true,
},
{
name: "Non-standard order of components",
input: "D:(A;;FA;;;SY)O:SY",
wantErr: false,
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seGroupDefaulted | seSACLDefaulted | seDACLPresent,
dacl: &acl{
aclRevision: 2,
aclSize: 28, // 4 bytes for AceCount and Sbz1, 20 bytes for the single ACE, 4 bytes for Sbz2
aceCount: 1,
aclType: "D",
control: seSelfRelative | seGroupDefaulted | seSACLDefaulted | seDACLPresent, // This field is a copy of SD.Control
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20, // 4 bytes for ACE header + 4 bytes for mask + 12 bytes for SID
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
},
ownerSID: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
},
{
name: "All control flags",
input: "D:PAIARRNOIOS:PAIARRNOIO",
want: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted |
seDACLPresent | seSACLPresent |
seDACLProtected | seDACLAutoInherited | seDACLAutoInheritRe |
seSACLProtected | seSACLAutoInherited | seSACLAutoInheritRe,
dacl: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "D",
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted |
seDACLPresent | seSACLPresent |
seDACLProtected | seDACLAutoInherited | seDACLAutoInheritRe |
seSACLProtected | seSACLAutoInherited | seSACLAutoInheritRe, // This field is a copy of SD.Control
},
sacl: &acl{
aclRevision: 2,
aclSize: 8,
aclType: "S",
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted |
seDACLPresent | seSACLPresent |
seDACLProtected | seDACLAutoInherited | seDACLAutoInheritRe |
seSACLProtected | seSACLAutoInherited | seSACLAutoInheritRe, // This field is a copy of SD.Control
},
},
wantErr: false,
},
}
for _, tt := range tests {
tt := tt // Capture range variable for parallel testing
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := FromString(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("ParseSecurityDescriptorString() error = nil, wantErr = true")
}
return
}
if err != nil {
t.Errorf("ParseSecurityDescriptorString() unexpected error = %v", err)
return
}
// Compare SecurityDescriptor fields
compareSecurityDescriptors(t, got, tt.want)
})
}
}
func TestParseSIDString(t *testing.T) {
// Test high authority values close to boundary conditions
maxAuthority := uint64(1<<48 - 1)
tests := []struct {
name string
input string
want *sid
wantErr error
}{
{
name: "Well-known SID short form (SYSTEM)",
input: "SY",
want: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
{
name: "Well-known SID full form (SYSTEM)",
input: "S-1-5-18",
want: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
{
name: "Complex SID",
input: "S-1-5-21-3623811015-3361044348-30300820-1013",
want: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 3623811015, 3361044348, 30300820, 1013},
},
},
{
name: "Minimum valid SID",
input: "S-1-0-0",
want: &sid{
revision: 1,
identifierAuthority: 0,
subAuthority: []uint32{0},
},
},
{
name: "Maximum sub-authorities",
input: "S-1-5-21-1-2-3-4-5-6-7-8-9-10-11-12-13-14",
want: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14},
},
},
{
name: "Invalid format - no S- prefix",
input: "1-5-18",
wantErr: ErrInvalidSIDFormat,
},
{
name: "Invalid format - empty string",
input: "",
wantErr: ErrInvalidSIDFormat,
},
{
name: "Invalid format - missing components",
input: "S-1",
wantErr: ErrInvalidSIDFormat,
},
{
name: "Invalid revision",
input: "S-2-5-18",
wantErr: ErrInvalidRevision,
},
{
name: "Invalid revision - not a number",
input: "S-X-5-18",
wantErr: ErrInvalidRevision,
},
{
name: "Invalid authority - not a number",
input: "S-1-X-18",
wantErr: ErrInvalidAuthority,
},
{
name: "Invalid sub-authority - not a number",
input: "S-1-5-X",
wantErr: ErrInvalidSubAuthority,
},
{
name: "Too many sub-authorities",
input: "S-1-5-21-1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16",
wantErr: ErrTooManySubAuthorities,
},
{
name: "High authority value in hex",
input: "S-1-0xFFFFFFFF0000-1-2",
want: &sid{
revision: 1,
identifierAuthority: 0xFFFFFFFF0000,
subAuthority: []uint32{1, 2},
},
},
{
name: "Authority value just below 2^32 in decimal",
input: "S-1-4294967295-1-2",
want: &sid{
revision: 1,
identifierAuthority: 4294967295,
subAuthority: []uint32{1, 2},
},
},
{
name: "Authority value maximum (2^48-1) in hex",
input: fmt.Sprintf("S-1-0x%X-1-2", maxAuthority),
want: &sid{
revision: 1,
identifierAuthority: maxAuthority,
subAuthority: []uint32{1, 2},
},
},
{
name: "Authority value too large in hex",
input: "S-1-0x1000000000000-1-2", // 2^48
wantErr: ErrInvalidAuthority,
},
{
name: "Invalid hex authority format - bad characters",
input: "S-1-0xGHIJKL-1-2",
wantErr: ErrInvalidAuthority,
},
{
name: "Invalid hex authority format - missing digits",
input: "S-1-0x-1-2",
wantErr: ErrInvalidAuthority,
},
}
for _, tt := range tests {
tt := tt // capture range variable for parallel execution
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Enable parallel execution
gotR, err := parseSIDString(tt.input)
if tt.wantErr != nil {
if gotR != nil {
t.Error("parseSIDString() returned non-nil SID when error was expected")
}
if err == nil {
t.Errorf("parseSIDString() error = nil, wantErr %v", tt.wantErr)
return
}
if !errors.Is(err, tt.wantErr) {
t.Errorf("parseSIDString() error = %v, wantErr %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Errorf("parseSIDString() unexpected error = %v", err)
return
}
if gotR == nil {
t.Error("parseSIDString() returned nil SID when success was expected")
return
}
got, err := gotR.toSID(tt.want.sids())
if err != nil {
t.Errorf("toSID() unexpected error = %v", err)
return
}
if got.revision != tt.want.revision {
t.Errorf("Revision = %v, want %v", got.revision, tt.want.revision)
}
if got.identifierAuthority != tt.want.identifierAuthority {
t.Errorf("IdentifierAuthority = %v, want %v",
got.identifierAuthority, tt.want.identifierAuthority)
}
if len(got.subAuthority) != len(tt.want.subAuthority) {
t.Errorf("SubAuthority length = %v, want %v",
len(got.subAuthority), len(tt.want.subAuthority))
} else {
for i := range got.subAuthority {
if got.subAuthority[i] != tt.want.subAuthority[i] {
t.Errorf("SubAuthority[%d] = %v, want %v",
i, got.subAuthority[i], tt.want.subAuthority[i])
}
}
}
})
}
}
func TestComplete(t *testing.T) {
tests := []struct {
name string
r rid
s sid
want *sid
wantErr error
}{
{
name: "Valid completion",
r: rid(300), // on purpose is not a well-known RID so we can verify in test report
s: sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 123, 456, 789, 2983},
},
want: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 123, 456, 789, 300},
},
wantErr: nil,
},
{
name: "Empty sub-authority",
r: rid(300),
s: sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{},
},
want: nil,
wantErr: ErrMissingSubAuthorities,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.r.complete(tt.s)
if !errors.Is(err, tt.wantErr) {
t.Errorf("complete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr == nil {
if got == nil {
t.Fatal("complete() returned nil, want valid sid")
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("complete() = %v, want %v", got, tt.want)
}
}
})
}
}
// Helper function to compare ACL fields
func compareACLs(t *testing.T, prefix string, got, want *acl) {
t.Helper()
if got.aclRevision != want.aclRevision {
t.Errorf("%s.AclRevision = %v, want %v", prefix, got.aclRevision, want.aclRevision)
t.FailNow()
return
}
if got.aclSize != want.aclSize {
t.Errorf("%s.AclSize = %v, want %v", prefix, got.aclSize, want.aclSize)
t.FailNow()
return
}
if got.aceCount != want.aceCount {
t.Errorf("%s.AceCount = %v, want %v", prefix, got.aceCount, want.aceCount)
t.FailNow()
return
}
if got.aclType != want.aclType {
t.Errorf("%s.AclType = %v, want %v", prefix, got.aclType, want.aclType)
t.FailNow()
return
}
if got.control != want.control {
t.Errorf("%s.Control = %v, want %v", prefix, got.control, want.control)
t.FailNow()
return
}
// Compare ACEs
if len(got.aces) != len(want.aces) {
t.Errorf("%s.ACEs length = %v, want %v", prefix, len(got.aces), len(want.aces))
t.FailNow()
return
}
for i := range got.aces {
// Compare ACE Header
if got.aces[i].header.aceType != want.aces[i].header.aceType {
t.Errorf("%s.ACE[%d].Header.AceType = %v, want %v",
prefix, i, got.aces[i].header.aceType, want.aces[i].header.aceType)
}
if got.aces[i].header.aceFlags != want.aces[i].header.aceFlags {
t.Errorf("%s.ACE[%d].Header.AceFlags = %v, want %v",
prefix, i, got.aces[i].header.aceFlags, want.aces[i].header.aceFlags)
}
if got.aces[i].header.aceSize != want.aces[i].header.aceSize {
t.Errorf("%s.ACE[%d].Header.AceSize = %v, want %v",
prefix, i, got.aces[i].header.aceSize, want.aces[i].header.aceSize)
}
// Compare ACE AccessMask
if got.aces[i].accessMask != want.aces[i].accessMask {
t.Errorf("%s.ACE[%d].AccessMask = %v, want %v",
prefix, i, got.aces[i].accessMask, want.aces[i].accessMask)
}
// Compare ACE SID
if !reflect.DeepEqual(got.aces[i].sid, want.aces[i].sid) {
t.Errorf("%s.ACE[%d].SID = %v, want %v",
prefix, i, got.aces[i].sid, want.aces[i].sid)
}
}
}
// Helper function to compare ACE fields
func compareACEs(t *testing.T, prefix string, got, want *ace) {
t.Helper()
// Compare ACE Header
if got.header.aceType != want.header.aceType {
t.Errorf("%s.Header.AceType = %v, want %v", prefix, got.header.aceType, want.header.aceType)
t.FailNow()
return
}
if got.header.aceFlags != want.header.aceFlags {
t.Errorf("%s.Header.AceFlags = %v, want %v", prefix, got.header.aceFlags, want.header.aceFlags)
t.FailNow()
return
}
if got.header.aceSize != want.header.aceSize {
t.Errorf("%s.Header.AceSize = %v, want %v", prefix, got.header.aceSize, want.header.aceSize)
t.FailNow()
return
}
// Compare ACE AccessMask
if got.accessMask != want.accessMask {
t.Errorf("%s.AccessMask = %v, want %v", prefix, got.accessMask, want.accessMask)
t.FailNow()
return
}
// Compare ACE SID
if (got.sid == nil) != (want.sid == nil) {
t.Errorf("%s.SID presence mismatch: got %v, want %v", prefix, got.sid != nil, want.sid != nil)
t.FailNow()
return
} else if got.sid != nil {
compareSIDs(t, prefix+".SID", got.sid, want.sid)
}
}
// Helper function to compare control flags with detailed difference reporting
func compareControlFlags(t *testing.T, got, want uint16) {
t.Helper()
// If flags match exactly, no need to do detailed comparison
if got == want {
return
}
// Map of control flags to their string descriptions
controlFlagNames := map[uint16]string{
seOwnerDefaulted: "SE_OWNER_DEFAULTED",
seGroupDefaulted: "SE_GROUP_DEFAULTED",
seDACLPresent: "SE_DACL_PRESENT",
seDACLDefaulted: "SE_DACL_DEFAULTED",
seSACLPresent: "SE_SACL_PRESENT",
seSACLDefaulted: "SE_SACL_DEFAULTED",
seDACLAutoInheritRe: "SE_DACL_AUTO_INHERIT_RE",
seSACLAutoInheritRe: "SE_SACL_AUTO_INHERIT_RE",
seDACLAutoInherited: "SE_DACL_AUTO_INHERITED",
seSACLAutoInherited: "SE_SACL_AUTO_INHERITED",
seDACLProtected: "SE_DACL_PROTECTED",
seSACLProtected: "SE_SACL_PROTECTED",
seSelfRelative: "SE_SELF_RELATIVE",
}
// Build arrays of flag differences
var (
missingFlags []string // Flags that are in 'want' but not in 'got'
extraFlags []string // Flags that are in 'got' but not in 'want'
)
// Check each known flag
for flag, flagName := range controlFlagNames {
hasFlag := got&flag != 0
wantFlag := want&flag != 0
if wantFlag && !hasFlag {
missingFlags = append(missingFlags, flagName)
} else if hasFlag && !wantFlag {
extraFlags = append(extraFlags, flagName)
}
}
// Detect any unknown flags
knownFlags := uint16(0)
for flag := range controlFlagNames {
knownFlags |= flag
}
unknownGot := got &^ knownFlags
unknownWant := want &^ knownFlags
if unknownGot != 0 {
extraFlags = append(extraFlags, fmt.Sprintf("unknown_flags(0x%04x)", unknownGot))
}
if unknownWant != 0 {
missingFlags = append(missingFlags, fmt.Sprintf("unknown_flags(0x%04x)", unknownWant))
}
// Sort the arrays for consistent output
sort.Strings(missingFlags)
sort.Strings(extraFlags)
// Build the error message
var msg strings.Builder
msg.WriteString(fmt.Sprintf("Control flags mismatch (got=0x%04x, want=0x%04x):\n", got, want))
if len(missingFlags) > 0 {
msg.WriteString(" Missing flags:\n")
for _, flag := range missingFlags {
msg.WriteString(fmt.Sprintf(" - %s\n", flag))
}
}
if len(extraFlags) > 0 {
msg.WriteString(" Extra flags:\n")
for _, flag := range extraFlags {
msg.WriteString(fmt.Sprintf(" + %s\n", flag))
}
}
t.Error(msg.String())
t.FailNow()
}
// Helper function to compare SecurityDescriptor fields
func compareSecurityDescriptors(t *testing.T, got, want *SecurityDescriptor) {
t.Helper()
if got.revision != want.revision {
t.Errorf("Revision = %v, want %v", got.revision, want.revision)
t.FailNow()
return
}
compareControlFlags(t, got.control, want.control)
// Compare Owner SID
if (got.ownerSID == nil) != (want.ownerSID == nil) {
t.Errorf("OwnerSID presence mismatch: got %v, want %v", got.ownerSID != nil, want.ownerSID != nil)
t.FailNow()
return
} else if got.ownerSID != nil {
compareSIDs(t, "OwnerSID", got.ownerSID, want.ownerSID)
}
// Compare Group SID
if (got.groupSID == nil) != (want.groupSID == nil) {
t.Errorf("GroupSID presence mismatch: got %v, want %v", got.groupSID != nil, want.groupSID != nil)
t.FailNow()
return
} else if got.groupSID != nil {
compareSIDs(t, "GroupSID", got.groupSID, want.groupSID)
}
// Compare DACL
if (got.dacl == nil) != (want.dacl == nil) {
t.Errorf("DACL presence mismatch: got %v, want %v", got.dacl != nil, want.dacl != nil)
t.FailNow()
return
} else if got.dacl != nil {
compareACLs(t, "DACL", got.dacl, want.dacl)
}
// Compare SACL
if (got.sacl == nil) != (want.sacl == nil) {
t.Errorf("SACL presence mismatch: got %v, want %v", got.sacl != nil, want.sacl != nil)
t.FailNow()
return
} else if got.sacl != nil {
compareACLs(t, "SACL", got.sacl, want.sacl)
}
}
// Helper function to compare SID fields
func compareSIDs(t *testing.T, prefix string, got, want *sid) {
t.Helper()
if got.revision != want.revision {
t.Errorf("%s.Revision = %v, want %v", prefix, got.revision, want.revision)
t.FailNow()
return
}
if got.identifierAuthority != want.identifierAuthority {
t.Errorf("%s.IdentifierAuthority = %v, want %v", prefix, got.identifierAuthority, want.identifierAuthority)
t.FailNow()
return
}
if len(got.subAuthority) != len(want.subAuthority) {
t.Errorf("%s.SubAuthority length = %v, want %v\nwant: %s\ngot : %s", prefix, len(got.subAuthority), len(want.subAuthority), want.String(), got.String())
t.FailNow()
return
}
for i, sub := range got.subAuthority {
if sub != want.subAuthority[i] {
t.Errorf("%s.SubAuthority[%d] = %v, want %v", prefix, i, sub, want.subAuthority[i])
t.FailNow()
return
}
}
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/go.mod 0000664 0000000 0000000 00000000114 15150254773 0022556 0 ustar 00root root 0000000 0000000 module github.com/cloudsoda/sddl
go 1.22
require golang.org/x/sys v0.28.0
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/go.sum 0000664 0000000 0000000 00000000231 15150254773 0022603 0 ustar 00root root 0000000 0000000 golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/scripts/ 0000775 0000000 0000000 00000000000 15150254773 0023143 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/scripts/sddl.ps1 0000664 0000000 0000000 00000007335 15150254773 0024526 0 ustar 00root root 0000000 0000000 # enable this file with:
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
# Test-SddlConversion -Path "path-to-file.ext"
function Test-SddlConversion {
<#
.SYNOPSIS
Converts and displays the Security Descriptor of a file in both SDDL string and binary formats.
.DESCRIPTION
This function takes a file path as input, retrieves its Security Descriptor,
and then displays it in two formats:
1. As an SDDL (Security Descriptor Definition Language) string
2. As a base64-encoded binary representation
.PARAMETER Path
The path to the file or directory whose Security Descriptor is to be displayed.
.EXAMPLE
Test-SddlConversion -Path "C:\Windows\System32\notepad.exe"
.NOTES
This function is useful for debugging and verifying Security Descriptor conversions.
It can help ensure that the SDDL string and binary representations are consistent.
#>
param (
[Parameter(Mandatory=$true)]
[string]$Path
)
Write-Host "SDDL string:"
$acl = Get-Acl $Path
Write-Host $acl.Sddl
Write-Host "`nBase64 binary form:"
$binary = $acl.GetSecurityDescriptorBinaryForm()
Write-Host ([Convert]::ToBase64String($binary))
}
# Set-ExecutionPolicy -Path "path-to-file.ext"
function Set-CustomOwnership {
<#
.SYNOPSIS
Sets custom ownership for a file or directory using specified RIDs.
.DESCRIPTION
This function changes the owner and group of a specified file or directory
using Relative Identifiers (RIDs) for the local machine's domain.
.PARAMETER Path
The path to the file or directory to modify.
.PARAMETER OwnerRID
The RID for the new owner. For example, 500 represents the local Administrator.
.PARAMETER GroupRID
The RID for the new group. For example, 512 represents the Domain Admins group.
.EXAMPLE
Set-CustomOwnership -Path "C:\example.txt" -OwnerRID 500 -GroupRID 512
#>
param (
[Parameter(Mandatory=$true)]
[string]$Path,
[Parameter(Mandatory=$true)]
[string]$OwnerRID,
[Parameter(Mandatory=$true)]
[string]$GroupRID
)
if (-not (Test-Path $Path)) {
Write-Error "File not found: $Path"
return
}
$localAdmin = Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount = True AND SID LIKE 'S-1-5-21-%-500'"
if (-not $localAdmin) {
Write-Error "Could not find local Administrator account"
return
}
$sidParts = $localAdmin.SID -split "-"
if ($sidParts.Length -lt 7) {
Write-Error "Invalid Administrator SID format"
return
}
$domainPart = $sidParts[0..($sidParts.Length-2)] -join "-"
$ownerSID = "$domainPart-$OwnerRID"
$groupSID = "$domainPart-$GroupRID"
Write-Host "Constructed Owner SID: $ownerSID"
Write-Host "Constructed Group SID: $groupSID"
Write-Host "Current path: $((Get-Item $Path).FullName)"
$acl = Get-Acl $Path
Write-Host "Current owner: $($acl.Owner)"
Write-Host "Current group: $($acl.Group)"
try {
$ownerSid = New-Object System.Security.Principal.SecurityIdentifier($ownerSID)
$groupSid = New-Object System.Security.Principal.SecurityIdentifier($groupSID)
$acl.SetOwner($ownerSid)
$acl.SetGroup($groupSid)
Set-Acl -Path $Path -AclObject $acl
$newAcl = Get-Acl $Path
Write-Host "New owner: $($newAcl.Owner)"
Write-Host "New group: $($newAcl.Group)"
}
catch {
Write-Error "Failed to change ownership: $_"
Write-Error "Exception details: $($_.Exception.Message)"
}
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/sddl.go 0000664 0000000 0000000 00000103333 15150254773 0022734 0 ustar 00root root 0000000 0000000 package sddl
import (
"encoding/binary"
"errors"
"fmt"
"slices"
"strings"
)
// Define common errors
var (
ErrInvalidAuthority = errors.New("invalid authority value")
ErrInvalidRevision = errors.New("invalid SID revision")
ErrInvalidSIDFormat = errors.New("invalid SID format")
ErrInvalidSubAuthority = errors.New("invalid sub-authority value")
ErrMissingDomainInformation = errors.New("missing domain information")
ErrMissingSubAuthorities = errors.New("missing sub-authorities")
ErrTooManySubAuthorities = errors.New("too many sub-authorities")
)
// constants for SECURITY_DESCRIPTOR parsing
//
// Defaulted refers to the situation where a security descriptor is taken from somewhere else,
// usually from the parent object. This is a common situation where a file inherits its permissions
// from the parent directory. In this case, the file's DACL is defaulted to the DACL of the directory.
//
// Inherited refers to the situation where a security descriptor is taken from somewhere else,
// usually from the parent object, but it is not the same as the parent object's security descriptor.
// It is a copy of the parent object's security descriptor, but it is applied to the child object.
//
// Protected refers to the situation where the security descriptor is protected against
// inheritance. This means that the security descriptor is not inherited by any child
// objects, and any changes to the security descriptor will not affect the child objects.
//
// Auto-inherited refers to the situation where a security descriptor is automatically
// inherited from the parent object. This means that the security descriptor is copied
// from the parent object to the child object when the child object is created.
//
// Auto-inherited required (RE) refers to the situation where a security descriptor is
// automatically inherited from the parent object, and the child object must inherit the
// security descriptor. This means that the child object cannot override the security
// descriptor of the parent object.
//
// # See
//
// - https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-control
// - https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-ace_header
// - https://docs.microsoft.com/en-us/windows/win32/secauthz/access-mask-format
const (
// Control flags
// seOwnerDefaulted - Owner is defaulted to current owner (SE_OWNER_DEFAULTED)
seOwnerDefaulted = 0x0001
// seGroupDefaulted - Group is defaulted to current group (SE_GROUP_DEFAULTED)
seGroupDefaulted = 0x0002
// seDACLPresent - Indicates that a DACL is present (SE_DACL_PRESENT)
seDACLPresent = 0x0004
// seDACLDefaulted - Indicates that DACL is defaulted (SE_DACL_DEFAULTED)
seDACLDefaulted = 0x0008
// seSACLPresent - SACL is present (SE_SACL_PRESENT)
seSACLPresent = 0x0010
// seSACLDefaulted - SACL is defaulted (SE_SACL_DEFAULTED)
seSACLDefaulted = 0x0020
// seDACLTrusted - DACL is trusted (SE_DACL_TRUSTED)
// In this context, 'trusted' means that the DACL was set explicitly by a user or an application,
// and should not be modified by the system.
seDACLTrusted = 0x0040
// seServerSecurity - Server security (SE_SERVER_SECURITY)
// This flag is set when the security descriptor is from a server object, such as a file share.
seServerSecurity = 0x0080
// seDACLAutoInheritRe - Auto-inherit parent DACL (SE_DACL_AUTO_INHERIT_RE)
seDACLAutoInheritRe = 0x0100
// seSACLAutoInheritRe - Auto-inherit parent SACL (SE_SACL_AUTO_INHERIT_RE)
seSACLAutoInheritRe = 0x0200
// seDACLAutoInherited - Auto-inherited DACL (SE_DACL_AUTO_INHERITED)
seDACLAutoInherited = 0x0400
// seSACLAutoInherited - Auto-inherited SACL (SE_SACL_AUTO_INHERITED)
seSACLAutoInherited = 0x0800
// seDACLProtected - DACL is protected (SE_DACL_PROTECTED)
seDACLProtected = 0x1000
// seSACLProtected - SACL is protected (SE_SACL_PROTECTED)
seSACLProtected = 0x2000
// seResourceManagerControlValid - Resource manager control is valid (SE_RESOURCE_MANAGER_CONTROL_VALID)
// This flag is set when the resource manager has verified that the security descriptor is valid.
// It is used by the system to ensure that the security descriptor was set by a trusted entity.
seResourceManagerControlValid = 0x4000
// seSelfRelative - Self relative flag which means the information is packed in a contiguous region of memory (SE_SELF_RELATIVE)
seSelfRelative = 0x8000
// ACE types
// accessAllowedACEType - Access allowed (ACCESS_ALLOWED_ACE_TYPE)
accessAllowedACEType = 0x0
// accessDeniedACEType - Access denied (ACCESS_DENIED_ACE_TYPE)
accessDeniedACEType = 0x1
// systemAuditACEType - System audit (SYSTEM_AUDIT_ACE_TYPE)
// This ACE type is used to specify system-level auditing for an object.
// It allows the system to track all access to the object and generate an audit log entry.
systemAuditACEType = 0x2
// systemAlarmACEType - System alarm (SYSTEM_ALARM_ACE_TYPE)
// This ACE type is used to specify system-level alarms for an object.
// It allows the system to generate alarms in response to access to the object.
systemAlarmACEType = 0x3
// accessAllowedObjectACEType - Access allowed object (ACCESS_ALLOWED_OBJECT_ACE_TYPE)
accessAllowedObjectACEType = 0x5
// ACE flags
// objectInheritACE - Object inherit (OBJECT_INHERIT_ACE)
// This flag is set when the ACE is inherited by objects of the same type as the object being modified.
objectInheritACE = 0x01
// containerInheritACE - Container inherit (CONTAINER_INHERIT_ACE)
// This flag is set when the ACE is inherited by objects of a different type than the object being modified.
containerInheritACE = 0x02
// noPropagateInheritACE - No propagate inherit (NO_PROPAGATE_INHERIT_ACE)
// This flag is set when the ACE is inherited by objects of a different type than the object being modified.
noPropagateInheritACE = 0x04
// inheritOnlyACE - Inherit only (INHERIT_ONLY_ACE)
// This flag is set when the ACE is inherited by objects of a different type than the object being modified.
inheritOnlyACE = 0x08
// inheritedACE - Inherited (INHERITED_ACE)
inheritedACE = 0x10
// successfulAccessACE - Successful access (SUCCESSFUL_ACCESS_ACE)
// This flag is set when the ACE type is ACCESS_ALLOWED_ACE_TYPE and the access is successful.
successfulAccessACE = 0x40
// failedAccessACE - Failed access (FAILED_ACCESS_ACE)
// This flag is set when the ACE type is ACCESS_DENIED_ACE_TYPE and the access is denied.
failedAccessACE = 0x80
)
// wellKnownSids maps short SID names to their full string representation as
// documented in the Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
var wellKnownSids = map[string]string{
"S-1-0-0": "NULL",
"S-1-1-0": "WD", // Everyone
"S-1-2-0": "LG", // Local GROUP
"S-1-3-0": "CC", // CREATOR CREATOR
"S-1-3-1": "CO", // CREATOR OWNER
"S-1-3-2": "CG", // CREATOR GROUP
"S-1-3-3": "OW", // OWNER RIGHTS
"S-1-5-1": "DU", // DIALUP
"S-1-5-2": "AN", // NETWORK
"S-1-5-3": "BT", // BATCH
"S-1-5-4": "IU", // INTERACTIVE
"S-1-5-6": "SU", // SERVICE
"S-1-5-7": "AS", // ANONYMOUS
"S-1-5-8": "PS", // PROXY
"S-1-5-9": "ED", // ENTERPRISE DOMAIN CONTROLLERS
"S-1-5-10": "SS", // SELF
"S-1-5-11": "AU", // Authenticated Users
"S-1-5-12": "RC", // RESTRICTED CODE
"S-1-5-18": "SY", // LOCAL SYSTEM
"S-1-5-32-544": "BA", // BUILTIN\Administrators
"S-1-5-32-545": "BU", // BUILTIN\Users
"S-1-5-32-546": "BG", // BUILTIN\Guests
"S-1-5-32-547": "PU", // BUILTIN\Power Users
"S-1-5-32-548": "AO", // BUILTIN\Account Operators
"S-1-5-32-549": "SO", // BUILTIN\Server Operators
"S-1-5-32-550": "PO", // BUILTIN\Print Operators
"S-1-5-32-551": "BO", // BUILTIN\Backup Operators
"S-1-5-32-552": "RE", // BUILTIN\Replicator
"S-1-5-32-554": "RU", // BUILTIN\Pre-Windows 2000 Compatible Access
"S-1-5-32-555": "RD", // BUILTIN\Remote Desktop Users
"S-1-5-32-556": "NO", // BUILTIN\Network Configuration Operators
"S-1-5-64-10": "AA", // Administrator Access
"S-1-5-64-14": "RA", // Remote Access
"S-1-5-64-21": "OA", // Operation Access
}
// accessMaskComponents maps permission codes to their bit values
var accessMaskComponents = map[string]uint32{
// Generic Rights (0xF0000000)
"GA": 0x10000000, // Generic All
"GX": 0x20000000, // Generic Execute
"GW": 0x40000000, // Generic Write
"GR": 0x80000000, // Generic Read
// ??
"MA": 0x02000000, // Maximum Allowed
"AS": 0x01000000, // Access System Security
// Standard Rights (0x001F0000)
"SY": 0x00100000, // Synchronize
"WO": 0x00080000, // Write Owner
"WD": 0x00040000, // Write DAC
"RC": 0x00020000, // Read Control
"SD": 0x00010000, // Delete
// Directory Service Object Access Rights (0x0000FFFF)
"CR": 0x00000100, // Control Access
"LO": 0x00000080, // List Object
"DT": 0x00000040, // Delete Tree
"WP": 0x00000020, // Write Property
"RP": 0x00000010, // Read Property
"SW": 0x00000008, // Self Write
"LC": 0x00000004, // List Children
"DC": 0x00000002, // Delete Child
"CC": 0x00000001, // Create Child
}
// WellKnownAccessMasks maps common combined access masks to their string representations
var wellKnownAccessMasks = map[uint32]string{
0x001f01ff: "FA", // File All (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF)
0x00120089: "FR", // File Read (READ_CONTROL | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE)
0x00120116: "FW", // File Write (READ_CONTROL | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE)
0x001200a0: "FX", // File Execute (READ_CONTROL | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE)
}
// reversedAccessMaskComponents maps access mask values to their short names
var reversedAccessMaskComponents = make(map[uint32]string)
// reverseWellKnownSids maps short SID names to their full string representation
var reverseWellKnownSids = make(map[string]string)
// reverseWellKnownAccessMasks maps access masks to their short names
var reverseWellKnownAccessMasks = make(map[string]uint32)
func init() {
// Initialize the reverse mapping of wellKnownSids
for k, v := range wellKnownSids {
reverseWellKnownSids[v] = k
}
// Initialize the reverse mapping of wellKnownAccessMasks
for k, v := range wellKnownAccessMasks {
reverseWellKnownAccessMasks[v] = k
}
// Initialize the reverse mapping of accessMaskComponents
for k, v := range accessMaskComponents {
reversedAccessMaskComponents[v] = k
}
}
// ace represents a Windows Access Control Entry (ACE)
// The ace structure is used in the ACL data structure to specify access control information for an object.
// It contains information such as the type of ace, the access control information, and the SID of the trustee.
// See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-ace
type ace struct {
// header is the ACE header, which contains the type of ACE, flags, and size.
header *aceHeader
// accessMask is the access mask containing the access rights that are being granted or denied.
// It is a combination of the standard access rights and the specific rights defined by the object.
// See https://docs.microsoft.com/en-us/windows/win32/consent/access-mask-format
accessMask uint32
// sid is the sid of the trustee, which is the user or group that the ACE is granting or denying access to.
sid *sid
}
// accessString returns a string representation of the access mask, checking for well-known combinations first
func (e *ace) accessString() string {
var accessStr string
if value, ok := wellKnownAccessMasks[e.accessMask]; ok {
accessStr = value
} else {
maskComponents, remainingMask := decomposeAccessMask(e.accessMask)
accessStr = strings.Join(maskComponents, "")
if remainingMask != 0 {
accessStr = fmt.Sprintf("0x%08X", e.accessMask)
}
}
return accessStr
}
// Binary converts an ACE structure to its binary representation following Windows format.
// The binary format is:
// - ACE Header:
// - AceType (1 byte)
// - AceFlags (1 byte)
// - AceSize (2 bytes, little-endian)
//
// - AccessMask (4 bytes, little-endian)
// - SID in binary format (variable size)
func (e *ace) Binary() []byte {
// Validate ACE structure
if e == nil {
panic("cannot convert nil ACE to binary")
}
if e.header == nil {
panic("cannot convert ACE with nil header to binary")
}
if e.sid == nil {
panic("cannot convert ACE with nil SID to binary")
}
// Convert SID to binary first to get its size
sidBinary := e.sid.Binary()
// Calculate total ACE size: 4 (header) + 4 (access mask) + len(sidBinary)
aceSize := 4 + 4 + len(sidBinary)
if aceSize > 65535 { // Check if size fits in uint16
panic("ACE size exceeds maximum size of 65535 bytes")
}
// Validate that the calculated size matches the header size
if uint16(aceSize) != e.header.aceSize {
panic("calculated ACE size doesn't match header size")
}
// Create result buffer
result := make([]byte, aceSize)
// Set ACE header
result[0] = e.header.aceType
result[1] = e.header.aceFlags
binary.LittleEndian.PutUint16(result[2:4], uint16(aceSize))
// Set access mask (4 bytes, little-endian)
binary.LittleEndian.PutUint32(result[4:8], e.accessMask)
// Copy SID binary representation
copy(result[8:], sidBinary)
return result
}
// flagsString converts the ACE flags to string
func (e *ace) flagsString() string {
var flagsStr string
if e.header.aceType == systemAuditACEType {
if e.header.aceFlags&successfulAccessACE != 0 {
flagsStr += "SA"
}
if e.header.aceFlags&failedAccessACE != 0 {
flagsStr += "FA"
}
}
// Add inheritance flags
if e.header.aceFlags&objectInheritACE != 0 {
flagsStr += "OI"
}
if e.header.aceFlags&containerInheritACE != 0 {
flagsStr += "CI"
}
if e.header.aceFlags&inheritOnlyACE != 0 {
flagsStr += "IO"
}
if e.header.aceFlags&inheritedACE != 0 {
flagsStr += "ID"
}
return flagsStr
}
// String returns a string representation of the ACE.
func (e *ace) String() string {
return fmt.Sprintf("(%s;%s;%s;;;%s)", e.typeString(), e.flagsString(), e.accessString(), e.sid.String())
}
// StringIndent returns a string representation of the ACE with the specified indentation margin.
// The margin parameter specifies the number of spaces to prepend to the output.
func (e *ace) StringIndent(margin int) string {
eStr := fmt.Sprintf("(%s;%s;%s;;;%s)", e.typeString(), e.flagsString(), e.accessString(), e.sid.DebugString())
return strings.Repeat(" ", margin) + eStr
}
// typeString returns a string representation of the ACE type
func (e *ace) typeString() string {
switch e.header.aceType {
case accessAllowedACEType:
return "A"
case accessDeniedACEType:
return "D"
case systemAuditACEType:
return "AU"
default:
return fmt.Sprintf("0x%02X", e.header.aceType)
}
}
// aceHeader represents the Windows ACE_HEADER structure, which is the header of an Access Control Entry (ACE)
// See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/628ebb1d-c509-4ea0-a10f-77ef97ca4586
type aceHeader struct {
// acetype - Type of ACE (ACCESS_ALLOWED_ACE_TYPE, ACCESS_DENIED_ACE_TYPE, etc.)
aceType byte
// aceflags (OBJECT_INHERIT_ACE, CONTAINER_INHERIT_ACE, etc.)
aceFlags byte
// aceSize is the total size of the ACE in bytes
aceSize uint16
}
// acl represents the Windows Access Control List (ACL) structure
// See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/20233ed8-a6c6-4097-aafa-dd545ed24428
type acl struct {
// aclRevision is the revision of the ACL format. Currently, only revision 2 is supported. See
aclRevision byte
// Sbz1 is reserved; must be zero
sbzl byte
// aclSize is the size of the ACL in bytes
aclSize uint16
// aceCount is the number of ACEs in the ACL
aceCount uint16
// sbz2 is reserved; must be zero
sbz2 uint16
// The following fields are not part of the original structure, but they are used in conjuntion with AclType and Control to build the string representation
// aclType is "D" for DACL, "S" for SACL.
//
// This field is not part of original structure, but it is used in conjuntion with Control to build the string representation
aclType string
// control are the Security Descriptor control flags defined in
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7d4dac05-9cef-4563-a058-f108abecce1d
//
// This field is not part of original structure, but it is used in conjuntion with AclType to build the string representation
control uint16
// aces is the list of Access Control Entries (ACEs)
//
// This field is not part of original structure, but it is used to build the string representation.
aces []ace
}
// Binary converts an ACL structure to its binary representation following Windows format.
//
// The binary format consists of:
// - ACL Header:
// - Revision (1 byte)
// - Sbz1 (1 byte, reserved)
// - AclSize (2 bytes, little-endian)
// - AceCount (2 bytes, little-endian)
// - Sbz2 (2 bytes, reserved)
//
// - Array of ACEs in binary format (variable size)
func (a *acl) Binary() []byte {
// Convert all ACEs to binary first to validate them and calculate total size
aceBinaries := make([][]byte, len(a.aces))
totalAceSize := 0
for i := range a.aces {
aceBinaries[i] = a.aces[i].Binary()
totalAceSize += len(aceBinaries[i])
}
// Calculate total ACL size: 8 (header) + sum of ACE sizes
aclSize := 8 + totalAceSize
if aclSize > 65535 { // Check if size fits in uint16
panic(fmt.Errorf("ACL size %d exceeds maximum size of 65535 bytes", aclSize))
}
// Validate that calculated size matches the ACL size field
if uint16(aclSize) != a.aclSize {
panic(fmt.Errorf("calculated ACL size %d doesn't match header size %d", aclSize, a.aclSize))
}
// Validate ACE count
if uint16(len(a.aces)) != a.aceCount {
panic(fmt.Errorf("actual ACE count %d doesn't match header count %d", len(a.aces), a.aceCount))
}
// Create result buffer
result := make([]byte, aclSize)
// Set ACL header
result[0] = a.aclRevision
result[1] = a.sbzl // Reserved byte
binary.LittleEndian.PutUint16(result[2:4], uint16(aclSize))
binary.LittleEndian.PutUint16(result[4:6], uint16(len(a.aces)))
binary.LittleEndian.PutUint16(result[6:8], a.sbz2) // Reserved bytes
// Copy each ACE's binary representation
offset := 8
for _, aceBinary := range aceBinaries {
copy(result[offset:], aceBinary)
offset += len(aceBinary)
}
return result
}
// FlagsString returns a string representation of the ACL flags.
// It constructs the flag string based on the ACL type (DACL or SACL) and the control flags.
// The returned string format is "Type:Flags", where Type is either "D" for DACL or "S" for SACL,
// and Flags is a combination of the following:
// - "P" for Protected
// - "AI" for Auto-Inherited
// - "AR" for Auto-Inherit Required
// - "R" for Read-Only
//
// If no flags are set, it returns just the ACL type.
func (a *acl) FlagsString() string {
var aclFlags []string
if a.aclType == "D" {
if a.control&seDACLProtected != 0 {
aclFlags = append(aclFlags, "P")
}
if a.control&seDACLAutoInherited != 0 {
aclFlags = append(aclFlags, "AI")
}
if a.control&seDACLAutoInheritRe != 0 {
aclFlags = append(aclFlags, "AR")
}
if a.control&seDACLDefaulted != 0 {
aclFlags = append(aclFlags, "R")
}
} else if a.aclType == "S" {
if a.control&seSACLProtected != 0 {
aclFlags = append(aclFlags, "P")
}
if a.control&seSACLAutoInherited != 0 {
aclFlags = append(aclFlags, "AI")
}
if a.control&seSACLAutoInheritRe != 0 {
aclFlags = append(aclFlags, "AR")
}
if a.control&seSACLDefaulted != 0 {
aclFlags = append(aclFlags, "R")
}
}
return strings.Join(aclFlags, "")
}
func (a *acl) String() string {
result := a.FlagsString()
var aces []string
for _, ace := range a.aces {
aces = append(aces, ace.String())
}
return result + strings.Join(aces, "")
}
// StringIndent returns a string representation of the ACL with the specified indentation margin.
// It formats the ACL flags and each ACE on separate lines, with ACEs indented 4 spaces further
// than the margin parameter.
//
// Parameters:
// - margin: number of spaces to prepend to each line
//
// Returns a multi-line string with the ACL flags followed by indented ACEs.
func (a *acl) StringIndent(margin int) string {
marginStr := strings.Repeat(" ", margin)
bldr := strings.Builder{}
bldr.WriteString(marginStr + a.FlagsString() + "\n")
for _, ace := range a.aces {
bldr.WriteString(ace.StringIndent(margin+4) + "\n")
}
return bldr.String()
}
// SecurityDescriptor represents the Windows SECURITY_DESCRIPTOR structure.
//
// A security descriptor is a data structure that contains the security
// information associated with a securable object, such as a file, registry
// key, or network share. It includes an owner SID, a primary group SID,
// a discretionary access control list (DACL) that specifies the access
// rights allowed or denied to specific users or groups, and a system
// access control list (SACL) that specifies the types of auditing that
// are to be generated for specific users or groups.
//
// See:
// - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7d4dac05-9cef-4563-a058-f108abecce1d
// - https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-control
type SecurityDescriptor struct {
// revision of the security descriptor format.
// Valid values are 1 (for Windows XP and later) and 2 (for Windows 2000).
// The revision determines the offset of the owner and group SIDs:
// in revision 1, the offset is 4 bytes, and in revision 2, the offset is 8 bytes.
revision byte
// sbzl is Reserved; must be zero
sbzl byte
// control flags
// The control field specifies the type of security descriptor and other flags.
control uint16
// Offset of owner SID in bytes relative to start of security descriptor
ownerOffset uint32
// Offset of group SID in bytes relative to start of security descriptor
groupOffset uint32
// Offset of SACL in bytes relative to start of security descriptor
saclOffset uint32
// Offset of DACL in bytes relative to start of security descriptor
daclOffset uint32
// The following fields are not part of original structure but are needed for string representation
// ownerSID is the Owner of the SID.
//
// This field is not part of original structure, but it is used to build the string representation.
ownerSID *sid
// groupSID is the Group of the SID.
//
// This field is not part of original structure, but it is used to build the string representation.
groupSID *sid
// sacl is the System Access Control List (SACL).
//
// The sacl is used to specify the types of auditing that are to be generated for specific users or groups.
// It is used to generate audit logs when a user or group attempts to access a securable object in a certain way.
//
// This field is not part of original structure, but it is used to build the string representation.
sacl *acl
// dacl is the Discretionary Access Control List (DACL).
//
// The dacl controls access to the securable object based on the user or group that is accessing it.
//
// This field is not part of original structure, but it is used to build the string representation.
dacl *acl
}
// Binary converts a SecurityDescriptor structure to its binary representation in self-relative format.
// The binary format consists of:
// - Fixed part:
// - Revision (1 byte)
// - Sbz1 (1 byte, reserved)
// - Control (2 bytes, little-endian)
// - OwnerOffset (4 bytes, little-endian)
// - GroupOffset (4 bytes, little-endian)
// - SaclOffset (4 bytes, little-endian)
// - DaclOffset (4 bytes, little-endian)
//
// - Variable part (in canonical order):
// - Owner SID
// - Group SID
// - SACL
// - DACL
func (sd *SecurityDescriptor) Binary() []byte {
// Force SE_SELF_RELATIVE flag as we're creating a self-relative security descriptor
sd.control |= seSelfRelative
// Convert all components to binary first to calculate total size and validate
var ownerBinary, groupBinary, saclBinary, daclBinary []byte
// Convert Owner SID if present
if sd.ownerSID != nil {
ownerBinary = sd.ownerSID.Binary()
}
// Convert Group SID if present
if sd.groupSID != nil {
groupBinary = sd.groupSID.Binary()
}
// Convert SACL if present and control flags indicate it should be
if sd.sacl != nil {
if sd.control&seSACLPresent == 0 {
panic("SACL present but SE_SACL_PRESENT flag not set")
}
saclBinary = sd.sacl.Binary()
} else if sd.control&seSACLPresent != 0 {
panic("SE_SACL_PRESENT flag set but SACL is nil")
}
// Convert DACL if present and control flags indicate it should be
if sd.dacl != nil {
if sd.control&seDACLPresent == 0 {
panic("DACL present but SE_DACL_PRESENT flag not set")
}
daclBinary = sd.dacl.Binary()
} else if sd.control&seDACLPresent != 0 {
panic("SE_DACL_PRESENT flag set but DACL is nil")
}
// Calculate total size: 20 (fixed header) + sizes of all components
totalSize := 20 + len(ownerBinary) + len(groupBinary) + len(saclBinary) + len(daclBinary)
// Create result buffer
result := make([]byte, totalSize)
// Set fixed header
result[0] = sd.revision
result[1] = sd.sbzl
binary.LittleEndian.PutUint16(result[2:4], sd.control)
// Initialize current offset for variable part
currentOffset := 20
// Set Owner SID and its offset if present
if ownerBinary != nil {
binary.LittleEndian.PutUint32(result[4:8], uint32(currentOffset))
copy(result[currentOffset:], ownerBinary)
currentOffset += len(ownerBinary)
}
// Set Group SID and its offset if present
if groupBinary != nil {
binary.LittleEndian.PutUint32(result[8:12], uint32(currentOffset))
copy(result[currentOffset:], groupBinary)
currentOffset += len(groupBinary)
}
// Set SACL and its offset if present
if saclBinary != nil {
binary.LittleEndian.PutUint32(result[12:16], uint32(currentOffset))
copy(result[currentOffset:], saclBinary)
currentOffset += len(saclBinary)
}
// Set DACL and its offset if present
if daclBinary != nil {
binary.LittleEndian.PutUint32(result[16:20], uint32(currentOffset))
copy(result[currentOffset:], daclBinary)
}
return result
}
func (sd *SecurityDescriptor) String() string {
var parts []string
if sd.ownerSID != nil {
ownerSIDString := sd.ownerSID.String()
parts = append(parts, fmt.Sprintf("O:%s", ownerSIDString))
}
if sd.groupSID != nil {
groupSIDString := sd.groupSID.String()
parts = append(parts, fmt.Sprintf("G:%s", groupSIDString))
}
if sd.dacl != nil {
daclStr := sd.dacl.String()
parts = append(parts, fmt.Sprintf("D:%s", daclStr))
}
if sd.sacl != nil {
saclStr := sd.sacl.String()
parts = append(parts, fmt.Sprintf("S:%s", saclStr))
}
return strings.Join(parts, "")
}
// StringIndent returns a formatted string representation of the SecurityDescriptor with the specified
// indentation margin. It includes the control flags, owner, group, and ACLs (if present), each
// properly indented for better readability.
//
// Parameters:
// - margin: number of spaces to prepend to each line
//
// Returns a multi-line string containing the formatted security descriptor components.
func (sd *SecurityDescriptor) StringIndent(margin int) string {
marginStr := strings.Repeat(" ", margin)
bldr := strings.Builder{}
if sd.ownerSID != nil {
bldr.WriteString(marginStr + "O: " + sd.ownerSID.String() + "\n")
}
if sd.groupSID != nil {
bldr.WriteString(marginStr + "G: " + sd.groupSID.String() + "\n")
}
if sd.dacl != nil {
bldr.WriteString(marginStr + "D:\n" + sd.dacl.StringIndent(margin+4) + "\n")
}
if sd.sacl != nil {
bldr.WriteString(marginStr + "S:\n" + sd.sacl.StringIndent(margin+4) + "\n")
}
return bldr.String()
}
// sid represents a Windows Security Identifier (SID)
//
// Note: SubAuthorityCount is needed for parsing, but once the structure is built, it can be determined from SubAuthority, hence the field is omitted in the structure
type sid struct {
// revision indicates the revision level of the SID structure.
// It is used to determine the format of the SID structure.
// The current revision level is 1.
revision byte
// identifierAuthority is the authority part of the SID. It is a 6-byte
// value that identifies the authority issuing the SID. The high-order
// 2 bytes contain the revision level of the SID. The next byte is the
// identifier authority value. The low-order 3 bytes are zero.
identifierAuthority uint64
// subAuthority is the sub-authority parts of the SID.
// The number of sub-authorities is determined by SubAuthorityCount.
// The sub-authorities are in the order they appear in the SID string
// (i.e. S-1-5-21-a-b-c-d-e, where d and e are sub-authorities).
// The sub-authorities are stored in little-endian order.
// See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid
subAuthority []uint32
}
// Binary converts a SID structure to its binary representation following Windows format.
// The binary format is:
// - Revision (1 byte)
// - SubAuthorityCount (1 byte)
// - IdentifierAuthority (6 bytes, big-endian)
// - SubAuthorities (4 bytes each, little-endian)
func (s *sid) Binary() []byte {
// Validate SID structure
if s == nil {
panic("cannot convert nil SID to binary")
}
if s.revision != 1 {
panic(fmt.Errorf("%w: revision must be 1, was %d", ErrInvalidSIDFormat, s.revision))
}
// Check number of sub-authorities (maximum is 15 in Windows)
if len(s.subAuthority) > 15 {
panic(fmt.Errorf("%w: got %d, maximum is 15", ErrTooManySubAuthorities, len(s.subAuthority)))
}
// Check authority value fits in 48 bits
if s.identifierAuthority >= 1<<48 {
panic(fmt.Errorf("%w: value %d exceeds maximum of 2^48-1", ErrInvalidAuthority, s.identifierAuthority))
}
// Calculate total size:
// 1 byte revision + 1 byte count + 6 bytes authority + (4 bytes × number of sub-authorities)
size := 8 + (4 * len(s.subAuthority))
result := make([]byte, size)
// Set revision
result[0] = s.revision
// Set sub-authority count
result[1] = byte(len(s.subAuthority))
// Set authority value - convert uint64 to 6 bytes in big-endian order
// We're using big-endian because Windows stores the authority as a 6-byte
// value in network byte order (big-endian)
auth := s.identifierAuthority
for i := 7; i >= 2; i-- {
result[i] = byte(auth & 0xFF)
auth >>= 8
}
// Set sub-authorities in little-endian order
// Windows stores these as 32-bit integers in little-endian format
for i, subAuth := range s.subAuthority {
offset := 8 + (4 * i)
binary.LittleEndian.PutUint32(result[offset:], subAuth)
}
return result
}
// DebugString returns a string representation of the SID with additional debugging information.
// It includes the raw string representation whithout converting to well-known SID, alongside the
// final SID (in case they were different)
func (s *sid) DebugString() string {
st := s.String()
rs := s.rawString()
if st != rs {
return fmt.Sprintf("%s [%s]", st, rs)
}
return st
}
// Domain returns a slice of uint32 containing all sub-authorities between the first and last one.
// For example, if the SID is S-1-5-21-a-b-c-123, it will return [a,b,c].
// If there are not enough sub-authorities (less than 3), it returns an empty slice.
func (s *sid) Domain() []uint32 {
if len(s.subAuthority) < 3 {
return []uint32{}
}
return s.subAuthority[1 : len(s.subAuthority)-1]
}
func (s *sid) isGeneric() bool {
raw := s.rawString()
_, ok := wellKnownSids[raw]
return ok
}
func (s *sid) rawString() string {
authority := fmt.Sprintf("%d", s.identifierAuthority)
if s.identifierAuthority >= 1<<32 {
authority = fmt.Sprintf("0x%x", s.identifierAuthority)
}
sidStr := fmt.Sprintf("S-%d-%s", s.revision, authority)
for _, subAuthority := range s.subAuthority {
sidStr += fmt.Sprintf("-%d", subAuthority)
}
return sidStr
}
// String returns a string representation of the SID. If the SID corresponds to a well-known
// SID, the short well-known SID name will be returned instead of the full SID string.
//
// The returned string will be in the format
// "S-----...-".
// If the SID is well-known, the string will be in the format "".
func (s *sid) String() string {
s.Validate()
sidStr := s.rawString()
if wk, ok := wellKnownSids[sidStr]; ok {
return wk
}
// Well-known RIDs (after experimenting, only these two were converted to a short form)
// perhaps because they belong to concrete users, while the rest represent groups
if strings.HasPrefix(sidStr, "S-1-5-21-") && len(s.subAuthority) > 4 {
switch s.subAuthority[len(s.subAuthority)-1] {
case 500:
return "LA"
case 501:
return "LG"
}
}
return sidStr
}
func (s *sid) Validate() {
// Check authority value fits in 48 bits
if s.identifierAuthority >= 1<<48 {
panic(fmt.Errorf("%w: value %d exceeds maximum of 2^48-1", ErrInvalidAuthority, s.identifierAuthority))
}
// Check number of sub-authorities (maximum is 15 in Windows)
if len(s.subAuthority) > 15 {
panic(fmt.Errorf("%w: got %d, maximum is 15", ErrTooManySubAuthorities, len(s.subAuthority)))
}
if s.revision != 1 {
panic(fmt.Errorf("%w: revision must be 1, was %d", ErrInvalidSIDFormat, s.revision))
}
}
// decomposeAccessMask breaks down an access mask into its individual components
// it also returns the mask without the components
func decomposeAccessMask(mask uint32) ([]string, uint32) {
var components []string
// Check components in order (least significant bits first)
maskValues := make([]uint32, 0, len(reversedAccessMaskComponents))
for val := range reversedAccessMaskComponents {
maskValues = append(maskValues, val)
}
slices.Sort(maskValues)
for _, val := range maskValues {
name := reversedAccessMaskComponents[val]
if mask&val == val {
components = append(components, name)
mask ^= val
}
}
return components, mask
}
// composeAccessMask combines individual permission components into an access mask
// it also return the components that were unable to be combined
func composeAccessMask(components []string) (uint32, []string) {
var remaining []string
var mask uint32
for _, code := range components {
if val, ok := accessMaskComponents[code]; ok {
mask |= val
} else {
remaining = append(remaining, code)
}
}
return mask, remaining
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/sddl_test.go 0000664 0000000 0000000 00000061117 15150254773 0023776 0 ustar 00root root 0000000 0000000 package sddl
import (
"bytes"
"fmt"
"strings"
"testing"
)
func TestACE_Binary(t *testing.T) {
tests := []struct {
name string
ace *ace
want []byte
}{
{
name: "valid basic ACE (SYSTEM - Full Access)",
ace: &ace{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
want: []byte{
// ACE Header
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags (none)
0x14, 0x00, // Size (20 bytes)
// Access Mask
0xFF, 0x01, 0x1F, 0x00, // 0x1F01FF (Full Access)
// SID (SYSTEM)
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
},
},
{
name: "valid audit ACE with flags",
ace: &ace{
header: &aceHeader{
aceType: systemAuditACEType,
aceFlags: successfulAccessACE | failedAccessACE,
aceSize: 20,
},
accessMask: 0x120089, // File Read
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
},
want: []byte{
// ACE Header
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0xC0, // Flags (SUCCESSFUL_ACCESS_ACE | FAILED_ACCESS_ACE)
0x14, 0x00, // Size (20 bytes)
// Access Mask
0x89, 0x00, 0x12, 0x00, // 0x120089 (File Read)
// SID (SYSTEM)
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
},
},
{
name: "valid ACE with inheritance flags",
ace: &ace{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: containerInheritACE | objectInheritACE,
aceSize: 24,
},
accessMask: 0x1F01FF,
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{32, 544}, // BUILTIN\Administrators
},
},
want: []byte{
// ACE Header
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x03, // Flags (CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE)
0x18, 0x00, // Size (24 bytes)
// Access Mask
0xFF, 0x01, 0x1F, 0x00, // 0x1F01FF (Full Access)
// SID (BUILTIN\Administrators)
0x01, // Revision
0x02, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority
0x20, 0x00, 0x00, 0x00, // SubAuthority[0] (32)
0x20, 0x02, 0x00, 0x00, // SubAuthority[1] (544)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.ace.Binary()
if !bytes.Equal(got, tt.want) {
t.Errorf("ACE.Binary() = %v, want %v", got, tt.want)
// Print detailed comparison for debugging
if len(got) != len(tt.want) {
t.Errorf("Length mismatch: got %d bytes, want %d bytes", len(got), len(tt.want))
} else {
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("Mismatch at byte %d: got 0x%02X, want 0x%02X", i, got[i], tt.want[i])
}
}
}
}
// Check reversibility for both binary and string
back, err := parseACEBinary(got)
if err != nil {
t.Errorf("Binary() -> parseACEBinary() error parsing back binary representation: %v", err)
return
}
compareACEs(t, "Binary() -> parseACEBinary()", back, tt.ace)
str := tt.ace.String()
backR, err := parseACEString(str)
if err != nil {
t.Errorf("Binary() -> ACE.String() -> parseACEString() error parsing back string representation: %v", err)
return
}
back, err = backR.toACE(tt.ace.sids())
if err != nil {
t.Errorf("Binary() -> ACE.String() -> parseACEString() -> toACE() error: %v", err)
return
}
compareACEs(t, "Binary() -> ACE.String() -> parseACEString()", back, tt.ace)
})
}
}
func TestACL_Binary(t *testing.T) {
t.Parallel()
// formatBytes is a helper function to format byte slices for better error messages
var formatBytes = func(b []byte) string {
if b == nil {
return "nil"
}
var builder strings.Builder
for i, by := range b {
if i > 0 {
if i%16 == 0 {
builder.WriteString("\n")
} else {
builder.WriteString(" ")
}
}
builder.WriteString(fmt.Sprintf("%02x", by))
}
return builder.String()
}
tests := []struct {
name string
acl *acl
want []byte
}{
{
name: "Empty ACL",
acl: &acl{
aclRevision: 2,
sbzl: 0,
aclSize: 8, // Just header size
aceCount: 0,
sbz2: 0,
aclType: "D",
control: seDACLPresent,
},
want: []byte{
0x02, // Revision
0x00, // Sbz1
0x08, 0x00, // Size (8 bytes)
0x00, 0x00, // AceCount (0)
0x00, 0x00, // Sbz2
},
},
{
name: "ACL with single ACE - Allow System Full Access",
acl: &acl{
aclRevision: 2,
sbzl: 0,
aclSize: 28, // 8 (header) + 20 (ACE)
aceCount: 1,
sbz2: 0,
aclType: "D",
control: seDACLPresent,
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF, // Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5, // NT Authority
subAuthority: []uint32{18}, // Local System
},
},
},
},
want: []byte{
// ACL Header
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes)
0x01, 0x00, // AceCount (1)
0x00, 0x00, // Sbz2
// ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
// SID (S-1-5-18, SYSTEM)
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority (NT)
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
},
},
{
name: "ACL with multiple ACEs",
acl: &acl{
aclRevision: 2,
sbzl: 0,
aclSize: 48, // 8 (header) + 20 (first ACE) + 20 (second ACE)
aceCount: 2,
sbz2: 0,
aclType: "D",
control: seDACLPresent,
aces: []ace{
{
header: &aceHeader{
aceType: accessAllowedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x1F01FF, // Full Access
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18}, // System
},
},
{
header: &aceHeader{
aceType: accessDeniedACEType,
aceFlags: 0,
aceSize: 20,
},
accessMask: 0x120089, // Read Access
sid: &sid{
revision: 1,
identifierAuthority: 1,
subAuthority: []uint32{0}, // Everyone
},
},
},
},
want: []byte{
// ACL Header
0x02, // Revision
0x00, // Sbz1
0x30, 0x00, // Size (48 bytes)
0x02, 0x00, // AceCount (2)
0x00, 0x00, // Sbz2
// First ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // IdentifierAuthority (NT)
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
// Second ACE
0x01, // Type (ACCESS_DENIED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size (20 bytes)
0x89, 0x00, 0x12, 0x00, // Access mask (Read Access)
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // IdentifierAuthority (World)
0x00, 0x00, 0x00, 0x00, // SubAuthority (0)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.acl.Binary()
if !bytes.Equal(got, tt.want) {
t.Errorf("ACL.Binary() =\n%v\nwant\n%v", formatBytes(got), formatBytes(tt.want))
}
// Check reversibility for both binary and string
back, err := parseACLBinary(got, tt.acl.aclType, tt.acl.control)
if err != nil {
t.Errorf("ACL.Binary() -> parseACLBinary() got error: %v", err)
return
}
compareACLs(t, "ACL.Binary() -> parseACLBinary()", back, tt.acl)
str := tt.acl.String()
backR, err := parseACLString(tt.acl.aclType, str)
if err != nil {
t.Errorf("ACL.Binary() -> ACL.String() -> parseACLString() got error: %v", err)
return
}
back, err = backR.toACL(tt.acl.sids())
if err != nil {
t.Errorf("ACL.Binary() -> ACL.String() -> parseACLString() -> toACL() got error: %v", err)
return
}
compareACLs(t, "ACL.Binary() -> ACL.String() -> parseACLString()", back, tt.acl)
})
}
}
func TestSecurityDescriptor_Binary(t *testing.T) {
t.Parallel()
// Helper function to create a basic SID
createSID := func(authority uint64, subAuth ...uint32) *sid {
return &sid{
revision: 1,
identifierAuthority: authority,
subAuthority: subAuth,
}
}
// Helper function to create a basic ACE
createACE := func(aceType byte, aceFlags byte, accessMask uint32, sid *sid) *ace {
size := uint16(8 + 12) // 8 bytes for header+mask + minimum 12 bytes for SID
if sid != nil {
size = uint16(8 + 8 + 4*len(sid.subAuthority))
}
return &ace{
header: &aceHeader{
aceType: aceType,
aceFlags: aceFlags,
aceSize: size,
},
accessMask: accessMask,
sid: sid,
}
}
// Helper function to create a basic ACL
createACL := func(aclType string, control uint16, aces ...ace) *acl {
size := uint16(8) // ACL header size
for _, ace := range aces {
size += ace.header.aceSize
}
return &acl{
aclRevision: 2,
sbzl: 0,
aclSize: size,
aceCount: uint16(len(aces)),
sbz2: 0,
aclType: aclType,
control: control,
aces: aces,
}
}
tests := []struct {
name string
sd *SecurityDescriptor
want []byte
}{
{
name: "Empty self-relative security descriptor",
sd: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seDACLDefaulted | seSACLDefaulted,
},
want: []byte{
0x01, // Revision
0x00, // Sbz1
0x2b, 0x80, // Control (SE_SELF_RELATIVE | SE_OWNER_DEFAULTED | SE_GROUP_DEFAULTED | SE_DACL_DEFAULTED | SE_SACL_DEFAULTED)
0x00, 0x00, 0x00, 0x00, // Owner offset
0x00, 0x00, 0x00, 0x00, // Group offset
0x00, 0x00, 0x00, 0x00, // Sacl offset
0x00, 0x00, 0x00, 0x00, // Dacl offset
},
},
{
name: "Security descriptor with owner only (SYSTEM)",
sd: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seGroupDefaulted | seDACLDefaulted | seSACLDefaulted,
ownerSID: createSID(5, 18), // SYSTEM
},
want: []byte{
// Header
0x01, // Revision
0x00, // Sbz1
0x2a, 0x80, // Control (SE_SELF_RELATIVE | SE_GROUP_DEFAULTED | SE_DACL_DEFAULTED | SE_SACL_DEFAULTED)
0x14, 0x00, 0x00, 0x00, // Owner offset (20)
0x00, 0x00, 0x00, 0x00, // Group offset
0x00, 0x00, 0x00, 0x00, // Sacl offset
0x00, 0x00, 0x00, 0x00, // Dacl offset
// Owner SID (SYSTEM)
0x01, 0x01, // Revision, SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (5)
0x12, 0x00, 0x00, 0x00, // SubAuthority (18)
},
},
{
name: "Security descriptor with owner and group",
sd: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seDACLDefaulted | seSACLDefaulted,
ownerSID: createSID(5, 18), // SYSTEM
groupSID: createSID(1, 0), // Everyone
},
want: []byte{
// Header
0x01, // Revision
0x00, // Sbz1
0x28, 0x80, // Control (SE_SELF_RELATIVE | SE_DACL_DEFAULTED | SE_SACL_DEFAULTED)
0x14, 0x00, 0x00, 0x00, // Owner offset (20)
0x20, 0x00, 0x00, 0x00, // Group offset (32)
0x00, 0x00, 0x00, 0x00, // Sacl offset
0x00, 0x00, 0x00, 0x00, // Dacl offset
// Owner SID (SYSTEM)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
// Group SID (Everyone)
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00,
},
},
{
name: "Security descriptor with DACL",
sd: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seOwnerDefaulted | seGroupDefaulted | seDACLPresent | seSACLDefaulted,
dacl: createACL("D", seSelfRelative|seOwnerDefaulted|seGroupDefaulted|seDACLPresent|seSACLDefaulted, // Same as SD.Control since this field is a copy
*createACE(accessAllowedACEType, 0, 0x1F01FF, createSID(5, 18))), // Full access for SYSTEM
},
want: []byte{
// Header
0x01, // Revision
0x00, // Sbz1
0x27, 0x80, // Control (SE_SELF_RELATIVE | SE_OWNER_DEFAULTED | SE_GROUP_DEFAULTED | SE_DACL_PRESENT | SE_SACL_DEFAULTED)
0x00, 0x00, 0x00, 0x00, // Owner offset
0x00, 0x00, 0x00, 0x00, // Group offset
0x00, 0x00, 0x00, 0x00, // Sacl offset
0x14, 0x00, 0x00, 0x00, // Dacl offset (20)
// DACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes = 8 header + 20 ACE)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01, // SID: Rev=1, Count=1
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority=5
0x12, 0x00, 0x00, 0x00, // SubAuth=18 (SYSTEM)
},
},
{
name: "Security descriptor with SACL",
sd: &SecurityDescriptor{
revision: 1,
control: seOwnerDefaulted | seGroupDefaulted | seDACLDefaulted | seSelfRelative | seSACLPresent,
sacl: createACL("S", seOwnerDefaulted|seGroupDefaulted|seDACLDefaulted|seSelfRelative|seSACLPresent, // Same as SD.Control since this field is a copy
*createACE(systemAuditACEType, successfulAccessACE, 0x1F01FF, createSID(5, 18))), // Audit SYSTEM access
},
want: []byte{
// Header
0x01, // Revision
0x00, // Sbz1
0x1b, 0x80, // Control (SE_OWNER_DEFAULTED | SE_GROUP_DEFAULTED | SE_DACL_DEFAULTED | SE_SELF_RELATIVE | SE_SACL_PRESENT)
0x00, 0x00, 0x00, 0x00, // Owner offset
0x00, 0x00, 0x00, 0x00, // Group offset
0x14, 0x00, 0x00, 0x00, // Sacl offset (20)
0x00, 0x00, 0x00, 0x00, // Dacl offset
// SACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes = 8 header + 20 ACE)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// ACE
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x40, // Flags (SUCCESSFUL_ACCESS_ACE)
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask (Full Access)
0x01, 0x01, // SID: Rev=1, Count=1
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority=5
0x12, 0x00, 0x00, 0x00, // SubAuth=18 (SYSTEM)
},
},
{
name: "Complete security descriptor",
sd: &SecurityDescriptor{
revision: 1,
control: seSelfRelative | seDACLPresent | seSACLPresent,
ownerSID: createSID(5, 18), // SYSTEM
groupSID: createSID(1, 0), // Everyone
sacl: createACL("S", seSelfRelative|seDACLPresent|seSACLPresent, // Same as SD.Control since this field is a copy
*createACE(systemAuditACEType, successfulAccessACE, 0x1F01FF, createSID(5, 18))),
dacl: createACL("D", seSelfRelative|seDACLPresent|seSACLPresent, // Same as SD.Control since this field is a copy
*createACE(accessAllowedACEType, 0, 0x1F01FF, createSID(5, 18))),
},
want: []byte{
// Header
0x01, // Revision
0x00, // Sbz1
0x14, 0x80, // Control (SE_SELF_RELATIVE | SE_DACL_PRESENT | SE_SACL_PRESENT)
0x14, 0x00, 0x00, 0x00, // Owner offset (20)
0x20, 0x00, 0x00, 0x00, // Group offset (32)
0x2C, 0x00, 0x00, 0x00, // Sacl offset (44)
0x48, 0x00, 0x00, 0x00, // Dacl offset (72)
// Owner SID (SYSTEM)
0x01, 0x01, // Rev=1, Count=1
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority=5
0x12, 0x00, 0x00, 0x00, // SubAuth=18
// Group SID (Everyone)
0x01, 0x01, // Rev=1, Count=1
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // Authority=1
0x00, 0x00, 0x00, 0x00, // SubAuth=0
// SACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// SACL ACE
0x02, // Type (SYSTEM_AUDIT_ACE_TYPE)
0x40, // Flags (SUCCESSFUL_ACCESS_ACE)
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
// DACL
0x02, // Revision
0x00, // Sbz1
0x1C, 0x00, // Size (28 bytes)
0x01, 0x00, // AceCount
0x00, 0x00, // Sbz2
// DACL ACE
0x00, // Type (ACCESS_ALLOWED_ACE_TYPE)
0x00, // Flags
0x14, 0x00, // Size (20 bytes)
0xFF, 0x01, 0x1F, 0x00, // Access mask
0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x12, 0x00, 0x00, 0x00,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.sd.Binary()
if len(got) != len(tt.want) {
t.Errorf("Binary() length mismatch\ngot = %d bytes\nwant = %d bytes", len(got), len(tt.want))
return
}
// Find first difference in binary output
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("Binary() mismatch at offset %d (0x%02x):\ngot = %02x\nwant = %02x",
i, i, got[i], tt.want[i])
// Print context around the mismatch
start := i - 4
if start < 0 {
start = 0
}
end := i + 4
if end > len(got) {
end = len(got)
}
t.Errorf("Context around mismatch (offset 0x%02x):", i)
t.Errorf("got = % 02x", got[start:end])
t.Errorf("want = % 02x", tt.want[start:end])
return
}
}
// If we get here, the lengths match and all bytes match
// Check reversibility for both binary and string
back, err := FromBinary(got)
if err != nil {
t.Errorf("Binary() -> ParseSecurityDescriptorBinary() unexpected error = %v", err)
return
}
compareSecurityDescriptors(t, back, tt.sd)
str := tt.sd.String()
sd, err := FromString(str)
if err != nil {
t.Errorf("String() -> ParseSecurityDescriptorString() unexpected error = %v", err)
return
}
compareSecurityDescriptors(t, sd, tt.sd)
})
}
}
func TestSID_Binary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
sid *sid
want []byte
wantErr error
}{
{
name: "NULL SID (S-1-0-0)",
sid: &sid{
revision: 1,
identifierAuthority: 0,
subAuthority: []uint32{0},
},
want: []byte{
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Authority (0 in big-endian)
0x00, 0x00, 0x00, 0x00, // SubAuthority[0] = 0 in little-endian
},
},
{
name: "Well-known SID - Local System (S-1-5-18)",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18},
},
want: []byte{
0x01, // Revision
0x01, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (5 in big-endian)
0x12, 0x00, 0x00, 0x00, // SubAuthority[0] = 18 in little-endian
},
},
{
name: "Well-known SID - BUILTIN\\Administrators (S-1-5-32-544)",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{32, 544},
},
want: []byte{
0x01, // Revision
0x02, // SubAuthorityCount
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority (5 in big-endian)
0x20, 0x00, 0x00, 0x00, // SubAuthority[0] = 32 in little-endian
0x20, 0x02, 0x00, 0x00, // SubAuthority[1] = 544 in little-endian
},
},
{
name: "Maximum valid authority value (2^48-1)",
sid: &sid{
revision: 1,
identifierAuthority: (1 << 48) - 1,
subAuthority: []uint32{1},
},
want: []byte{
0x01, // Revision
0x01, // SubAuthorityCount
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Authority (2^48-1 in big-endian)
0x01, 0x00, 0x00, 0x00, // SubAuthority[0] = 1 in little-endian
},
},
{
name: "Maximum number of sub-authorities (15)",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
},
},
want: []byte{
0x01, // Revision
0x0F, // SubAuthorityCount (15)
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority
// SubAuthorities in little-endian
0x01, 0x00, 0x00, 0x00, // 1
0x02, 0x00, 0x00, 0x00, // 2
0x03, 0x00, 0x00, 0x00, // 3
0x04, 0x00, 0x00, 0x00, // 4
0x05, 0x00, 0x00, 0x00, // 5
0x06, 0x00, 0x00, 0x00, // 6
0x07, 0x00, 0x00, 0x00, // 7
0x08, 0x00, 0x00, 0x00, // 8
0x09, 0x00, 0x00, 0x00, // 9
0x0A, 0x00, 0x00, 0x00, // 10
0x0B, 0x00, 0x00, 0x00, // 11
0x0C, 0x00, 0x00, 0x00, // 12
0x0D, 0x00, 0x00, 0x00, // 13
0x0E, 0x00, 0x00, 0x00, // 14
0x0F, 0x00, 0x00, 0x00, // 15
},
},
{
name: "Well known RID (LA)",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 2781442215, 2946190836, 3058968086, 500},
},
want: []byte{
0x01, // Revision
0x05, // SubAuthorityCount (5)
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // Authority
// SubAuthorities in little-endian
0x15, 0x00, 0x00, 0x00, // 21
0xA7, 0x70, 0xC9, 0xA5, // 2781442215
0xF4, 0x4D, 0x9B, 0xAF, // 2946190836
0x16, 0x26, 0x54, 0xB6, // 3058968086
0xF4, 0x01, 0x00, 0x00, // 500
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.sid.Binary()
// Check successful cases
if !bytes.Equal(got, tt.want) {
t.Errorf("Binary() = %v, want %v", got, tt.want)
// Detailed comparison for debugging
if len(got) != len(tt.want) {
t.Errorf("Binary() length = %d, want %d", len(got), len(tt.want))
} else {
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("Binary() byte[%d] = 0x%02X, want 0x%02X",
i, got[i], tt.want[i])
}
}
}
}
// Check reversibility for both binary and string
back, err := parseSIDBinary(got)
if err != nil {
t.Errorf("Binary() -> parseSIDBinary() error parsing back binary representation: %v", err)
return
}
compareSIDs(t, "Binary() -> parseSIDBinary()", back, tt.sid)
str := tt.sid.String()
backR, err := parseSIDString(str)
if err != nil {
t.Errorf("Binary() -> String() -> parseSIDString() error parsing back string representation: %v", err)
return
}
back, err = backR.toSID(tt.sid.sids())
if err != nil {
t.Errorf("Binary() -> String() -> parseSIDString() -> toSID() error: %v", err)
return
}
compareSIDs(t, "Binary() -> String() -> parseSIDString()", back, tt.sid)
})
}
}
func TestSID_Domain(t *testing.T) {
tests := []struct {
name string
sid *sid
want []uint32
}{
{
name: "valid domain SID",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 2781442215, 2946190836, 3058968086, 500},
},
want: []uint32{2781442215, 2946190836, 3058968086},
},
{
name: "too few sub-authorities",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{18, 500},
},
want: []uint32{},
},
{
name: "exactly three sub-authorities",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{21, 123, 500},
},
want: []uint32{123},
},
{
name: "empty sub-authorities",
sid: &sid{
revision: 1,
identifierAuthority: 5,
subAuthority: []uint32{},
},
want: []uint32{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.sid.Domain()
if len(got) != len(tt.want) {
t.Errorf("Domain() got len = %v, want len = %v", len(got), len(tt.want))
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("Domain()[%d] = %v, want %v", i, got[i], tt.want[i])
}
}
})
}
}
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/ 0000775 0000000 0000000 00000000000 15150254773 0023265 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/README.md 0000664 0000000 0000000 00000003046 15150254773 0024547 0 ustar 00root root 0000000 0000000 # About Examples
Except for `binary`, `dacl-and-sacl`, and `powershell` the other have the same structure as follows:
1. File `from-windows.raw.txt` contains security descriptor contents (obtained by calling `GetSecurityInfo`) from a windows file in three lines:
a) as produced by `ConvertSecurityDescriptorToStringSecurityDescriptorW`,
b) the output of a) and then call `ConvertStringSecurityDescriptorToSecurityDescriptorW`, base64 encoded
c) calling `MakeSelfRelativeSD` without string conversion, base64 encoded
Note the file is UTF-16LE encoded since it is the output of a windows program
2. File `from-windows.txt` contains the same information but UTF-8 encoded
3. File `parser-output.txt` contains the output of the parser (function `main.go:main()`) when reading lines 2-3 of `from-windows.txt` file
4. File `compare.txt` contains a) the first line of `frim-windows.txt`, b) the contents of `parser-output.txt`; the purpose is for comparision
In case of `binary`, it contains a single raw binary file that can be used to test, by calling `ParseSecurityDescriptor()` function.
In case of `dacl-and-sacl`, it contains two groups of files: 1) security descriptors as produced by windows API (both string and binary format), and 2) security descriptors as parsed by sddl under linux. **Note** that in the case of windows, the very original files are UTF-16LE encoded, so they were converted to UTF-8 LF in order to be used by sddl under linux
In case of `powershell`, it contains a single file with the output of the powershell script `scripts/sddl.ps1` golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/binary/ 0000775 0000000 0000000 00000000000 15150254773 0024551 5 ustar 00root root 0000000 0000000 share1_file-from-arash.txt_sd.bin 0000664 0000000 0000000 00000000404 15150254773 0032705 0 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/binary „ 0 L 6NV9…?øóÍ7¤T 6NV9…?øóÍ7¤ ¸ $ ÿ 6NV9…?øóÍ7¤R $ ÿ 6NV9…?øóÍ7¤S ÿ ÿ © ! $ ÿ 6NV9…?øóÍ7¤T golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl/ 0000775 0000000 0000000 00000000000 15150254773 0025650 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl/hello.txt.linux.b64 0000664 0000000 0000000 00000000571 15150254773 0031247 0 ustar 00root root 0000000 0000000 AQAUjBQAAAAwAAAATAAAAHgAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIALAABAAAAAkAkAKkAAgABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAgCgAAUAAAABACQAFgEAAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+oDAAAAACQAiQASAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+oDAAAAEBQA/wEfAAEBAAAAAAAFEgAAAAAQGAD/AR8AAQIAAAAAAAUgAAAAIAIAAAAQJAD/AR8AAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAA==
hello.txt.linux.sddl.utf8 0000664 0000000 0000000 00000000567 15150254773 0032415 0 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;FR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)S:AI(AU;SA;CCSWWPLORC;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
hello.txt.windows.bin.b64 0000664 0000000 0000000 00000000571 15150254773 0032272 0 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl AQAUjBQAAAAwAAAA7AAAAEwAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIAoAAFAAAAAQAkABYBAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAAAAkAIkAEgABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+kDAAACACwAAQAAAAJAJACpAAIAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAA==
hello.txt.windows.bin.b64.utf16le 0000664 0000000 0000000 00000001366 15150254773 0033562 0 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl ÿþA Q A U j B Q A A A A w A A A A 7 A A A A E w A A A A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f p A w A A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 A Q I A A A I A o A A F A A A A A Q A k A B Y B A A A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f q A w A A A A A k A I k A E g A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f q A w A A A B A U A P 8 B H w A B A Q A A A A A A B R I A A A A A E B g A / w E f A A E C A A A A A A A F I A A A A C A C A A A A E C Q A / w E f A A E F A A A A A A A F F Q A A A B b Y d X B i 3 S F J U 6 5 G 9 + k D A A A C A C w A A Q A A A A J A J A C p A A I A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 6 Q M A A A = =
hello.txt.windows.sddl.utf16le 0000664 0000000 0000000 00000001362 15150254773 0033342 0 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl ÿþO : S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 G : S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 5 1 3 D : A I ( D ; ; D C L C R P C R ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 2 ) ( A ; ; F R ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 2 ) ( A ; I D ; F A ; ; ; S Y ) ( A ; I D ; F A ; ; ; B A ) ( A ; I D ; F A ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 ) S : A I ( A U ; S A ; C C S W W P L O R C ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 )
hello.txt.windows.sddl.utf8 0000664 0000000 0000000 00000000567 15150254773 0032750 0 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;FR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)S:AI(AU;SA;CCSWWPLORC;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/many-perms/ 0000775 0000000 0000000 00000000000 15150254773 0025355 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/many-perms/compare.txt 0000664 0000000 0000000 00000001626 15150254773 0027551 0 ustar 00root root 0000000 0000000 O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;0x1200a9;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;RA;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;RA;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/many-perms/from-windows.raw.txt 0000664 0000000 0000000 00000003564 15150254773 0031351 0 ustar 00root root 0000000 0000000 ÿþS D D L : O : S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 G : S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 5 1 3 D : A I ( D ; ; D C L C R P C R ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 2 ) ( A ; ; 0 x 1 2 0 0 a 9 ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 2 ) ( A ; I D ; F A ; ; ; S Y ) ( A ; I D ; F A ; ; ; B A ) ( A ; I D ; F A ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 )
A Q A E h L Q A A A D Q A A A A A A A A A B Q A A A A C A K A A B Q A A A A E A J A A W A Q A A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 6 g M A A A A A J A C p A B I A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 6 g M A A A A Q F A D / A R 8 A A Q E A A A A A A A U S A A A A A B A Y A P 8 B H w A B A g A A A A A A B S A A A A A g A g A A A B A k A P 8 B H w A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f p A w A A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 6 Q M A A A E F A A A A A A A F F Q A A A B b Y d X B i 3 S F J U 6 5 G 9 w E C A A A =
A Q A E h B Q A A A A w A A A A A A A A A E w A A A A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f p A w A A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 A Q I A A A I A o A A F A A A A A Q A k A B Y B A A A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f q A w A A A A A k A K k A E g A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f q A w A A A B A U A P 8 B H w A B A Q A A A A A A B R I A A A A A E B g A / w E f A A E C A A A A A A A F I A A A A C A C A A A A E C Q A / w E f A A E F A A A A A A A F F Q A A A B b Y d X B i 3 S F J U 6 5 G 9 + k D A A A =
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/many-perms/from-windows.txt 0000664 0000000 0000000 00000001660 15150254773 0030554 0 ustar 00root root 0000000 0000000 O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;0x1200a9;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
AQAEhLQAAADQAAAAAAAAABQAAAACAKAABQAAAAEAJAAWAQAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36gMAAAAAJACpABIAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36gMAAAAQFAD/AR8AAQEAAAAAAAUSAAAAABAYAP8BHwABAgAAAAAABSAAAAAgAgAAABAkAP8BHwABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9wECAAA=
AQAEhBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIAoAAFAAAAAQAkABYBAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAAAAkAKkAEgABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+kDAAA=
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/many-perms/parser-output.txt 0000664 0000000 0000000 00000001140 15150254773 0030744 0 ustar 00root root 0000000 0000000 O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;RA;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:AI(D;;DCLCRPCR;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;;RA;;;S-1-5-21-1886771222-1226956130-4148604499-1002)(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/powershell/ 0000775 0000000 0000000 00000000000 15150254773 0025451 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/powershell/foo-binary-string.txt 0000664 0000000 0000000 00000000644 15150254773 0031567 0 ustar 00root root 0000000 0000000 SDDL string:
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:PAI(A;OICI;FA;;;LA)(A;OICI;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
Base64 binary form:
AQAElBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIAUAACAAAAAAMkAP8BHwABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvf0AQAAAAMkAP8BHwABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAA
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/single-perm/ 0000775 0000000 0000000 00000000000 15150254773 0025507 5 ustar 00root root 0000000 0000000 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/single-perm/compare.txt 0000664 0000000 0000000 00000001050 15150254773 0027672 0 ustar 00root root 0000000 0000000 O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/single-perm/from-windos.raw.txt 0000664 0000000 0000000 00000002370 15150254773 0031306 0 ustar 00root root 0000000 0000000 ÿþS D D L : O : S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 G : S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 5 1 3 D : ( A ; I D ; F A ; ; ; S Y ) ( A ; I D ; F A ; ; ; B A ) ( A ; I D ; F A ; ; ; S - 1 - 5 - 2 1 - 1 8 8 6 7 7 1 2 2 2 - 1 2 2 6 9 5 6 1 3 0 - 4 1 4 8 6 0 4 4 9 9 - 1 0 0 1 )
A Q A E g G w A A A C I A A A A A A A A A B Q A A A A C A F g A A w A A A A A Q F A D / A R 8 A A Q E A A A A A A A U S A A A A A B A Y A P 8 B H w A B A g A A A A A A B S A A A A A g A g A A A B A k A P 8 B H w A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f p A w A A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 6 Q M A A A E F A A A A A A A F F Q A A A B b Y d X B i 3 S F J U 6 5 G 9 w E C A A A =
A Q A E o B Q A A A A w A A A A A A A A A E w A A A A B B Q A A A A A A B R U A A A A W 2 H V w Y t 0 h S V O u R v f p A w A A A Q U A A A A A A A U V A A A A F t h 1 c G L d I U l T r k b 3 A Q I A A A I A W A A D A A A A A B A U A P 8 B H w A B A Q A A A A A A B R I A A A A A E B g A / w E f A A E C A A A A A A A F I A A A A C A C A A A A E C Q A / w E f A A E F A A A A A A A F F Q A A A B b Y d X B i 3 S F J U 6 5 G 9 + k D A A A =
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/single-perm/from-windos.txt 0000664 0000000 0000000 00000001162 15150254773 0030514 0 ustar 00root root 0000000 0000000 O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
AQAEgGwAAACIAAAAAAAAABQAAAACAFgAAwAAAAAQFAD/AR8AAQEAAAAAAAUSAAAAABAYAP8BHwABAgAAAAAABSAAAAAgAgAAABAkAP8BHwABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9wECAAA=
AQAEoBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIAWAADAAAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+kDAAA=
golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/single-perm/parser-output.txt 0000664 0000000 0000000 00000000560 15150254773 0031103 0 ustar 00root root 0000000 0000000 O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)
O:S-1-5-21-1886771222-1226956130-4148604499-1001G:S-1-5-21-1886771222-1226956130-4148604499-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-1886771222-1226956130-4148604499-1001)