pax_global_header00006660000000000000000000000064151502547730014523gustar00rootroot0000000000000052 comment=9c9b93b769dbc93439fda51d9554597ec75e1872 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/000077500000000000000000000000001515025477300214545ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/.gitignore000066400000000000000000000011001515025477300234340ustar00rootroot00000000000000# 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/LICENSE000066400000000000000000000167441515025477300224750ustar00rootroot00000000000000 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.md000066400000000000000000000111361515025477300227350ustar00rootroot00000000000000# 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/000077500000000000000000000000001515025477300222175ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/sddl/000077500000000000000000000000001515025477300231455ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/cmd/sddl/main.go000066400000000000000000000064111515025477300244220ustar00rootroot00000000000000package 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.go000066400000000000000000000007201515025477300256360ustar00rootroot00000000000000//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.go000066400000000000000000000202221515025477300261700ustar00rootroot00000000000000//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.go000066400000000000000000000133241515025477300243150ustar00rootroot00000000000000package 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.go000066400000000000000000000612251515025477300253570ustar00rootroot00000000000000package 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.go000066400000000000000000000661031515025477300243420ustar00rootroot00000000000000package 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.go000066400000000000000000001102741515025477300254000ustar00rootroot00000000000000package 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.mod000066400000000000000000000001141515025477300225560ustar00rootroot00000000000000module github.com/cloudsoda/sddl go 1.22 require golang.org/x/sys v0.28.0 golang-github-cloudsoda-sddl-0.0~git20250224.926454e/go.sum000066400000000000000000000002311515025477300226030ustar00rootroot00000000000000golang.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/000077500000000000000000000000001515025477300231435ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/scripts/sddl.ps1000066400000000000000000000073351515025477300245260ustar00rootroot00000000000000# 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.go000066400000000000000000001033331515025477300227340ustar00rootroot00000000000000package 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.go000066400000000000000000000611171515025477300237760ustar00rootroot00000000000000package 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/000077500000000000000000000000001515025477300232655ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/README.md000066400000000000000000000030461515025477300245470ustar00rootroot00000000000000# 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/000077500000000000000000000000001515025477300245515ustar00rootroot00000000000000share1_file-from-arash.txt_sd.bin000066400000000000000000000004041515025477300327050ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/binary„0L6NV9…?øóÍ7¤T6NV9…?øóÍ7¤¸$ÿ6NV9…?øóÍ7¤R$ÿ6NV9…?øóÍ7¤Sÿÿ © !$ÿ6NV9…?øóÍ7¤Tgolang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl/000077500000000000000000000000001515025477300256505ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-sacl/hello.txt.linux.b64000066400000000000000000000005711515025477300312470ustar00rootroot00000000000000AQAUjBQAAAAwAAAATAAAAHgAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIALAABAAAAAkAkAKkAAgABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAgCgAAUAAAABACQAFgEAAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+oDAAAAACQAiQASAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+oDAAAAEBQA/wEfAAEBAAAAAAAFEgAAAAAQGAD/AR8AAQIAAAAAAAUgAAAAIAIAAAAQJAD/AR8AAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAA== hello.txt.linux.sddl.utf8000066400000000000000000000005671515025477300324150ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-saclO: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.b64000066400000000000000000000005711515025477300322720ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-saclAQAUjBQAAAAwAAAA7AAAAEwAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIAoAAFAAAAAQAkABYBAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAAAAkAIkAEgABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+kDAAACACwAAQAAAAJAJACpAAIAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAA== hello.txt.windows.bin.b64.utf16le000066400000000000000000000013661515025477300335620ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-saclÿþAQAUjBQAAAAwAAAA7AAAAEwAAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfpAwAAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb3AQIAAAIAoAAFAAAAAQAkABYBAAABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAAAAkAIkAEgABBQAAAAAABRUAAAAW2HVwYt0hSVOuRvfqAwAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAABbYdXBi3SFJU65G9+kDAAACACwAAQAAAAJAJACpAAIAAQUAAAAAAAUVAAAAFth1cGLdIUlTrkb36QMAAA== hello.txt.windows.sddl.utf16le000066400000000000000000000013621515025477300333420ustar00rootroot00000000000000golang-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.sddl.utf8000066400000000000000000000005671515025477300327500ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/dacl-and-saclO: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/000077500000000000000000000000001515025477300253555ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/many-perms/compare.txt000066400000000000000000000016261515025477300275510ustar00rootroot00000000000000O: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.txt000066400000000000000000000035641515025477300313510ustar00rootroot00000000000000ÿþSDDL: 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/from-windows.txt000066400000000000000000000016601515025477300305540ustar00rootroot00000000000000O: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.txt000066400000000000000000000011401515025477300307440ustar00rootroot00000000000000O: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/000077500000000000000000000000001515025477300254515ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/powershell/foo-binary-string.txt000066400000000000000000000006441515025477300315670ustar00rootroot00000000000000SDDL 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/000077500000000000000000000000001515025477300255075ustar00rootroot00000000000000golang-github-cloudsoda-sddl-0.0~git20250224.926454e/testdata/single-perm/compare.txt000066400000000000000000000010501515025477300276720ustar00rootroot00000000000000O: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.txt000066400000000000000000000023701515025477300313060ustar00rootroot00000000000000ÿþSDDL: 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/from-windos.txt000066400000000000000000000011621515025477300305140ustar00rootroot00000000000000O: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.txt000066400000000000000000000005601515025477300311030ustar00rootroot00000000000000O: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)