Skip to content

Commit

Permalink
Add initial support to verify the password for the hacluster user (#164)
Browse files Browse the repository at this point in the history
* Add initial support to verify the password for the hacluster user
  • Loading branch information
rtorrero committed Jan 4, 2023
1 parent b0c5d71 commit db91817
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 93 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ require (
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/tredoe/osutil v1.0.6 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03O
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/tredoe/fileutil v1.0.5/go.mod h1:HFzzpvg+3Q8LgmZgo1mVF5epHc/CVkWKEb3hja+/1Zo=
github.com/tredoe/goutil v1.0.0/go.mod h1:Qhf75QLcNEChimbl4wb8nROzw9PCFCPYTEUmTnoszXY=
github.com/tredoe/osutil v1.0.6 h1:KJvG9AFmUPLe3hsNKyPMIjNx77CkAJtMKVS4ugAT7vM=
github.com/tredoe/osutil v1.0.6/go.mod h1:zNq93p2DLHJWkHi2/+zi3xOjZl8xxiv3tiI2A6zcB3w=
github.com/trento-project/contracts/go v0.0.0-20221102082204-01db6a700272 h1:lsmtXU2jklj6kWSsEQkuwrJlazzY23DJHhr4SEiirPM=
github.com/trento-project/contracts/go v0.0.0-20221102082204-01db6a700272/go.mod h1:TB1Ey9OP0Pot//XiShangAVoV+dO+eYlKYo9SC18PMM=
github.com/vektra/mockery/v2 v2.16.0 h1:NMfQMZ4dKAHxR0rTHCt47HyJlBgFdYmrLlHhZIj2yeo=
Expand Down
78 changes: 0 additions & 78 deletions internal/factsengine/gatherers/_todo/verifypassword.go

This file was deleted.

1 change: 1 addition & 0 deletions internal/factsengine/gatherers/gatherer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ func StandardGatherers() map[string]FactGatherer {
SBDConfigGathererName: NewDefaultSBDGatherer(),
SBDDumpGathererName: NewDefaultSBDDumpGatherer(),
SapHostCtrlGathererName: NewDefaultSapHostCtrlGatherer(),
VerifyPasswordGathererName: NewDefaultPasswordGatherer(),
}
}
115 changes: 115 additions & 0 deletions internal/factsengine/gatherers/verifypassword.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package gatherers

import (
"fmt"
"strings"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
crypt "github.com/tredoe/osutil/user/crypt"
sha512crypt "github.com/tredoe/osutil/user/crypt/sha512_crypt"
"github.com/trento-project/agent/pkg/factsengine/entities"
"github.com/trento-project/agent/pkg/utils"
)

const (
VerifyPasswordGathererName = "verify_password"
)

// nolint:gochecknoglobals
var (
checkableUsernames = []string{"hacluster"}
unsafePasswords = []string{"linux"}
)

// nolint:gochecknoglobals
var (
VerifyPasswordInvalidUsername = entities.FactGatheringError{
Type: "verify-password-invalid-username",
Message: "requested user is not whitelisted for password check",
}

VerifyPasswordCryptError = entities.FactGatheringError{
Type: "verify-password-crypt-error",
Message: "error while verifying the password for user",
}
)

type VerifyPasswordGatherer struct {
executor utils.CommandExecutor
}

func NewDefaultPasswordGatherer() *VerifyPasswordGatherer {
return NewVerifyPasswordGatherer(utils.Executor{})
}

func NewVerifyPasswordGatherer(executor utils.CommandExecutor) *VerifyPasswordGatherer {
return &VerifyPasswordGatherer{
executor,
}
}

/*
This gatherer expects only the username for which the password will be verified
*/
func (g *VerifyPasswordGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) {
facts := []entities.Fact{}
log.Infof("Starting password verifying facts gathering process")

for _, factReq := range factsRequests {
if !utils.Contains(checkableUsernames, factReq.Argument) {
gatheringError := VerifyPasswordInvalidUsername.Wrap(factReq.Argument)
fact := entities.NewFactGatheredWithError(factReq, gatheringError)
facts = append(facts, fact)
continue
}
username := factReq.Argument

salt, hash, err := g.getSalt(username)
if err != nil {
log.Error(err)
}
log.Debugf("Obtained salt using user %s: %s", username, salt)

crypter := sha512crypt.New()
isPasswordWeak := false
var gatheringError *entities.FactGatheringError = nil
for _, password := range unsafePasswords {
passwordBytes := []byte(password)

matchErr := crypter.Verify(hash, passwordBytes)
if matchErr == nil {
isPasswordWeak = true
break
}
if !errors.Is(matchErr, crypt.ErrKeyMismatch) {
gatheringError = VerifyPasswordCryptError.Wrap(username)
break
}
}

if gatheringError != nil {
fact := entities.NewFactGatheredWithError(factReq, gatheringError)
facts = append(facts, fact)
continue
}

fact := entities.NewFactGatheredWithRequest(factReq,
&entities.FactValueBool{Value: isPasswordWeak})
facts = append(facts, fact)
}

log.Infof("Requested password verifying facts gathered")
return facts, nil
}

func (g *VerifyPasswordGatherer) getSalt(user string) ([]byte, string, error) {
shadow, err := g.executor.Exec("getent", "shadow", user)
if err != nil {
return nil, "", errors.Wrap(err, "Error getting salt")
}
salt := strings.Split(string(shadow), "$")[2]
hash := strings.Split(string(shadow), ":")[1]

return []byte(fmt.Sprintf("$6$%s", salt)), hash, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import (

"github.com/stretchr/testify/suite"
"github.com/trento-project/agent/pkg/factsengine/entities"
mocks "github.com/trento-project/agent/pkg/factsengine/gatherers/mocks"
utilsMocks "github.com/trento-project/agent/pkg/utils/mocks"
)

type PasswordTestSuite struct {
suite.Suite
mockExecutor *mocks.CommandExecutor
mockExecutor *utilsMocks.CommandExecutor
}

func TestPasswordTestSuite(t *testing.T) {
suite.Run(t, new(PasswordTestSuite))
}

func (suite *PasswordTestSuite) SetupTest() {
suite.mockExecutor = new(mocks.CommandExecutor)
suite.mockExecutor = new(utilsMocks.CommandExecutor)
}

func (suite *PasswordTestSuite) TestPasswordGatherEqual() {
Expand All @@ -34,18 +34,18 @@ func (suite *PasswordTestSuite) TestPasswordGatherEqual() {
factRequests := []entities.FactRequest{
{
Name: "hacluster",
Gatherer: "password",
Argument: "hacluster:linux",
Gatherer: "verify_password",
Argument: "hacluster",
CheckID: "check1",
},
}

factResults, err := verifyPasswordGatherer.Gather(factRequests)

expectedResults := []entities.FactsGatheredItem{
expectedResults := []entities.Fact{
{
Name: "hacluster",
Value: true,
Value: &entities.FactValueBool{Value: true},
CheckID: "check1",
},
}
Expand All @@ -67,18 +67,18 @@ func (suite *PasswordTestSuite) TestPasswordGatherNotEqual() {
factRequests := []entities.FactRequest{
{
Name: "hacluster",
Gatherer: "password",
Argument: "hacluster:linux",
Gatherer: "verify_password",
Argument: "hacluster",
CheckID: "check1",
},
}

factResults, err := verifyPasswordGatherer.Gather(factRequests)

expectedResults := []entities.FactsGatheredItem{
expectedResults := []entities.Fact{
{
Name: "hacluster",
Value: false,
Value: &entities.FactValueBool{Value: false},
CheckID: "check1",
},
}
Expand All @@ -87,21 +87,66 @@ func (suite *PasswordTestSuite) TestPasswordGatherNotEqual() {
suite.ElementsMatch(expectedResults, factResults)
}

func (suite *PasswordTestSuite) TestPasswordGatherCryptError() {
shadow := []byte("hacluster:$aaaa$aaaa")

suite.mockExecutor.On("Exec", "getent", "shadow", "hacluster").Return(
shadow, nil)

verifyPasswordGatherer := NewVerifyPasswordGatherer(suite.mockExecutor)

factRequests := []entities.FactRequest{
{
Name: "hacluster",
Gatherer: "verify_password",
Argument: "hacluster",
CheckID: "check1",
},
}

factResults, err := verifyPasswordGatherer.Gather(factRequests)

expectedResults := []entities.Fact{
{
Name: "hacluster",
CheckID: "check1",
Value: nil,
Error: &entities.FactGatheringError{
Message: "error while verifying the password for user: hacluster",
Type: "verify-password-crypt-error",
},
},
}

suite.NoError(err)
suite.ElementsMatch(expectedResults, factResults)
}

func (suite *PasswordTestSuite) TestPasswordGatherWrongArguments() {
verifyPasswordGatherer := &VerifyPasswordGatherer{} // nolint

factRequests := []entities.FactRequest{
{
Name: "hacluster",
Gatherer: "password",
Argument: "linux",
Name: "pepito",
Gatherer: "verify_password",
Argument: "pepito",
CheckID: "check1",
},
}

factResults, err := verifyPasswordGatherer.Gather(factRequests)

expectedResults := []entities.FactsGatheredItem{}
expectedResults := []entities.Fact{
{
Name: "pepito",
CheckID: "check1",
Value: nil,
Error: &entities.FactGatheringError{
Message: "requested user is not whitelisted for password check: pepito",
Type: "verify-password-invalid-username",
},
},
}

suite.NoError(err)
suite.ElementsMatch(expectedResults, factResults)
Expand Down
9 changes: 9 additions & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ func FindMatches(pattern string, text []byte) map[string]interface{} {
}
return configMap
}

func Contains[T comparable](s []T, e T) bool {
for _, v := range s {
if v == e {
return true
}
}
return false
}

0 comments on commit db91817

Please sign in to comment.