Split the monolithic transaction into focused modules: append-only crash-recoverable journal under ~/.elydora/transactions with owner tokens and per-transaction namespaces, staged atomic publication (stage .pending, fsync, rename, fsync), platform-specific atomic replacement (MoveFileEx, renameat2, renamex_np), private-directory hardening per OS, lock-guarded recovery, and workspace cleanup that removes only transaction-created empty directories. Add the Kiro IDE 1.0 plugin: strict v1 hook schema with one enabled PreToolUse guard and PostToolUse audit pair bound to the active workspace, agent-to-workspace identity binding in runtime config, and exact migration of the legacy elydora-audit.kiro.hook contract. Gates: go test ./... and -race pass, go vet clean, govulncheck reports zero reachable vulnerabilities, git diff --check clean.
229 lines
6.8 KiB
Go
229 lines
6.8 KiB
Go
//go:build windows
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const transactionFileAllAccess windows.ACCESS_MASK = 0x1f01ff
|
|
|
|
const transactionParentMutationAccess windows.ACCESS_MASK = windows.FILE_WRITE_DATA |
|
|
windows.FILE_APPEND_DATA |
|
|
windows.DELETE |
|
|
windows.WRITE_DAC |
|
|
windows.WRITE_OWNER |
|
|
windows.GENERIC_WRITE |
|
|
windows.GENERIC_ALL |
|
|
0x00000040 // FILE_DELETE_CHILD
|
|
|
|
func createPrivateTransactionDirectory(path string) error {
|
|
descriptor, _, err := privateTransactionSecurityDescriptor()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pointer, err := windows.UTF16PtrFromString(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
attributes := windows.SecurityAttributes{
|
|
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
|
|
SecurityDescriptor: descriptor,
|
|
}
|
|
if err := windows.CreateDirectory(pointer, &attributes); err != nil {
|
|
return err
|
|
}
|
|
return verifyPrivateTransactionDirectory(path)
|
|
}
|
|
|
|
func ensurePrivateTransactionDirectory(path string) error {
|
|
info, err := os.Lstat(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
if createErr := createPrivateTransactionDirectory(path); createErr == nil {
|
|
return nil
|
|
} else if !errors.Is(createErr, os.ErrExist) &&
|
|
!errors.Is(createErr, windows.ERROR_ALREADY_EXISTS) {
|
|
return createErr
|
|
}
|
|
info, err = os.Lstat(path)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return fmt.Errorf("transaction namespace is not a physical directory: %s", path)
|
|
}
|
|
descriptor, user, err := privateTransactionSecurityDescriptor()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dacl, _, err := descriptor.DACL()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := windows.SetNamedSecurityInfo(
|
|
path,
|
|
windows.SE_FILE_OBJECT,
|
|
windows.OWNER_SECURITY_INFORMATION|
|
|
windows.DACL_SECURITY_INFORMATION|
|
|
windows.PROTECTED_DACL_SECURITY_INFORMATION,
|
|
user,
|
|
nil,
|
|
dacl,
|
|
nil,
|
|
); err != nil {
|
|
return fmt.Errorf("protect transaction namespace %s: %w", path, err)
|
|
}
|
|
return verifyPrivateTransactionDirectory(path)
|
|
}
|
|
|
|
func privateTransactionSecurityDescriptor() (*windows.SECURITY_DESCRIPTOR, *windows.SID, error) {
|
|
user, err := windows.GetCurrentProcessToken().GetTokenUser()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sid, err := user.User.Sid.Copy()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sidText := sid.String()
|
|
if sidText == "" {
|
|
return nil, nil, fmt.Errorf("resolve current Windows user SID")
|
|
}
|
|
descriptor, err := windows.SecurityDescriptorFromString(
|
|
"O:" + sidText + "D:P(A;OICI;FA;;;" + sidText + ")",
|
|
)
|
|
return descriptor, sid, err
|
|
}
|
|
|
|
func verifyPrivateTransactionDirectory(path string) error {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return fmt.Errorf("transaction namespace is not a physical directory: %s", path)
|
|
}
|
|
user, err := windows.GetCurrentProcessToken().GetTokenUser()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
descriptor, err := windows.GetNamedSecurityInfo(
|
|
path,
|
|
windows.SE_FILE_OBJECT,
|
|
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect transaction namespace DACL at %s: %w", path, err)
|
|
}
|
|
owner, _, err := descriptor.Owner()
|
|
if err != nil || owner == nil || !owner.Equals(user.User.Sid) {
|
|
return errors.Join(fmt.Errorf("transaction namespace owner changed: %s", path), err)
|
|
}
|
|
control, _, err := descriptor.Control()
|
|
if err != nil || control&windows.SE_DACL_PROTECTED == 0 {
|
|
return errors.Join(fmt.Errorf("transaction namespace DACL inherits access: %s", path), err)
|
|
}
|
|
dacl, _, err := descriptor.DACL()
|
|
if err != nil || dacl == nil || dacl.AceCount != 1 {
|
|
return errors.Join(fmt.Errorf("transaction namespace DACL is not owner-only: %s", path), err)
|
|
}
|
|
var ace *windows.ACCESS_ALLOWED_ACE
|
|
if err := windows.GetAce(dacl, 0, &ace); err != nil || ace == nil {
|
|
return errors.Join(fmt.Errorf("inspect transaction namespace owner ACE: %s", path), err)
|
|
}
|
|
aceSID := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
|
if ace.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE ||
|
|
ace.Header.AceFlags != windows.OBJECT_INHERIT_ACE|windows.CONTAINER_INHERIT_ACE ||
|
|
ace.Mask != transactionFileAllAccess || !aceSID.Equals(user.User.Sid) {
|
|
return fmt.Errorf("transaction namespace DACL is not owner-only: %s", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyTransactionNamespaceParent(path string) error {
|
|
info, err := os.Lstat(path)
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return errors.Join(fmt.Errorf("transaction namespace parent is not a physical directory: %s", path), err)
|
|
}
|
|
user, err := windows.GetCurrentProcessToken().GetTokenUser()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
descriptor, err := windows.GetNamedSecurityInfo(
|
|
path,
|
|
windows.SE_FILE_OBJECT,
|
|
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect transaction namespace parent owner at %s: %w", path, err)
|
|
}
|
|
owner, _, err := descriptor.Owner()
|
|
if err != nil || owner == nil || !owner.Equals(user.User.Sid) {
|
|
return errors.Join(
|
|
fmt.Errorf("transaction namespace parent owner does not match current user: %s", path),
|
|
err,
|
|
)
|
|
}
|
|
if err := verifyTransactionNamespaceParentDACL(path, descriptor, user.User.Sid); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyTransactionNamespaceParentDACL(
|
|
path string,
|
|
descriptor *windows.SECURITY_DESCRIPTOR,
|
|
currentUser *windows.SID,
|
|
) error {
|
|
dacl, _, err := descriptor.DACL()
|
|
if err != nil || dacl == nil {
|
|
return errors.Join(fmt.Errorf("transaction namespace parent has no restrictive DACL: %s", path), err)
|
|
}
|
|
system, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
administrators, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for index := uint32(0); index < uint32(dacl.AceCount); index++ {
|
|
var ace *windows.ACCESS_ALLOWED_ACE
|
|
if err := windows.GetAce(dacl, index, &ace); err != nil || ace == nil {
|
|
return errors.Join(fmt.Errorf("inspect transaction namespace parent ACE at %s", path), err)
|
|
}
|
|
if ace.Header.AceType == windows.ACCESS_DENIED_ACE_TYPE {
|
|
continue
|
|
}
|
|
if ace.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE {
|
|
return fmt.Errorf(
|
|
"transaction namespace parent has unsupported DACL ACE type %d: %s",
|
|
ace.Header.AceType,
|
|
path,
|
|
)
|
|
}
|
|
if ace.Header.AceFlags&windows.INHERIT_ONLY_ACE != 0 ||
|
|
ace.Mask&transactionParentMutationAccess == 0 {
|
|
continue
|
|
}
|
|
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
|
if !sid.IsValid() {
|
|
return fmt.Errorf("transaction namespace parent has invalid DACL SID: %s", path)
|
|
}
|
|
if !sid.Equals(currentUser) && !sid.Equals(system) && !sid.Equals(administrators) {
|
|
return fmt.Errorf(
|
|
"transaction namespace parent grants mutation access to untrusted SID %s: %s",
|
|
sid.String(),
|
|
path,
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|