all: implement windows permcheck

This commit is contained in:
Eugene Burkov 2024-11-27 19:19:11 +03:00
parent b75ed7d4d3
commit 09e4ae1ba1
9 changed files with 555 additions and 167 deletions

View File

@ -689,7 +689,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
} }
if !opts.noPermCheck { if !opts.noPermCheck {
checkPermissions(Context.workDir, confPath, dataDir, statsDir, querylogDir) checkPermissions(ctx, slogLogger, Context.workDir, confPath, dataDir, statsDir, querylogDir)
} }
Context.web.start() Context.web.start()
@ -751,12 +751,22 @@ func newUpdater(
// checkPermissions checks and migrates permissions of the files and directories // checkPermissions checks and migrates permissions of the files and directories
// used by AdGuard Home, if needed. // used by AdGuard Home, if needed.
func checkPermissions(workDir, confPath, dataDir, statsDir, querylogDir string) { func checkPermissions(
if permcheck.NeedsMigration(confPath) { ctx context.Context,
permcheck.Migrate(workDir, dataDir, statsDir, querylogDir, confPath) baseLogger *slog.Logger,
workDir string,
confPath string,
dataDir string,
statsDir string,
querylogDir string,
) {
l := baseLogger.With(slogutil.KeyPrefix, "permcheck")
if permcheck.NeedsMigration(ctx, l, workDir, confPath) {
permcheck.Migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
} }
permcheck.Check(workDir, dataDir, statsDir, querylogDir, confPath) permcheck.Check(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
} }
// initUsers initializes context auth module. Clears config users field. // initUsers initializes context auth module. Clears config users field.

View File

@ -0,0 +1,95 @@
//go:build unix
package permcheck
import (
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
// check is the Unix-specific implementation of [Check].
func check(
ctx context.Context,
l *slog.Logger,
workDir string,
dataDir string,
statsDir string,
querylogDir string,
confFilePath string,
) {
checkDir(ctx, l, workDir)
checkFile(ctx, l, confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication.
checkDir(ctx, l, dataDir)
checkDir(ctx, l, filepath.Join(dataDir, "filters"))
checkFile(ctx, l, filepath.Join(dataDir, "sessions.db"))
checkFile(ctx, l, filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir {
checkDir(ctx, l, querylogDir)
}
checkFile(ctx, l, filepath.Join(querylogDir, "querylog.json"))
checkFile(ctx, l, filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir {
checkDir(ctx, l, statsDir)
}
checkFile(ctx, l, filepath.Join(statsDir, "stats.db"))
}
// checkDir checks the permissions of a single directory. The results are
// logged at the appropriate level.
func checkDir(ctx context.Context, l *slog.Logger, dirPath string) {
checkPath(ctx, l, dirPath, typeDir, aghos.DefaultPermDir)
}
// checkFile checks the permissions of a single file. The results are logged at
// the appropriate level.
func checkFile(ctx context.Context, l *slog.Logger, filePath string) {
checkPath(ctx, l, filePath, typeFile, aghos.DefaultPermFile)
}
// checkPath checks the permissions of a single filesystem entity. The results
// are logged at the appropriate level.
func checkPath(ctx context.Context, l *slog.Logger, entPath, fileType string, want fs.FileMode) {
s, err := os.Stat(entPath)
if err != nil {
logFunc := l.ErrorContext
if errors.Is(err, os.ErrNotExist) {
logFunc = l.DebugContext
}
logFunc(
ctx,
"checking permissions",
"type", fileType,
"path", entPath,
slogutil.KeyError, err,
)
return
}
// TODO(a.garipov): Add a more fine-grained check and result reporting.
perm := s.Mode().Perm()
if perm != want {
l.WarnContext(
ctx,
"found unexpected permissions",
"type", fileType,
"path", entPath,
"got", fmt.Sprintf("%#o", perm),
"want", fmt.Sprintf("%#o", want),
)
}
}

View File

@ -0,0 +1,49 @@
//go:build windows
package permcheck
import (
"context"
"log/slog"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"golang.org/x/sys/windows"
)
// check is the Windows-specific implementation of [Check].
func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
dacl, owner, err := getSecurityInfo(workDir)
if err != nil {
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
return
}
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
l.WarnContext(ctx, "working directory owner is not in administrators group")
}
err = rangeACEs(dacl, func(hdr windows.ACE_HEADER, mask windows.ACCESS_MASK, sid *windows.SID) (cont bool) {
l.DebugContext(ctx, "checking entry", "sid", sid, "mask", mask)
warn := false
switch {
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
// Skip non-allowed ACEs.
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
// Non-administrator ACEs should not have any access rights.
warn = mask > 0
default:
// Administrators should full control access rights.
warn = mask&fullControlMask != fullControlMask
}
if warn {
l.WarnContext(ctx, "unexpected access control entry", "mask", mask, "sid", sid)
}
return true
})
if err != nil {
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
}
}

View File

@ -1,93 +0,0 @@
package permcheck
import (
"io/fs"
"os"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
// NeedsMigration returns true if AdGuard Home files need permission migration.
//
// TODO(a.garipov): Consider ways to detect this better.
func NeedsMigration(confFilePath string) (ok bool) {
s, err := os.Stat(confFilePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Likely a first run. Don't check.
return false
}
log.Error("permcheck: checking if files need migration: %s", err)
// Unexpected error. Try to migrate just in case.
return true
}
return s.Mode().Perm() != aghos.DefaultPermFile
}
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
// the results at an appropriate level.
func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
chmodDir(workDir)
chmodFile(confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication.
chmodDir(dataDir)
chmodDir(filepath.Join(dataDir, "filters"))
chmodFile(filepath.Join(dataDir, "sessions.db"))
chmodFile(filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir {
chmodDir(querylogDir)
}
chmodFile(filepath.Join(querylogDir, "querylog.json"))
chmodFile(filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir {
chmodDir(statsDir)
}
chmodFile(filepath.Join(statsDir, "stats.db"))
}
// chmodDir changes the permissions of a single directory. The results are
// logged at the appropriate level.
func chmodDir(dirPath string) {
chmodPath(dirPath, typeDir, aghos.DefaultPermDir)
}
// chmodFile changes the permissions of a single file. The results are logged
// at the appropriate level.
func chmodFile(filePath string) {
chmodPath(filePath, typeFile, aghos.DefaultPermFile)
}
// chmodPath changes the permissions of a single filesystem entity. The results
// are logged at the appropriate level.
func chmodPath(entPath, fileType string, fm fs.FileMode) {
err := os.Chmod(entPath, fm)
if err == nil {
log.Info("permcheck: changed permissions for %s %q", fileType, entPath)
return
} else if errors.Is(err, os.ErrNotExist) {
log.Debug("permcheck: changing permissions for %s %q: %s", fileType, entPath, err)
return
}
log.Error(
"permcheck: SECURITY WARNING: cannot change permissions for %s %q to %#o: %s; "+
"this can leave your system vulnerable, see "+
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
fileType,
entPath,
fm,
err,
)
}

View File

@ -0,0 +1,107 @@
//go:build unix
package permcheck
import (
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
// needsMigration is a Unix-specific implementation of [NeedsMigration].
//
// TODO(a.garipov): Consider ways to detect this better.
func needsMigration(ctx context.Context, l *slog.Logger, _, confFilePath string) (ok bool) {
s, err := os.Stat(confFilePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Likely a first run. Don't check.
return false
}
l.ErrorContext(ctx, "checking a need for permission migration", slogutil.KeyError, err)
// Unexpected error. Try to migrate just in case.
return true
}
return s.Mode().Perm() != aghos.DefaultPermFile
}
// migrate is a Unix-specific implementation of [Migrate].
func migrate(
ctx context.Context,
l *slog.Logger,
workDir string,
dataDir string,
statsDir string,
querylogDir string,
confFilePath string,
) {
chmodDir(ctx, l, workDir)
chmodFile(ctx, l, confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication.
chmodDir(ctx, l, dataDir)
chmodDir(ctx, l, filepath.Join(dataDir, "filters"))
chmodFile(ctx, l, filepath.Join(dataDir, "sessions.db"))
chmodFile(ctx, l, filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir {
chmodDir(ctx, l, querylogDir)
}
chmodFile(ctx, l, filepath.Join(querylogDir, "querylog.json"))
chmodFile(ctx, l, filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir {
chmodDir(ctx, l, statsDir)
}
chmodFile(ctx, l, filepath.Join(statsDir, "stats.db"))
}
// chmodDir changes the permissions of a single directory. The results are
// logged at the appropriate level.
func chmodDir(ctx context.Context, l *slog.Logger, dirPath string) {
chmodPath(ctx, l, dirPath, typeDir, aghos.DefaultPermDir)
}
// chmodFile changes the permissions of a single file. The results are logged
// at the appropriate level.
func chmodFile(ctx context.Context, l *slog.Logger, filePath string) {
chmodPath(ctx, l, filePath, typeFile, aghos.DefaultPermFile)
}
// chmodPath changes the permissions of a single filesystem entity. The results
// are logged at the appropriate level.
func chmodPath(ctx context.Context, l *slog.Logger, entPath, fileType string, fm fs.FileMode) {
switch err := os.Chmod(entPath, fm); {
case err == nil:
l.InfoContext(ctx, "changed permissions", "type", fileType, "path", entPath)
case errors.Is(err, os.ErrNotExist):
l.DebugContext(
ctx,
"changing permissions",
"type", fileType,
"path", entPath,
slogutil.KeyError, err,
)
default:
l.ErrorContext(
ctx,
"can not change permissions; this can leave your system vulnerable, see "+
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
"type", fileType,
"path", entPath,
"target_perm", fmt.Sprintf("%#o", fm),
slogutil.KeyError, err,
)
}
}

View File

@ -0,0 +1,107 @@
//go:build windows
package permcheck
import (
"context"
"log/slog"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"golang.org/x/sys/windows"
)
// needsMigration is the Windows-specific implementation of [NeedsMigration].
func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) {
dacl, owner, err := getSecurityInfo(workDir)
if err != nil {
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
return true
}
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
return true
}
err = rangeACEs(dacl, func(
hdr windows.ACE_HEADER,
mask windows.ACCESS_MASK,
sid *windows.SID,
) (cont bool) {
switch {
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
// Skip non-allowed access control entries.
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
// Non-administrator access control entries should not have any
// access rights.
ok = mask > 0
default:
// Administrators should have full control.
ok = mask&fullControlMask != fullControlMask
}
// Stop ranging if the access control entry is unexpected.
return !ok
})
if err != nil {
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
return true
}
return ok
}
// migrate is the Windows-specific implementation of [Migrate].
func migrate(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
dacl, owner, err := getSecurityInfo(workDir)
if err != nil {
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
return
}
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
var admins *windows.SID
admins, err = windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
l.ErrorContext(ctx, "creating administrators sid", slogutil.KeyError, err)
} else {
l.InfoContext(ctx, "migrating working directory owner", "sid", admins)
owner = admins
}
}
// TODO(e.burkov): Check for duplicates?
var accessEntries []windows.EXPLICIT_ACCESS
err = rangeACEs(dacl, func(
hdr windows.ACE_HEADER,
mask windows.ACCESS_MASK,
sid *windows.SID,
) (cont bool) {
switch {
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
// Add non-allowed access control entries as is.
l.InfoContext(ctx, "migrating deny control entry", "sid", sid)
accessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask))
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
// Skip non-administrator ACEs.
l.InfoContext(ctx, "removing access control entry", "sid", sid)
default:
l.InfoContext(ctx, "migrating access control entry", "sid", sid, "mask", mask)
accessEntries = append(accessEntries, newFullExplicitAccess(sid))
}
return true
})
if err != nil {
l.ErrorContext(ctx, "ranging trough access control entries", slogutil.KeyError, err)
return
}
err = setSecurityInfo(workDir, owner, accessEntries)
if err != nil {
l.ErrorContext(ctx, "setting security info", slogutil.KeyError, err)
}
}

View File

@ -1,20 +1,15 @@
// Package permcheck contains code for simplifying permissions checks on files // Package permcheck contains code for simplifying permissions checks on files
// and directories. // and directories.
//
// TODO(a.garipov): Improve the approach on Windows.
package permcheck package permcheck
import ( import (
"io/fs" "context"
"os" "log/slog"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
) )
// File type constants for logging. // File type constants for logging.
//
// TODO(e.burkov): !! Use in Windows logging.
const ( const (
typeDir = "directory" typeDir = "directory"
typeFile = "file" typeFile = "file"
@ -22,65 +17,33 @@ const (
// Check checks the permissions on important files. It logs the results at // Check checks the permissions on important files. It logs the results at
// appropriate levels. // appropriate levels.
func Check(workDir, dataDir, statsDir, querylogDir, confFilePath string) { func Check(
checkDir(workDir) ctx context.Context,
l *slog.Logger,
checkFile(confFilePath) workDir string,
dataDir string,
// TODO(a.garipov): Put all paths in one place and remove this duplication. statsDir string,
checkDir(dataDir) querylogDir string,
checkDir(filepath.Join(dataDir, "filters")) confFilePath string,
checkFile(filepath.Join(dataDir, "sessions.db")) ) {
checkFile(filepath.Join(dataDir, "leases.json")) check(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
if dataDir != querylogDir {
checkDir(querylogDir)
}
checkFile(filepath.Join(querylogDir, "querylog.json"))
checkFile(filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir {
checkDir(statsDir)
}
checkFile(filepath.Join(statsDir, "stats.db"))
} }
// checkDir checks the permissions of a single directory. The results are // NeedsMigration returns true if AdGuard Home files need permission migration.
// logged at the appropriate level. func NeedsMigration(ctx context.Context, l *slog.Logger, workDir, confFilePath string) (ok bool) {
func checkDir(dirPath string) { return needsMigration(ctx, l, workDir, confFilePath)
checkPath(dirPath, typeDir, aghos.DefaultPermDir)
} }
// checkFile checks the permissions of a single file. The results are logged at // Migrate attempts to change the permissions of AdGuard Home's files. It logs
// the appropriate level. // the results at an appropriate level.
func checkFile(filePath string) { func Migrate(
checkPath(filePath, typeFile, aghos.DefaultPermFile) ctx context.Context,
} l *slog.Logger,
workDir string,
// checkPath checks the permissions of a single filesystem entity. The results dataDir string,
// are logged at the appropriate level. statsDir string,
func checkPath(entPath, fileType string, want fs.FileMode) { querylogDir string,
s, err := os.Stat(entPath) confFilePath string,
if err != nil { ) {
logFunc := log.Error migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
if errors.Is(err, os.ErrNotExist) {
logFunc = log.Debug
}
logFunc("permcheck: checking %s %q: %s", fileType, entPath, err)
return
}
// TODO(a.garipov): Add a more fine-grained check and result reporting.
perm := s.Mode().Perm()
if perm != want {
log.Info(
"permcheck: SECURITY WARNING: %s %q has unexpected permissions %#o; want %#o",
fileType,
entPath,
perm,
want,
)
}
} }

View File

@ -0,0 +1,150 @@
//go:build windows
package permcheck
import (
"fmt"
"unsafe"
"github.com/AdguardTeam/golibs/errors"
"golang.org/x/sys/windows"
)
// securityInfo defines the parts of a security descriptor to retrieve and set.
const securityInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |
windows.DACL_SECURITY_INFORMATION |
windows.PROTECTED_DACL_SECURITY_INFORMATION |
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
const objectType = windows.SE_FILE_OBJECT
// fileDeleteChildRight is the mask bit for the right to delete a child object.
// It seems to be missing from the windows package.
//
// See https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask.
const fileDeleteChildRight = 0b1000000
// fullControlMask is the mask for full control access rights.
const fullControlMask windows.ACCESS_MASK = windows.FILE_LIST_DIRECTORY |
windows.FILE_WRITE_DATA |
windows.FILE_APPEND_DATA |
windows.FILE_READ_EA |
windows.FILE_WRITE_EA |
windows.FILE_TRAVERSE |
fileDeleteChildRight |
windows.FILE_READ_ATTRIBUTES |
windows.FILE_WRITE_ATTRIBUTES |
windows.DELETE |
windows.READ_CONTROL |
windows.WRITE_DAC |
windows.WRITE_OWNER |
windows.SYNCHRONIZE
// aceFunc is a function that handles access control entries in the
// discretionary access control list. It should return true to continue ranging
// or false to stop.
type aceFunc = func(
hdr windows.ACE_HEADER,
mask windows.ACCESS_MASK,
sid *windows.SID,
) (cont bool)
// rangeACEs ranges over the access control entries in the discretionary access
// control list of the specified security descriptor.
func rangeACEs(dacl *windows.ACL, f aceFunc) (err error) {
var errs []error
for i := range uint32(dacl.AceCount) {
var ace *windows.ACCESS_ALLOWED_ACE
err = windows.GetAce(dacl, i, &ace)
if err != nil {
errs = append(errs, fmt.Errorf("getting entry at index %d: %w", i, err))
continue
}
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
if !f(ace.Header, ace.Mask, sid) {
break
}
}
if err = errors.Join(errs...); err != nil {
return fmt.Errorf("checking access control entries: %w", err)
}
return nil
}
// setSecurityInfo sets the security information on the specified file, using
// ents to create a discretionary access control list.
func setSecurityInfo(fname string, owner *windows.SID, ents []windows.EXPLICIT_ACCESS) (err error) {
if len(ents) == 0 {
ents = []windows.EXPLICIT_ACCESS{
newFullExplicitAccess(owner),
}
}
acl, err := windows.ACLFromEntries(ents, nil)
if err != nil {
return fmt.Errorf("creating access control list: %w", err)
}
err = windows.SetNamedSecurityInfo(fname, objectType, securityInfo, owner, nil, acl, nil)
if err != nil {
return fmt.Errorf("setting security info: %w", err)
}
return nil
}
// getSecurityInfo retrieves the security information for the specified file.
func getSecurityInfo(fname string) (dacl *windows.ACL, owner *windows.SID, err error) {
sd, err := windows.GetNamedSecurityInfo(fname, objectType, securityInfo)
if err != nil {
return nil, nil, fmt.Errorf("getting security descriptor: %w", err)
}
owner, _, err = sd.Owner()
if err != nil {
return nil, nil, fmt.Errorf("getting owner sid: %w", err)
}
dacl, _, err = sd.DACL()
if err != nil {
return nil, nil, fmt.Errorf("getting discretionary access control list: %w", err)
}
return dacl, owner, nil
}
// newFullExplicitAccess creates a new explicit access entry with full control
// permissions.
func newFullExplicitAccess(sid *windows.SID) (expAcc windows.EXPLICIT_ACCESS) {
// TODO(e.burkov): !! lookup account type
return windows.EXPLICIT_ACCESS{
AccessPermissions: fullControlMask,
AccessMode: windows.GRANT_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(sid),
},
}
}
// newDenyExplicitAccess creates a new explicit access entry with specified deny
// permissions.
func newDenyExplicitAccess(sid *windows.SID, mask windows.ACCESS_MASK) (expAcc windows.EXPLICIT_ACCESS) {
// TODO(e.burkov): !! lookup account type
return windows.EXPLICIT_ACCESS{
AccessPermissions: mask,
AccessMode: windows.DENY_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(sid),
},
}
}

View File

@ -62,8 +62,8 @@ set -f -u
# NOTE: Flag -H for grep is non-POSIX but all of Busybox, GNU, macOS, and # NOTE: Flag -H for grep is non-POSIX but all of Busybox, GNU, macOS, and
# OpenBSD support it. # OpenBSD support it.
# #
# NOTE: Exclude the permission_windows.go, because it requires unsafe for the # NOTE: Exclude the security_windows.go, because it requires unsafe for the OS
# OS APIs. # APIs.
# #
# TODO(a.garipov): Add golibs/log. # TODO(a.garipov): Add golibs/log.
blocklist_imports() { blocklist_imports() {
@ -72,7 +72,7 @@ blocklist_imports() {
-name '*.go' \ -name '*.go' \
'!' '(' \ '!' '(' \
-name '*.pb.go' \ -name '*.pb.go' \
-o -path './internal/aghos/permission_windows.go' \ -o -path './internal/permcheck/security_windows.go' \
')' \ ')' \
-exec \ -exec \
'grep' \ 'grep' \