Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
772324247a | ||
![]() |
7b0156775c | ||
![]() |
c6efc2d83c |
7
TODO
7
TODO
@ -1,8 +1,9 @@
|
||||
- refactor the elevation detection stuff. I'm not terribly happy with it.
|
||||
|
||||
- password generator utility/library
|
||||
-- incorporate with r00t2.io/pwgen
|
||||
-- incorporate with https://github.com/tredoe/osutil ?
|
||||
-- cli flag to dump flat hashes too
|
||||
--- https://github.com/hlandau/passlib
|
||||
-- incoprporated separately; https://git.r00t2.io/r00t2/PWGen (import r00t2.io/pwgen)
|
||||
-- cli flag to dump flat hashes too (https://github.com/hlandau/passlib and others soon in pwgen)
|
||||
|
||||
- auger needs to be build-constrained to linux.
|
||||
|
||||
|
11
fsutils/errs.go
Normal file
11
fsutils/errs.go
Normal file
@ -0,0 +1,11 @@
|
||||
package fsutils
|
||||
|
||||
import (
|
||||
`syscall`
|
||||
)
|
||||
|
||||
var (
|
||||
// Yes, I know. "Why ENOTTY?" I don't know, ask Linus.
|
||||
// If you see "inappropriate ioctl for device", it's this'un.
|
||||
ErrFsAttrsUnsupported error = syscall.ENOTTY
|
||||
)
|
163
funcs_idstate.go
Normal file
163
funcs_idstate.go
Normal file
@ -0,0 +1,163 @@
|
||||
package sysutils
|
||||
|
||||
// Checked consolidates all the provided checked functions.
|
||||
func (i *IDState) Checked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.uidsChecked &&
|
||||
i.gidsChecked &&
|
||||
i.sudoChecked &&
|
||||
i.ppidUidChecked &&
|
||||
i.ppidGidChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsReal consolidates all the elevation/dropped-privs checks into a single method.
|
||||
|
||||
It will only return true if no sudo was detected and *all* UIDs/GIDs match.
|
||||
*/
|
||||
func (i *IDState) IsReal(real bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
real = true
|
||||
|
||||
for _, b := range []bool{
|
||||
i.IsSuid(),
|
||||
i.IsSgid(),
|
||||
i.IsSudoUser(),
|
||||
i.IsSudoGroup(),
|
||||
} {
|
||||
if b {
|
||||
real = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoGroup is true if any of the group sudo env vars are set,
|
||||
or the parent process has a different group (and is not PID 1).
|
||||
|
||||
It will always return false if SudoChecked returns false oor PPIDGIDsChecked returns false.
|
||||
*/
|
||||
func (i *IDState) IsSudoGroup() (sudo bool) {
|
||||
|
||||
if i == nil || !i.sudoChecked || !i.ppidGidChecked {
|
||||
return
|
||||
}
|
||||
|
||||
sudo = i.SudoEnvGroup || !i.PPIDGidMatch
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoUser is true if any of the user sudo env vars are set,
|
||||
or the parent process has a different owner (and is not PID 1).
|
||||
|
||||
It will always return false if SudoChecked returns false or PPIDUIDsChecked returns false.
|
||||
*/
|
||||
func (i *IDState) IsSudoUser() (sudo bool) {
|
||||
|
||||
if i == nil || !i.sudoChecked || !i.ppidUidChecked {
|
||||
return
|
||||
}
|
||||
|
||||
sudo = i.SudoEnvUser || !i.PPIDUidMatch
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsSuid is true if the RUID does not match EUID or SUID. It will always return false if UIDsChecked returns false.
|
||||
func (i *IDState) IsSuid() (suid bool) {
|
||||
|
||||
if i == nil || !i.uidsChecked {
|
||||
return
|
||||
}
|
||||
|
||||
suid = i.RUID != i.EUID || i.RUID != i.SUID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsSgid is true if the RGID does not match EGID or SGID. It will always return false if GIDsChecked returns false.
|
||||
func (i *IDState) IsSgid() (sgid bool) {
|
||||
|
||||
if i == nil || !i.gidsChecked {
|
||||
return
|
||||
}
|
||||
|
||||
sgid = i.RGID != i.EGID || i.RGID != i.SGID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GIDsChecked is true if the GIDs presented can be trusted.
|
||||
func (i *IDState) GIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.gidsChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PPIDGIDsChecked is true if PPIDGidMatch can be trusted.
|
||||
func (i *IDState) PPIDGIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.ppidGidChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PPIDUIDsChecked is true if PPIDUidMatch can be trusted.
|
||||
func (i *IDState) PPIDUIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.ppidUidChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SudoChecked is true if SudoEnvVars can be trusted
|
||||
func (i *IDState) SudoChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.sudoChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UIDsChecked is true if the UIDs presented can be trusted.
|
||||
func (i *IDState) UIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.uidsChecked
|
||||
|
||||
return
|
||||
}
|
50
funcs_linux.go
Normal file
50
funcs_linux.go
Normal file
@ -0,0 +1,50 @@
|
||||
package sysutils
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`os`
|
||||
|
||||
`golang.org/x/sys/unix`
|
||||
`r00t2.io/sysutils/envs`
|
||||
)
|
||||
|
||||
// GetIDState returns current ID/elevation information. An IDState should *not* be explicitly created/defined.
|
||||
func GetIDState() (ids IDState) {
|
||||
|
||||
var err error
|
||||
|
||||
ids.RUID, ids.EUID, ids.SUID = unix.Getresuid()
|
||||
ids.uidsChecked = true
|
||||
ids.RGID, ids.EGID, ids.SGID = unix.Getresgid()
|
||||
ids.gidsChecked = true
|
||||
|
||||
ids.SudoEnvCmd = envs.HasEnv("SUDO_COMMAND")
|
||||
ids.SudoEnvHome = envs.HasEnv("SUDO_HOME")
|
||||
ids.SudoEnvGroup = envs.HasEnv("SUDO_GID")
|
||||
ids.SudoEnvUser = envs.HasEnv("SUDO_UID") || envs.HasEnv("SUDO_USER")
|
||||
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
|
||||
ids.SudoEnvVars = true
|
||||
}
|
||||
ids.sudoChecked = true
|
||||
|
||||
// PID 1 will *always* be root, so that can return a false positive for sudo.
|
||||
if os.Getppid() != 1 {
|
||||
ids.stat = new(unix.Stat_t)
|
||||
if err = unix.Stat(
|
||||
fmt.Sprintf("/proc/%d/stat", os.Getppid()),
|
||||
ids.stat,
|
||||
); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
|
||||
ids.ppidUidChecked = true
|
||||
ids.PPIDGidMatch = ids.RGID == int(ids.stat.Gid)
|
||||
ids.ppidGidChecked = true
|
||||
}
|
||||
} else {
|
||||
ids.ppidUidChecked = true
|
||||
ids.ppidGidChecked = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
17
go.mod
17
go.mod
@ -6,8 +6,19 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
golang.org/x/sync v0.9.0
|
||||
golang.org/x/sys v0.26.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.5
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sys v0.33.0
|
||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
|
||||
r00t2.io/goutils v1.7.1
|
||||
r00t2.io/goutils v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
)
|
||||
|
40
go.sum
40
go.sum
@ -3,17 +3,45 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
|
||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
|
||||
r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc=
|
||||
r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
||||
r00t2.io/goutils v1.8.1 h1:TQcUycPKsYn0QI4uCqb56utmvu/vVSxlblBg98iXStg=
|
||||
r00t2.io/goutils v1.8.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
||||
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
|
||||
|
14
ispriv/consts_nix.go
Normal file
14
ispriv/consts_nix.go
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
const (
|
||||
sudoEnvPfx string = "SUDO_"
|
||||
sudoUidEnv string = sudoEnvPfx + "UID"
|
||||
sudoGidEnv string = sudoEnvPfx + "GID"
|
||||
sudoUnameEnv string = sudoEnvPfx + "USER"
|
||||
)
|
||||
|
||||
const (
|
||||
curLoginUidFile string = "/proc/self/loginuid"
|
||||
)
|
7
ispriv/doc_nix.go
Normal file
7
ispriv/doc_nix.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build unix
|
||||
|
||||
/*
|
||||
ispriv provides functions and a method to determine if a process is being run SUID/SGID, under sudo, etc.
|
||||
*/
|
||||
|
||||
package ispriv
|
7
ispriv/doc_windows.go
Normal file
7
ispriv/doc_windows.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
ispriv provides functions on Windows to determine the currentl privilege status.
|
||||
*/
|
||||
|
||||
package ispriv
|
68
ispriv/funcs_nix.go
Normal file
68
ispriv/funcs_nix.go
Normal file
@ -0,0 +1,68 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`os`
|
||||
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
)
|
||||
|
||||
/*
|
||||
GetProcIDs returns a ProcIDs from a given PID. An error will be raised if the process ID doesn't exist.
|
||||
A negative value indicates "self" (see also GetProcIDsSelf).
|
||||
|
||||
Note that if you are not EUID == 0 (root) or you/the sudo target user does not own the process,
|
||||
the returning ProcIDs is HIGHLY LIKELY to be very inaccurate.
|
||||
*/
|
||||
func GetProcIDs(pid int32) (p *ProcIDs, err error) {
|
||||
|
||||
var proc ProcIDs
|
||||
var ids []uint32
|
||||
|
||||
if pid < 0 {
|
||||
pid = int32(os.Getpid())
|
||||
}
|
||||
|
||||
if proc.proc, err = process.NewProcess(pid); err != nil {
|
||||
return
|
||||
}
|
||||
if ids, err = proc.proc.Gids(); err != nil {
|
||||
return
|
||||
}
|
||||
p.gids = &IdInfo{
|
||||
real: uint(ids[0]),
|
||||
effective: uint(ids[1]),
|
||||
savedSet: uint(ids[2]),
|
||||
filesystem: nil,
|
||||
}
|
||||
if len(ids) == 4 {
|
||||
p.gids.filesystem = new(uint)
|
||||
*p.gids.filesystem = uint(ids[3])
|
||||
}
|
||||
if ids, err = proc.proc.Uids(); err != nil {
|
||||
return
|
||||
}
|
||||
p.uids = &IdInfo{
|
||||
real: uint(ids[0]),
|
||||
effective: uint(ids[1]),
|
||||
savedSet: uint(ids[2]),
|
||||
filesystem: nil,
|
||||
}
|
||||
if len(ids) == 4 {
|
||||
p.uids.filesystem = new(uint)
|
||||
*p.uids.filesystem = uint(ids[3])
|
||||
}
|
||||
|
||||
p = &proc
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetProcIDsSelf returns a ProcIDs from the current process' PID.
|
||||
func GetProcIDsSelf() (p *ProcIDs, err error) {
|
||||
|
||||
p, err = GetProcIDs(int32(os.Getpid()))
|
||||
|
||||
return
|
||||
}
|
426
ispriv/funcs_procids_nix.go
Normal file
426
ispriv/funcs_procids_nix.go
Normal file
@ -0,0 +1,426 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`os`
|
||||
`os/user`
|
||||
`strconv`
|
||||
`strings`
|
||||
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
`golang.org/x/sys/unix`
|
||||
`r00t2.io/sysutils/envs`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
// GetEffective returns the EUID/EGID.
|
||||
func (p *ProcIDs) GetEffective() (euid, egid uint) {
|
||||
|
||||
euid = p.uids.effective
|
||||
egid = p.gids.effective
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetFS returns the FSUID/FSGID. Not all platforms have this, in which case they'll be nil.
|
||||
func (p *ProcIDs) GetFS() (fsuid, fsgid *uint) {
|
||||
|
||||
if p.uids.filesystem != nil {
|
||||
fsuid = new(uint)
|
||||
*fsuid = *p.uids.filesystem
|
||||
}
|
||||
if p.gids.filesystem != nil {
|
||||
fsgid = new(uint)
|
||||
*fsgid = *p.gids.filesystem
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetGids returms a set of a ProcIDs GIDs.
|
||||
fs will be nil if unsupported on the platform.
|
||||
If invoked with SGID, "savedSet" will be the SGID GID.
|
||||
*/
|
||||
func (p *ProcIDs) GetGids() (real, effective, savedSet uint, fs *uint) {
|
||||
|
||||
real = p.gids.real
|
||||
effective = p.gids.effective
|
||||
savedSet = p.gids.savedSet
|
||||
if p.gids.filesystem != nil {
|
||||
fs = new(uint)
|
||||
*fs = *p.gids.filesystem
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetReal returns the (R)UID/(R)GID.
|
||||
func (p *ProcIDs) GetReal() (ruid, rgid uint) {
|
||||
|
||||
ruid = p.uids.real
|
||||
rgid = p.gids.real
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetSaved returns the SUID/SGID.
|
||||
func (p *ProcIDs) GetSaved() (suid, sgid uint) {
|
||||
|
||||
suid = p.uids.savedSet
|
||||
sgid = p.gids.savedSet
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetUids returms a set of a ProcIDs UIDs.
|
||||
fs will be nil if unsupported on the platform.
|
||||
If invoked with SUID, "savedSet" will be the SUID UID.
|
||||
*/
|
||||
func (p *ProcIDs) GetUids() (real, effective, savedSet uint, fs *uint) {
|
||||
|
||||
real = p.uids.real
|
||||
effective = p.uids.effective
|
||||
savedSet = p.uids.savedSet
|
||||
if p.uids.filesystem != nil {
|
||||
fs = new(uint)
|
||||
*fs = *p.uids.filesystem
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSGID returns true if the process is Set GID/SGID.
|
||||
|
||||
Note that it will return false if invoked by a group with the same GID as an SGID that's set.
|
||||
*/
|
||||
func (p *ProcIDs) IsSGID() (isSgid bool) {
|
||||
|
||||
isSgid = p.gids.real != p.gids.savedSet
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSUID returns true if the process is Set UID/SUID.
|
||||
|
||||
Note that it will return false if invoked by a user with the same UID as an SUID that's set.
|
||||
*/
|
||||
func (p *ProcIDs) IsSUID() (isSuid bool) {
|
||||
|
||||
isSuid = p.uids.real != p.uids.savedSet
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudo does a very fast (and potentially inaccurate) evaluation of whether the process is running under sudo.
|
||||
|
||||
DO NOT use this function for security-sensitive uses, fully accurate results, or critical implementations!
|
||||
Use IsSudoWithConfidence instead for those cases.
|
||||
IsSudo only does the most basic of checking, which can be easily and completely overridden by a non-privileged user.
|
||||
*/
|
||||
func (p *ProcIDs) IsSudo() (isSudo bool) {
|
||||
|
||||
// This is how every other Joe Blow does this. It's an extremely dumb way to do it. The caller has been warned.
|
||||
for k, _ := range envs.GetEnvMap() {
|
||||
if strings.HasPrefix(k, sudoEnvPfx) {
|
||||
isSudo = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoDetailed returns true for a very fast evaluation of whether the process is running under sudo,
|
||||
and information about that context.
|
||||
(If isSudo is false, originalUid/originalGid will both be -1 and originalUser will be nil.)
|
||||
|
||||
DO NOT use this function for security-sensitive uses, fully accurate results, or critical implementations!
|
||||
Use IsSudoWithConfidenceDetailed instead for those cases.
|
||||
IsSudoDetailed only does the most basic of checking, which can be easily and completely overridden by a non-privileged user.
|
||||
*/
|
||||
func (p *ProcIDs) IsSudoDetailed() (isSudo bool, originalUid, originalGid int, originalUser *user.User, err error) {
|
||||
|
||||
if originalUid, originalGid, originalUser, err = p.getSudoInfoEnv(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if originalUid >= 0 || originalGid >= 0 || originalUser != nil {
|
||||
isSudo = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoWithConfidence is like IsSudo, but is *much* more throrough.
|
||||
|
||||
It not only returns isSudo, which is true if *any* indicators pass,
|
||||
but also:
|
||||
|
||||
* a confidence value (which indicates *how many* indicators *passed*)
|
||||
* a maxConfidence value (which indicates how many indicators were *tested*)
|
||||
* a score value (which is a float indicating overall confidence on a fixed and weighted scale; higher is more confident, 1.0 indicates 100% confidence)
|
||||
*/
|
||||
func (p *ProcIDs) IsSudoWithConfidence() (isSudo bool, confidence, maxConfidence uint, score float64, err error) {
|
||||
|
||||
// confidence/maxConfidence are not used directly; they're unweighted counters.
|
||||
var scoreConf uint
|
||||
var scoreMaxConf uint
|
||||
|
||||
score = float64(scoreConf) / float64(scoreMaxConf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoWithConfidenceDetailed is like IsSudoDetailed, but is *much* more throrough.
|
||||
|
||||
It not only returns the same results as IsSudoDetailed, but includes the same scoring values/system as IsSudoWithConfidence.
|
||||
*/
|
||||
func (p *ProcIDs) IsSudoWithConfidenceDetailed() (isSudo bool, confidence, maxConfidence uint, score float64, originalUid, originalGid int, originalUser *user.User, err error) {
|
||||
|
||||
var b []byte
|
||||
var ok bool
|
||||
var permErr bool
|
||||
var envUid int
|
||||
var envGid int
|
||||
var scoreConf uint
|
||||
var scoreMaxConf uint
|
||||
var curUser *user.User
|
||||
var envUser *user.User
|
||||
var curUid uint64
|
||||
var fstat unix.Stat_t
|
||||
var fsUid int
|
||||
var procFiles []process.OpenFilesStat
|
||||
var loginUidFile string = curLoginUidFile
|
||||
|
||||
if curUser, err = user.Current(); err != nil {
|
||||
return
|
||||
}
|
||||
if curUid, err = strconv.ParseUint(curUser.Uid, 10, 32); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if procFiles, err = p.proc.OpenFiles(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Env vars; only score 1x/each.
|
||||
maxConfidence += 3
|
||||
scoreMaxConf += 3
|
||||
if envUid, envGid, envUser, err = p.getSudoInfoEnv(); err != nil {
|
||||
return
|
||||
}
|
||||
originalUid, originalGid, originalUser = envUid, envGid, envUser
|
||||
if envUid >= 0 {
|
||||
confidence++
|
||||
scoreConf++
|
||||
}
|
||||
if envGid >= 0 {
|
||||
confidence++
|
||||
scoreConf++
|
||||
}
|
||||
if envUser != nil {
|
||||
confidence++
|
||||
scoreConf++
|
||||
}
|
||||
|
||||
/*
|
||||
TTY/PTY ownership. We (can) only check this if we're running in an interactive session.
|
||||
|
||||
Typically this is done via (golang.org/x/term).IsTerminal(),
|
||||
That pulls in a bunch of stuff I don't need, though, so I'll just replicate (...).IsTerminal() here;
|
||||
it's just a wrapped single function call.
|
||||
*/
|
||||
// procFiles[0] is always STDIN. Whether it's a pipe, or TTY/PTY, or file, etc.
|
||||
// (likewise, procFiles[1] is always STDOUT, procFiles[2] is always STDERR); however...
|
||||
if _, err = unix.IoctlGetTermios(int(procFiles[0].Fd), unix.TCGETS); err == nil {
|
||||
// Interactive
|
||||
maxConfidence++
|
||||
// This is only worth 2. It's pretty hard to fake unless origin user is root,
|
||||
// but it's ALSO usually set to the target user.
|
||||
scoreMaxConf += 2
|
||||
fstat = unix.Stat_t{}
|
||||
if err = unix.Fstat(int(procFiles[0].Fd), &fstat); err != nil {
|
||||
return
|
||||
}
|
||||
if uint64(fstat.Uid) != curUid {
|
||||
// This is a... *potential* indicator, if a lateral sudo was done (user1 => user2),
|
||||
// or root used sudo to *drop* privs to a regular user.
|
||||
// We mark it as a pass for confidence since it IS a terminal, and it's permission-related.
|
||||
confidence++
|
||||
scoreConf += 2
|
||||
originalUid = int(fstat.Uid)
|
||||
}
|
||||
} else {
|
||||
// err is OK; just means non-interactive. No counter or score/max score increase; basically a NO-OP.
|
||||
err = nil
|
||||
}
|
||||
|
||||
// /proc/self/loginuid
|
||||
// This is a REALLY good indicator. Probably the strongest next to reverse-walking the proc tree. It depends on PAM and auditd support, I think,
|
||||
// BUT if it's present it's *really* really strong.
|
||||
if ok, err = paths.RealPathExists(&loginUidFile); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
maxConfidence++
|
||||
scoreMaxConf += 5
|
||||
if b, err = os.ReadFile(loginUidFile); err != nil {
|
||||
return
|
||||
}
|
||||
if fsUid, err = strconv.Atoi(strings.TrimSpace(string(b))); err != nil {
|
||||
return
|
||||
}
|
||||
if uint64(fsUid) != curUid {
|
||||
confidence++
|
||||
scoreConf += 5
|
||||
originalUid = fsUid
|
||||
}
|
||||
}
|
||||
|
||||
// proc tree reverse walking.
|
||||
// This is, by far, the most reliable method.
|
||||
// There are some valid conditions in which this would fail due to permissions
|
||||
// (e.g. lateral sudo: user1 => user2), but if it's a permission error it's *probably*
|
||||
// a lateral move anyways.
|
||||
if isSudo, permErr, originalUid, originalGid, originalUser, err = p.revProcWalk(); err != nil {
|
||||
return
|
||||
}
|
||||
maxConfidence++
|
||||
scoreMaxConf += 10
|
||||
if permErr {
|
||||
confidence++
|
||||
scoreConf += 5
|
||||
} else if isSudo {
|
||||
confidence++
|
||||
scoreConf += 10
|
||||
}
|
||||
|
||||
score = float64(scoreConf) / float64(scoreMaxConf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
getSudoInfoEnv returns env var driven sudo information.
|
||||
|
||||
These are in no way guaranteed to be accurate as the user can remove or override them.
|
||||
*/
|
||||
func (p *ProcIDs) getSudoInfoEnv() (uid, gid int, u *user.User, err error) {
|
||||
|
||||
var ok bool
|
||||
var val string
|
||||
var envMap map[string]string = envs.GetEnvMap()
|
||||
|
||||
uid = -1
|
||||
gid = -1
|
||||
|
||||
if val, ok = envMap[sudoUnameEnv]; ok {
|
||||
if u, err = user.Lookup(val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if val, ok = envMap[sudoUidEnv]; ok {
|
||||
if uid, err = strconv.Atoi(val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if val, ok = envMap[sudoGidEnv]; ok {
|
||||
if gid, err = strconv.Atoi(val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
revProcWalk walks up the process tree ("proctree") until it either:
|
||||
|
||||
* finds a process invoked with sudo (true)
|
||||
* hits PID == 1 (false)
|
||||
* hits a permission error (true-ish)
|
||||
*/
|
||||
func (p *ProcIDs) revProcWalk() (sudoFound, isPermErr bool, origUid, origGid int, origUser *user.User, err error) {
|
||||
|
||||
var cmd []string
|
||||
var parent *ProcIDs
|
||||
var parentPid int32
|
||||
var parentUname string
|
||||
var parentUids []uint32
|
||||
var parentGids []uint32
|
||||
|
||||
origUid = -1
|
||||
origGid = -1
|
||||
|
||||
parent = p
|
||||
for {
|
||||
if parent == nil || parent.proc.Pid == 1 {
|
||||
break
|
||||
}
|
||||
if cmd, err = parent.proc.CmdlineSlice(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if cmd[0] == "sudo" {
|
||||
sudoFound = true
|
||||
if parentUname, err = parent.proc.Username(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if parentUids, err = parent.proc.Uids(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if parentGids, err = parent.proc.Gids(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if origUser, err = user.Lookup(parentUname); err != nil {
|
||||
return
|
||||
}
|
||||
origUid = int(parentUids[0])
|
||||
origGid = int(parentGids[0])
|
||||
}
|
||||
if sudoFound {
|
||||
break
|
||||
}
|
||||
if parentPid, err = parent.proc.Ppid(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if parent, err = GetProcIDs(parentPid); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
60
ispriv/funcs_windows.go
Normal file
60
ispriv/funcs_windows.go
Normal file
@ -0,0 +1,60 @@
|
||||
//go:build windows
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/windows`
|
||||
)
|
||||
|
||||
// IsAdmin returns true if currently running with Administrator privileges.
|
||||
func IsAdmin() (admin bool, err error) {
|
||||
|
||||
var sid *windows.SID
|
||||
var tok windows.Token
|
||||
|
||||
if err = windows.AllocateAndInitializeSid(
|
||||
&windows.SECURITY_NT_AUTHORITY, // identAuth
|
||||
2, // subAuth
|
||||
windows.SECURITY_BUILTIN_DOMAIN_RID, // subAuth0
|
||||
windows.DOMAIN_ALIAS_RID_ADMINS, // subAuth1
|
||||
0, 0, 0, 0, 0, 0, // subAuth2-10
|
||||
&sid, // sid
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
defer windows.FreeSid(sid)
|
||||
|
||||
tok = windows.Token(0)
|
||||
if admin, err = tok.IsMember(sid); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsElevated returns true if running in an elevated ("Run as Administrator") context.
|
||||
func IsElevated() (elevated bool) {
|
||||
|
||||
var tok windows.Token = windows.Token(0)
|
||||
|
||||
elevated = tok.IsElevated()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPrivileged indicates that the current security context is running both
|
||||
with Administrator priviliges AND is elevated.
|
||||
*/
|
||||
func IsPrivileged() (privileged bool, err error) {
|
||||
|
||||
if privileged, err = IsAdmin(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if privileged {
|
||||
privileged = IsElevated()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
19
ispriv/types_nix.go
Normal file
19
ispriv/types_nix.go
Normal file
@ -0,0 +1,19 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
)
|
||||
|
||||
type ProcIDs struct {
|
||||
proc *process.Process
|
||||
uids *IdInfo
|
||||
gids *IdInfo
|
||||
}
|
||||
type IdInfo struct {
|
||||
real uint
|
||||
effective uint
|
||||
savedSet uint
|
||||
filesystem *uint
|
||||
}
|
1
paths/TODO
Normal file
1
paths/TODO
Normal file
@ -0,0 +1 @@
|
||||
- search criteria should *also* support a timestamp range (e.g. so a search can be restricted to both older than AND newer than; e.g. older than 00:00, newer than 01:00)
|
@ -17,6 +17,17 @@ const (
|
||||
modeAnyExceptRegular pathMode = modeDir | modeSymlink | modePipe | modeSocket | modeDev | modeCharDev | modeIrregular
|
||||
)
|
||||
|
||||
// Miss reasons
|
||||
const (
|
||||
MissNoMiss missReason = ""
|
||||
MissNoMeta missReason = "Could not determine metadata"
|
||||
MissBadBase missReason = "Base name does not match BasePtrn"
|
||||
MissBadPath missReason = "Path does not match PathPtrn"
|
||||
MissBadTime missReason = "Time(s) does not/do not match Age"
|
||||
MissFile missReason = "Object is a file and NoFiles is set"
|
||||
MissType missReason = "Object does not match TargetType"
|
||||
)
|
||||
|
||||
// Times
|
||||
const TimeAny pathTimeType = 0
|
||||
const (
|
||||
|
12
paths/errs.go
Normal file
12
paths/errs.go
Normal file
@ -0,0 +1,12 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilErrChan error = errors.New("an initialized error channel is required")
|
||||
ErrNilMatchChan error = errors.New("an initialized matches channel is required")
|
||||
ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required")
|
||||
ErrNilWg error = errors.New("a non-nil sync.WaitGroup is required")
|
||||
)
|
282
paths/funcs.go
282
paths/funcs.go
@ -26,8 +26,7 @@ import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
`regexp`
|
||||
`slices`
|
||||
`sort`
|
||||
"strings"
|
||||
`sync`
|
||||
`time`
|
||||
@ -35,7 +34,6 @@ import (
|
||||
// "syscall"
|
||||
|
||||
`github.com/djherbis/times`
|
||||
`golang.org/x/sync/semaphore`
|
||||
`r00t2.io/goutils/bitmask`
|
||||
)
|
||||
|
||||
@ -277,86 +275,33 @@ func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SearchPaths gets a file/directory path list based on the provided criteria.
|
||||
// SearchFsPaths gets a file/directory/etc. path list based on the provided criteria.
|
||||
func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) {
|
||||
|
||||
targetType defines what should be included in the path list.
|
||||
It can consist of one or more (io/)fs.FileMode types OR'd together
|
||||
(ensure they are part of (io/)fs.ModeType).
|
||||
(You can use 0 to match regular files explicitly, and/or noFiles = true to exclude them.)
|
||||
var matched *FsSearchResult
|
||||
var missed *FsSearchResult
|
||||
|
||||
noFiles, if true, will explicitly filter out regular files from the path results.
|
||||
(Normally they are *always* included regardless of targetType.)
|
||||
|
||||
basePtrn may be nil; if it isn't, it will be applied to *base names*
|
||||
(that is, quux.txt rather than /foo/bar/baz/quux.txt).
|
||||
|
||||
pathPtrn is like basePtrn except it applies to the *entire* path,
|
||||
not just the basename, if not nil (e.g. /foo/bar/baz/quux.txt,
|
||||
not just quux.txt).
|
||||
|
||||
If age is not nil, it will be applied to the path object.
|
||||
It will match older files/directories/etc. if olderThan is true,
|
||||
otherwise it will match newer files/directories/etc.
|
||||
(olderThan is not used otherwise.)
|
||||
|
||||
ageType is one or more Time* constants OR'd together to describe which timestamp type to check.
|
||||
(Note that TimeCreated may not match if specified as it is only available on certain OSes,
|
||||
kernel versions, and filesystems. This may lead to files being excluded that may have otherwise
|
||||
been included.)
|
||||
(You can use TimeAny to specify any supported time.)
|
||||
*Any* matching timestamp of all specified (and supported) timestamp types matches,
|
||||
so be judicious with your selection. They are processed in order of:
|
||||
|
||||
* btime (birth/creation time) (if supported)
|
||||
* mtime (modification time -- contents have changed)
|
||||
* ctime (OS-specific behavior; generally disk metadata has changed) (if supported)
|
||||
* atime (access time)
|
||||
|
||||
olderThan (as mentioned above) will find paths *older* than age if true, otherwise *newer*.
|
||||
|
||||
now, if not nil, will be used to compare the age of files. (If nil, it will be populated at time of call.)
|
||||
*/
|
||||
func SearchFsPaths(
|
||||
root string,
|
||||
targetType fs.FileMode, noFiles bool,
|
||||
basePtrn, pathPtrn *regexp.Regexp,
|
||||
age *time.Duration, ageType pathTimeType, olderThan bool, now *time.Time,
|
||||
) (foundPaths []string, err error) {
|
||||
|
||||
if age != nil {
|
||||
if now == nil {
|
||||
now = new(time.Time)
|
||||
*now = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
if err = RealPath(&root); err != nil {
|
||||
if err = RealPath(&matcher.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = filepath.WalkDir(
|
||||
root,
|
||||
matcher.Root,
|
||||
func(path string, d fs.DirEntry, inErr error) (outErr error) {
|
||||
|
||||
var include bool
|
||||
|
||||
if inErr != nil {
|
||||
outErr = inErr
|
||||
return
|
||||
}
|
||||
|
||||
if include, outErr = filterPath(
|
||||
path, d,
|
||||
targetType, noFiles,
|
||||
basePtrn, pathPtrn,
|
||||
age, ageType, olderThan, now,
|
||||
); outErr != nil {
|
||||
if matched, missed, outErr = matcher.Match(path, d, nil); outErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if include {
|
||||
foundPaths = append(foundPaths, path)
|
||||
if matched != nil && !matcher.NoMatch {
|
||||
found = append(found, matched)
|
||||
}
|
||||
if missed != nil && !matcher.NoMismatch {
|
||||
miss = append(miss, missed)
|
||||
}
|
||||
|
||||
return
|
||||
@ -365,8 +310,18 @@ func SearchFsPaths(
|
||||
return
|
||||
}
|
||||
|
||||
if found == nil || len(found) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// And sort them.
|
||||
slices.Sort(foundPaths)
|
||||
sort.Slice(
|
||||
found,
|
||||
func(i, j int) (isLess bool) {
|
||||
isLess = found[i].Path < found[j].Path
|
||||
return
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
@ -375,162 +330,107 @@ func SearchFsPaths(
|
||||
SearchFsPathsAsync is exactly like SearchFsPaths, but dispatches off concurrent
|
||||
workers for the filtering logic instead of performing iteratively/recursively.
|
||||
It may, in some cases, be *slightly more* performant and *slightly less* in others.
|
||||
Additional options are documented below.
|
||||
Note that unlike SearchFsPaths, the results written to foundPathsChan are not
|
||||
guaranteed to be in any predictable order.
|
||||
Note that unlike SearchFsPaths, the results written to the
|
||||
FsSearchCriteriaAsync.ResChan are not guaranteed to be in any predictable order.
|
||||
|
||||
All channels are expected to have been initialized by the caller ahead of time,
|
||||
and all provided channels will be closed upon completion (so they are only safe
|
||||
to READ from after invoking SearchFsPathsAsync).
|
||||
|
||||
foundPathsChan is a channel to which matched filepaths will be written.
|
||||
|
||||
sem/semCtx are optional; if not nil, they can be used to limit/"batch" concurrent tasks.
|
||||
(semCtx is the context.Context used for sem when acquiring. It may be nil;
|
||||
one will be locally created if so.)
|
||||
The default will be to spawn all filtering logic concurrently.
|
||||
For very large directories, you almost assuredly do not want that -- it
|
||||
can cause a significant amount of I/O and CPU wait.
|
||||
(See https://pkg.go.dev/golang.org/x/sync/semaphore for details.)
|
||||
|
||||
wg *must not* be nil, and must be managed by the caller.
|
||||
SearchFsPathsAsync will exit with no errors but no-op if wg is nil.
|
||||
|
||||
errChan will receive any/all encountered errors.
|
||||
All channels are expected to have already been initialized by the caller.
|
||||
They will not be closed by this function.
|
||||
*/
|
||||
func SearchFsPathsAsync(
|
||||
root string,
|
||||
targetType fs.FileMode, noFiles bool,
|
||||
basePtrn, pathPtrn *regexp.Regexp,
|
||||
age *time.Duration, ageType pathTimeType, olderThan bool, now *time.Time,
|
||||
foundPathsChan chan string,
|
||||
sem *semaphore.Weighted, semCtx context.Context,
|
||||
wg *sync.WaitGroup,
|
||||
errChan chan error,
|
||||
) {
|
||||
func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) {
|
||||
|
||||
var err error
|
||||
var localWg sync.WaitGroup
|
||||
var wgLocal sync.WaitGroup
|
||||
var doneChan chan bool = make(chan bool, 1)
|
||||
|
||||
if wg == nil {
|
||||
if matcher.ErrChan == nil {
|
||||
panic(ErrNilErrChan)
|
||||
return
|
||||
}
|
||||
|
||||
if age != nil {
|
||||
if now == nil {
|
||||
now = new(time.Time)
|
||||
*now = time.Now()
|
||||
}
|
||||
if matcher.WG == nil {
|
||||
matcher.ErrChan <- ErrNilWg
|
||||
return
|
||||
}
|
||||
|
||||
if sem != nil && semCtx == nil {
|
||||
semCtx = context.Background()
|
||||
defer matcher.WG.Done()
|
||||
|
||||
if matcher.ResChan == nil && !matcher.NoMatch {
|
||||
matcher.ErrChan <- ErrNilMatchChan
|
||||
return
|
||||
}
|
||||
if matcher.MismatchChan == nil && !matcher.NoMismatch {
|
||||
matcher.ErrChan <- ErrNilMismatchChan
|
||||
return
|
||||
}
|
||||
|
||||
if err = RealPath(&matcher.Root); err != nil {
|
||||
matcher.ErrChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if matcher.Semaphore != nil && matcher.SemaphoreCtx == nil {
|
||||
matcher.SemaphoreCtx = context.Background()
|
||||
}
|
||||
|
||||
if err = filepath.WalkDir(
|
||||
root,
|
||||
matcher.Root,
|
||||
func(path string, de fs.DirEntry, inErr error) (outErr error) {
|
||||
localWg.Add(1)
|
||||
wg.Add(1)
|
||||
if sem != nil {
|
||||
if err = sem.Acquire(semCtx, 1); err != nil {
|
||||
|
||||
if inErr != nil {
|
||||
inErr = filterNoFileDir(inErr)
|
||||
if inErr != nil {
|
||||
outErr = inErr
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
wgLocal.Add(1)
|
||||
if matcher.Semaphore != nil {
|
||||
if err = matcher.Semaphore.Acquire(matcher.SemaphoreCtx, 1); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go func(p string, d fs.DirEntry) {
|
||||
var pErr error
|
||||
var pInclude bool
|
||||
var pResMatch *FsSearchResult
|
||||
var pResMiss *FsSearchResult
|
||||
|
||||
defer localWg.Done()
|
||||
defer wg.Done()
|
||||
defer wgLocal.Done()
|
||||
|
||||
if sem != nil {
|
||||
defer sem.Release(1)
|
||||
if matcher.Semaphore != nil {
|
||||
defer matcher.Semaphore.Release(1)
|
||||
}
|
||||
|
||||
if pInclude, pErr = filterPath(p, d, targetType, noFiles, basePtrn, pathPtrn, age, ageType, olderThan, now); pErr != nil {
|
||||
errChan <- pErr
|
||||
if pResMatch, pResMiss, pErr = matcher.Match(p, d, nil); pErr != nil {
|
||||
matcher.ErrChan <- pErr
|
||||
return
|
||||
}
|
||||
|
||||
if pInclude {
|
||||
foundPathsChan <- p
|
||||
if pResMatch != nil && !matcher.NoMatch {
|
||||
matcher.ResChan <- pResMatch
|
||||
}
|
||||
if pResMiss != nil && !matcher.NoMismatch {
|
||||
matcher.MismatchChan <- pResMiss
|
||||
}
|
||||
}(path, de)
|
||||
|
||||
return
|
||||
},
|
||||
); err != nil {
|
||||
errChan <- err
|
||||
err = filterNoFileDir(err)
|
||||
if err != nil {
|
||||
matcher.ErrChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
localWg.Wait()
|
||||
close(foundPathsChan)
|
||||
close(errChan)
|
||||
wgLocal.Wait()
|
||||
doneChan <- true
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// filterPath applies the filter logic used by SearchFSPaths and SearchFsPathsAync.
|
||||
func filterPath(
|
||||
path string, d fs.DirEntry,
|
||||
targetType fs.FileMode, noFiles bool,
|
||||
basePtrn, pathPtrn *regexp.Regexp,
|
||||
age *time.Duration, ageType pathTimeType, olderThan bool, now *time.Time,
|
||||
) (include bool, err error) {
|
||||
|
||||
var typeMode fs.FileMode
|
||||
var fi fs.FileInfo
|
||||
var tspec times.Timespec
|
||||
var typeFilter *bitmask.MaskBit = bitmask.NewMaskBitExplicit(uint(targetType))
|
||||
|
||||
if age != nil {
|
||||
if now == nil {
|
||||
now = new(time.Time)
|
||||
*now = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// patterns
|
||||
if pathPtrn != nil {
|
||||
if !pathPtrn.MatchString(path) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if basePtrn != nil {
|
||||
if !basePtrn.MatchString(filepath.Base(path)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// age
|
||||
if age != nil {
|
||||
if tspec, err = times.Stat(path); err != nil {
|
||||
return
|
||||
}
|
||||
if !filterTimes(tspec, age, &ageType, olderThan, now) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// fs object type (file, dir, etc.)
|
||||
if fi, err = d.Info(); err != nil {
|
||||
return
|
||||
}
|
||||
typeMode = fi.Mode().Type()
|
||||
if typeMode == 0 && noFiles {
|
||||
return
|
||||
} else if typeMode != 0 {
|
||||
if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
include = true
|
||||
<-doneChan
|
||||
|
||||
return
|
||||
}
|
||||
@ -597,3 +497,13 @@ func filterTimes(tspec times.Timespec, age *time.Duration, ageType *pathTimeType
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func filterNoFileDir(err error) (filtered error) {
|
||||
|
||||
filtered = err
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
filtered = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
125
paths/funcs_fssearchcriteria.go
Normal file
125
paths/funcs_fssearchcriteria.go
Normal file
@ -0,0 +1,125 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
`io/fs`
|
||||
`os`
|
||||
`path/filepath`
|
||||
`time`
|
||||
|
||||
`github.com/djherbis/times`
|
||||
`r00t2.io/goutils/bitmask`
|
||||
)
|
||||
|
||||
/*
|
||||
Match returns match (a ptr to a FsSearchResult if the specified path matches, otherwise nil),
|
||||
miss (ptr the specified path does not match, otherwise nil), and an fs.DirEntry and fs.FileInfo
|
||||
for path. d and/or fi may be nil.
|
||||
|
||||
If err is not nil, it represents an unexpected error and as such, both match and miss should be nil.
|
||||
|
||||
Match, miss, and err will all be nil if the filesystem object/path does not exist.
|
||||
*/
|
||||
func (f *FsSearchCriteria) Match(path string, d fs.DirEntry, fi fs.FileInfo) (match, miss *FsSearchResult, err error) {
|
||||
|
||||
var typeMode fs.FileMode
|
||||
var m FsSearchResult
|
||||
var typeFilter *bitmask.MaskBit = bitmask.NewMaskBitExplicit(uint(f.TargetType))
|
||||
|
||||
m = FsSearchResult{
|
||||
Path: path,
|
||||
DirEntry: d,
|
||||
FileInfo: fi,
|
||||
Criteria: f,
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// A DirEntry can be created from a FileInfo but not vice versa.
|
||||
if m.FileInfo == nil {
|
||||
if m.DirEntry != nil {
|
||||
if m.FileInfo, err = m.DirEntry.Info(); err != nil {
|
||||
err = filterNoFileDir(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if f.FollowSymlinks {
|
||||
if m.FileInfo, err = os.Stat(path); err != nil {
|
||||
err = filterNoFileDir(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if m.FileInfo, err = os.Lstat(path); err != nil {
|
||||
err = filterNoFileDir(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
m.DirEntry = fs.FileInfoToDirEntry(m.FileInfo)
|
||||
}
|
||||
}
|
||||
if m.DirEntry == nil {
|
||||
m.DirEntry = fs.FileInfoToDirEntry(m.FileInfo)
|
||||
}
|
||||
if m.DirEntry == nil || m.FileInfo == nil {
|
||||
m.MissReason = MissNoMeta
|
||||
miss = &m
|
||||
return
|
||||
}
|
||||
|
||||
if m.Times, err = times.Stat(path); err != nil {
|
||||
err = filterNoFileDir(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.PathPtrn != nil && !f.PathPtrn.MatchString(path) {
|
||||
m.MissReason = MissBadPath
|
||||
miss = &m
|
||||
return
|
||||
}
|
||||
if f.BasePtrn != nil && !f.BasePtrn.MatchString(filepath.Base(path)) {
|
||||
m.MissReason = MissBadBase
|
||||
miss = &m
|
||||
return
|
||||
}
|
||||
|
||||
// age
|
||||
if f.Age != nil {
|
||||
if f.Now == nil {
|
||||
f.Now = new(time.Time)
|
||||
*f.Now = time.Now()
|
||||
}
|
||||
if !filterTimes(m.Times, f.Age, &f.AgeType, f.OlderThan, f.Now) {
|
||||
m.MissReason = MissBadTime
|
||||
miss = &m
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// fs object type (file, dir, etc.)
|
||||
typeMode = m.FileInfo.Mode().Type()
|
||||
if typeMode == 0 && f.NoFiles {
|
||||
m.MissReason = MissFile
|
||||
miss = &m
|
||||
return
|
||||
} else if typeMode != 0 {
|
||||
if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) {
|
||||
m.MissReason = MissType
|
||||
miss = &m
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If it gets to here, it matches.
|
||||
match = &m
|
||||
|
||||
return
|
||||
}
|
127
paths/types.go
127
paths/types.go
@ -1,9 +1,136 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
`context`
|
||||
`io/fs`
|
||||
`regexp`
|
||||
`sync`
|
||||
`time`
|
||||
|
||||
`github.com/djherbis/times`
|
||||
`golang.org/x/sync/semaphore`
|
||||
`r00t2.io/goutils/bitmask`
|
||||
)
|
||||
|
||||
// FsSearchCriteria contains filter criteria for SearchFsPaths* functions.
|
||||
type FsSearchCriteria struct {
|
||||
// Root indicates the root to search.
|
||||
Root string `json:"root" toml:"RootPath" yaml:"Root Path" xml:"root,attr" validate:"dir"`
|
||||
// NoMatch, if true, will not return matches. If NoMatch and NoMismatch are both true, no results will be returned.
|
||||
NoMatch bool `json:"no_match" toml:"NoMatch" yaml:"No Matches" xml:"noMatch,attr"`
|
||||
// NoMismatch, if true, will not return mismatches. If NoMatch and NoMismatch are both true, no results will be returned.
|
||||
NoMismatch bool `json:"no_miss" toml:"NoMismatch" yaml:"No Mismatches" xml:"noMiss,attr"`
|
||||
/*
|
||||
TargetType defines what types of filesystem objects should be matched.
|
||||
It can consist of one or more (io/)fs.FileMode types OR'd together
|
||||
(ensure they are part of (io/)fs.ModeType).
|
||||
(You can use 0 to match regular files explicitly, and/or NoFiles = true to exclude them.)
|
||||
*/
|
||||
TargetType fs.FileMode `json:"type_tgt" toml:"TargetType" yaml:"Target Type" xml:"typeTgt,attr"`
|
||||
// NoFiles excludes files from TargetType-matching (as there isn't a way to explicitly exclude files otherwise if a non-zero mode is given).
|
||||
NoFiles bool `json:"no_file" toml:"ExcludeFiles" yaml:"Exclude Files" xml:"noFile,attr"`
|
||||
// FollowSymlinks, if true and a path being tested is a symlink, will use metadata (age, etc.) of the symlink itself rather than the link target.
|
||||
FollowSymlinks bool `json:"follow_sym" toml:"FollowSymlinks" yaml:"Follow Symlinks" xml:"followSym,attr"`
|
||||
// BasePtrn, if specified, will apply to the *base name (that is, quux.txt rather than /foo/bar/baz/quux.txt). See also PathPtrn.
|
||||
BasePtrn *regexp.Regexp `json:"ptrn_base,omitempty" toml:"BaseNamePattern,omitempty" yaml:"Base Name Pattern,omitempty" xml:"ptrnBase,attr,omitempty"`
|
||||
// PathPtrn, if specified, will apply to the *full path* (e.g. /foo/bar/baz/quux.txt, not just quux.txt). See also BasePtrn.
|
||||
PathPtrn *regexp.Regexp `json:"ptrn_path,omitempty" toml:"PathPattern,omitempty" yaml:"Path Pattern,omitempty" xml:"ptrnPath,attr,omitempty"`
|
||||
/*
|
||||
Age, if specified, indicates the comparison of Now againt the AgeType of filesystem objects.
|
||||
Use OlderThan to indicate if it should be older or newer.
|
||||
*/
|
||||
Age *time.Duration `json:"age,omitempty" toml:"Age,omitempty" yaml:"Age,omitempty" xml:"age,attr,omitempty"`
|
||||
/*
|
||||
AgeType can be one (or more, OR'd together) of the Time* constants in this package (TimeAny, TimeAccessed, TimeCreated,
|
||||
TimeChanged, TimeModified) to indicate what timestamp(s) to use for comparing Age.
|
||||
|
||||
The zero-value is TimeAny.
|
||||
|
||||
The first matching timestamp will pass all time comparisons.
|
||||
Be mindful of timestamp type support/limitations per OS/filesystem of Root.
|
||||
|
||||
Completely unused if Age is nil.
|
||||
*/
|
||||
AgeType pathTimeType `json:"type_age" toml:"AgeType" yaml:"Age Type" xml:"typeAge,attr"`
|
||||
/*
|
||||
OlderThan, if true (and Age is not nil), indicates that matching filesystem objects should have their
|
||||
AgeType older than Now. If false, their AgeType should be *newer* than Now.
|
||||
|
||||
Completely unused if Age is nil.
|
||||
*/
|
||||
OlderThan bool `json:"older" toml:"OlderThan" yaml:"Older Than" xml:"older,attr"`
|
||||
/*
|
||||
Now expresses a time to compare to Age via AgeType and OlderThan.
|
||||
Note that it may be any valid time, not necessarily "now".
|
||||
If Age is specified but Now is nil, it will be populated with time.Now() when the search is invoked.
|
||||
|
||||
Completely unused if Age is nil.
|
||||
*/
|
||||
Now *time.Time `json:"now,omitempty" toml:"Now,omitempty" yaml:"Now,omitempty" xml:"now,attr,omitempty"`
|
||||
}
|
||||
|
||||
// FsSearchCriteriaAsync extends FsSearchCriteria for use in an asynchronous (goroutine) manner.
|
||||
type FsSearchCriteriaAsync struct {
|
||||
FsSearchCriteria
|
||||
/*
|
||||
WG should be a non-nil pointer to a sync.WaitGroup.
|
||||
This is used to manage searching completion to the caller.
|
||||
|
||||
.Done() will be called once within the search function, but no .Add() will be called;
|
||||
.Add() should be done by the caller beforehand.
|
||||
*/
|
||||
WG *sync.WaitGroup
|
||||
// ResChan must be a non-nil channel for (positive) match results to be sent to.
|
||||
ResChan chan *FsSearchResult
|
||||
// MismatchChan, if not nil, will have negative matches/"misses" sent to it.
|
||||
MismatchChan chan *FsSearchResult
|
||||
/*
|
||||
ErrChan should be a non-nil error channel for any unexpected errors encountered.
|
||||
|
||||
If nil, a panic will be raised.
|
||||
*/
|
||||
ErrChan chan error
|
||||
/*
|
||||
Semaphore is completely optional, but if non-nil
|
||||
it will be used to limit concurrent filesystem
|
||||
object processing.
|
||||
|
||||
It is generally a Very Good Idea(TM) to use this,
|
||||
as the default is to dispatch all processing concurrently.
|
||||
This can lead to some heavy I/O and CPU wait.
|
||||
|
||||
(See https://pkg.go.dev/golang.org/x/sync/semaphore for details.)
|
||||
*/
|
||||
Semaphore *semaphore.Weighted
|
||||
/*
|
||||
SemaphoreCtx is the context.Context to use for Semaphore.
|
||||
If nil (but Sempaphore is not), one will be created locally/internally.
|
||||
*/
|
||||
SemaphoreCtx context.Context
|
||||
}
|
||||
|
||||
// FsSearchResult contains a match/miss result for FsSearchCriteria and FsSearchCriteriaAsync.
|
||||
type FsSearchResult struct {
|
||||
/*
|
||||
Path is the path to the object on the filesystem.
|
||||
It may or may not exist at the time of return,
|
||||
but will not be an empty string.
|
||||
*/
|
||||
Path string `json:"path" toml:"Path" yaml:"Path" xml:"path,attr"`
|
||||
// DirEntry is the fs.DirEntry for the Path; note that .Name() is the base name only. TODO: serialization?
|
||||
DirEntry fs.DirEntry `json:"-" toml:"-" yaml:"-" xml:"-"`
|
||||
// FileInfo is the fs.FileInfo for the Path; note that .Name() is the base name only. TODO: serialization?
|
||||
FileInfo fs.FileInfo `json:"-" toml:"-" yaml:"-" xml:"-"`
|
||||
// Criteria is the evaluated criteria specified that this FsSearchResult matched.
|
||||
Criteria *FsSearchCriteria `json:"criteria" toml:"Criteria" yaml:"Criteria" xml:"criteria"`
|
||||
// Times holds the mtime, ctime, etc. of the filesystem object (where supported). TODO: serialization?
|
||||
Times times.Timespec `json:"-" toml:"-" yaml:"-" xml:"-"`
|
||||
// MissReason contains the reason the result is a miss (MissNoMiss if a match); see the Miss* constants.
|
||||
MissReason missReason `json:"miss_reason" toml:"MissReason" yaml:"Miss Reason" xml:"miss,attr"`
|
||||
}
|
||||
|
||||
type missReason string
|
||||
|
||||
type pathMode bitmask.MaskBit
|
||||
|
||||
type pathTimeType bitmask.MaskBit
|
||||
|
53
types_linux.go
Normal file
53
types_linux.go
Normal file
@ -0,0 +1,53 @@
|
||||
package sysutils
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/unix`
|
||||
)
|
||||
|
||||
/*
|
||||
IDState collects information about the current running process.
|
||||
It should only be used as returned from GetIDState().
|
||||
Its methods WILL return false information if any of these values are altered.
|
||||
|
||||
FSUID/FSGID are not supported.
|
||||
*/
|
||||
type IDState struct {
|
||||
// RUID: Real UID
|
||||
RUID int
|
||||
// EUID: Effective UID
|
||||
EUID int
|
||||
// SUID: Saved Set UID
|
||||
SUID int
|
||||
// RGID: Real GID
|
||||
RGID int
|
||||
// EGID: Effective GID
|
||||
EGID int
|
||||
// SGID: Saved Set GID
|
||||
SGID int
|
||||
// SudoEnvUser is true if SUDO_USER or SUDO_UID is set.
|
||||
SudoEnvUser bool
|
||||
// SudoEnvGroup is true if SUDO_GID is set.
|
||||
SudoEnvGroup bool
|
||||
// SudoEnvCmd is true if SUDO_COMMAND is set.
|
||||
SudoEnvCmd bool
|
||||
// SudoEnvHome is true if SUDO_HOME is set.
|
||||
SudoEnvHome bool
|
||||
// SudoEnvVars is true if any of the "well-known" sudo environment variables are set.
|
||||
SudoEnvVars bool
|
||||
// PPIDUidMatch is true if the parent PID UID matches the current process UID (mismatch usually indicates sudo invocation).
|
||||
PPIDUidMatch bool
|
||||
// PPIDGidMatch is true if the parent PID GID matches the current process GID (mismatch usually indicates sudo invocation).
|
||||
PPIDGidMatch bool
|
||||
// uidsChecked is true if the RUID, EUID, and SUID have been populated. (They will be 0 if unset OR if root.)
|
||||
uidsChecked bool
|
||||
// gidsChecked is true if the RGID, EGID, and SGID have been populated. (They will be 0 if unset OR if root.)
|
||||
gidsChecked bool
|
||||
// sudoChecked is true if the SudoEnvVars is set.
|
||||
sudoChecked bool
|
||||
// ppidUidChecked is true if the PPIDUidMatch is set.
|
||||
ppidUidChecked bool
|
||||
// ppidGidChecked is true if the PPIDGidMatch is set.
|
||||
ppidGidChecked bool
|
||||
// stat holds the stat information for the parent PID.
|
||||
stat *unix.Stat_t
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user