diff --git a/database/secret.go b/database/secret.go index f9dae86d..a8e6c0e7 100644 --- a/database/secret.go +++ b/database/secret.go @@ -42,21 +42,22 @@ var ( // Secret is the database representation of a secret. type Secret struct { - ID sql.NullInt64 `sql:"id"` - Org sql.NullString `sql:"org"` - Repo sql.NullString `sql:"repo"` - Team sql.NullString `sql:"team"` - Name sql.NullString `sql:"name"` - Value sql.NullString `sql:"value"` - Type sql.NullString `sql:"type"` - Images pq.StringArray `sql:"images" gorm:"type:varchar(1000)"` - Events pq.StringArray `sql:"events" gorm:"type:varchar(1000)"` - AllowEvents sql.NullInt64 `sql:"allow_events"` - AllowCommand sql.NullBool `sql:"allow_command"` - CreatedAt sql.NullInt64 `sql:"created_at"` - CreatedBy sql.NullString `sql:"created_by"` - UpdatedAt sql.NullInt64 `sql:"updated_at"` - UpdatedBy sql.NullString `sql:"updated_by"` + ID sql.NullInt64 `sql:"id"` + Org sql.NullString `sql:"org"` + Repo sql.NullString `sql:"repo"` + Team sql.NullString `sql:"team"` + Name sql.NullString `sql:"name"` + Value sql.NullString `sql:"value"` + Type sql.NullString `sql:"type"` + Images pq.StringArray `sql:"images" gorm:"type:varchar(1000)"` + Events pq.StringArray `sql:"events" gorm:"type:varchar(1000)"` + AllowEvents sql.NullInt64 `sql:"allow_events"` + AllowCommand sql.NullBool `sql:"allow_command"` + AllowSubstitution sql.NullBool `sql:"allow_substitution"` + CreatedAt sql.NullInt64 `sql:"created_at"` + CreatedBy sql.NullString `sql:"created_by"` + UpdatedAt sql.NullInt64 `sql:"updated_at"` + UpdatedBy sql.NullString `sql:"updated_by"` } // Decrypt will manipulate the existing secret value by @@ -196,6 +197,7 @@ func (s *Secret) ToLibrary() *library.Secret { secret.SetEvents(s.Events) secret.SetAllowEvents(library.NewEventsFromMask(s.AllowEvents.Int64)) secret.SetAllowCommand(s.AllowCommand.Bool) + secret.SetAllowSubstitution(s.AllowSubstitution.Bool) secret.SetCreatedAt(s.CreatedAt.Int64) secret.SetCreatedBy(s.CreatedBy.String) secret.SetUpdatedAt(s.UpdatedAt.Int64) @@ -272,21 +274,22 @@ func (s *Secret) Validate() error { // to a database Secret type. func SecretFromLibrary(s *library.Secret) *Secret { secret := &Secret{ - ID: sql.NullInt64{Int64: s.GetID(), Valid: true}, - Org: sql.NullString{String: s.GetOrg(), Valid: true}, - Repo: sql.NullString{String: s.GetRepo(), Valid: true}, - Team: sql.NullString{String: s.GetTeam(), Valid: true}, - Name: sql.NullString{String: s.GetName(), Valid: true}, - Value: sql.NullString{String: s.GetValue(), Valid: true}, - Type: sql.NullString{String: s.GetType(), Valid: true}, - Images: pq.StringArray(s.GetImages()), - Events: pq.StringArray(s.GetEvents()), - AllowEvents: sql.NullInt64{Int64: s.GetAllowEvents().ToDatabase(), Valid: true}, - AllowCommand: sql.NullBool{Bool: s.GetAllowCommand(), Valid: true}, - CreatedAt: sql.NullInt64{Int64: s.GetCreatedAt(), Valid: true}, - CreatedBy: sql.NullString{String: s.GetCreatedBy(), Valid: true}, - UpdatedAt: sql.NullInt64{Int64: s.GetUpdatedAt(), Valid: true}, - UpdatedBy: sql.NullString{String: s.GetUpdatedBy(), Valid: true}, + ID: sql.NullInt64{Int64: s.GetID(), Valid: true}, + Org: sql.NullString{String: s.GetOrg(), Valid: true}, + Repo: sql.NullString{String: s.GetRepo(), Valid: true}, + Team: sql.NullString{String: s.GetTeam(), Valid: true}, + Name: sql.NullString{String: s.GetName(), Valid: true}, + Value: sql.NullString{String: s.GetValue(), Valid: true}, + Type: sql.NullString{String: s.GetType(), Valid: true}, + Images: pq.StringArray(s.GetImages()), + Events: pq.StringArray(s.GetEvents()), + AllowEvents: sql.NullInt64{Int64: s.GetAllowEvents().ToDatabase(), Valid: true}, + AllowCommand: sql.NullBool{Bool: s.GetAllowCommand(), Valid: true}, + AllowSubstitution: sql.NullBool{Bool: s.GetAllowSubstitution(), Valid: true}, + CreatedAt: sql.NullInt64{Int64: s.GetCreatedAt(), Valid: true}, + CreatedBy: sql.NullString{String: s.GetCreatedBy(), Valid: true}, + UpdatedAt: sql.NullInt64{Int64: s.GetUpdatedAt(), Valid: true}, + UpdatedBy: sql.NullString{String: s.GetUpdatedBy(), Valid: true}, } return secret.Nullify() diff --git a/database/secret_test.go b/database/secret_test.go index f4c6d23c..6c61ec44 100644 --- a/database/secret_test.go +++ b/database/secret_test.go @@ -171,6 +171,7 @@ func TestDatabase_Secret_ToLibrary(t *testing.T) { want.SetEvents([]string{"push", "tag", "deployment"}) want.SetAllowEvents(library.NewEventsFromMask(1)) want.SetAllowCommand(true) + want.SetAllowSubstitution(true) want.SetCreatedAt(tsCreate) want.SetCreatedBy("octocat") want.SetUpdatedAt(tsUpdate) @@ -295,6 +296,7 @@ func TestDatabase_SecretFromLibrary(t *testing.T) { s.SetEvents([]string{"push", "tag", "deployment"}) s.SetAllowEvents(library.NewEventsFromMask(1)) s.SetAllowCommand(true) + s.SetAllowSubstitution(true) s.SetCreatedAt(tsCreate) s.SetCreatedBy("octocat") s.SetUpdatedAt(tsUpdate) @@ -314,20 +316,21 @@ func TestDatabase_SecretFromLibrary(t *testing.T) { // type with all fields set to a fake value. func testSecret() *Secret { return &Secret{ - ID: sql.NullInt64{Int64: 1, Valid: true}, - Org: sql.NullString{String: "github", Valid: true}, - Repo: sql.NullString{String: "octocat", Valid: true}, - Team: sql.NullString{String: "octokitties", Valid: true}, - Name: sql.NullString{String: "foo", Valid: true}, - Value: sql.NullString{String: "bar", Valid: true}, - Type: sql.NullString{String: "repo", Valid: true}, - Images: []string{"alpine"}, - Events: []string{"push", "tag", "deployment"}, - AllowEvents: sql.NullInt64{Int64: 1, Valid: true}, - AllowCommand: sql.NullBool{Bool: true, Valid: true}, - CreatedAt: sql.NullInt64{Int64: tsCreate, Valid: true}, - CreatedBy: sql.NullString{String: "octocat", Valid: true}, - UpdatedAt: sql.NullInt64{Int64: tsUpdate, Valid: true}, - UpdatedBy: sql.NullString{String: "octocat2", Valid: true}, + ID: sql.NullInt64{Int64: 1, Valid: true}, + Org: sql.NullString{String: "github", Valid: true}, + Repo: sql.NullString{String: "octocat", Valid: true}, + Team: sql.NullString{String: "octokitties", Valid: true}, + Name: sql.NullString{String: "foo", Valid: true}, + Value: sql.NullString{String: "bar", Valid: true}, + Type: sql.NullString{String: "repo", Valid: true}, + Images: []string{"alpine"}, + Events: []string{"push", "tag", "deployment"}, + AllowEvents: sql.NullInt64{Int64: 1, Valid: true}, + AllowCommand: sql.NullBool{Bool: true, Valid: true}, + AllowSubstitution: sql.NullBool{Bool: true, Valid: true}, + CreatedAt: sql.NullInt64{Int64: tsCreate, Valid: true}, + CreatedBy: sql.NullString{String: "octocat", Valid: true}, + UpdatedAt: sql.NullInt64{Int64: tsUpdate, Valid: true}, + UpdatedBy: sql.NullString{String: "octocat2", Valid: true}, } } diff --git a/library/log.go b/library/log.go index 9c2ce146..3c41f429 100644 --- a/library/log.go +++ b/library/log.go @@ -3,8 +3,8 @@ package library import ( + "bytes" "fmt" - "regexp" "github.com/go-vela/types/constants" ) @@ -45,25 +45,14 @@ func (l *Log) AppendData(data []byte) { func (l *Log) MaskData(secrets []string) { data := l.GetData() + // early exit on empty log or secret list + if len(data) == 0 || len(secrets) == 0 { + return + } + + // byte replace data with masked logs for _, secret := range secrets { - // escape regexp meta characters if they exist within value of secret - // - // https://pkg.go.dev/regexp#QuoteMeta - escaped := regexp.QuoteMeta(secret) - - // create regexp to match secrets in the log data surrounded by regexp metacharacters - // - // https://pkg.go.dev/regexp#MustCompile - buffer := `(\s|^|=|"|\?|:|'|\.|,|&|$|;|\[|\])` - re := regexp.MustCompile((buffer + escaped + buffer)) - - // create a mask for the secret - mask := fmt.Sprintf("$1%s$2", constants.SecretLogMask) - - // replace all regexp matches of secret with mask - // - // https://pkg.go.dev/regexp#Regexp.ReplaceAll - data = re.ReplaceAll(data, []byte(mask)) + data = bytes.ReplaceAll(data, []byte(secret), []byte(constants.SecretLogMask)) } // update data field to masked logs diff --git a/library/log_test.go b/library/log_test.go index 5ea64669..e0125507 100644 --- a/library/log_test.go +++ b/library/log_test.go @@ -42,59 +42,83 @@ func TestLibrary_Log_AppendData(t *testing.T) { func TestLibrary_Log_MaskData(t *testing.T) { // set up test secrets - sVals := []string{"secret", "((%.YY245***pP.><@@}}", "littlesecret", "extrasecret"} - - // set up test logs - s1 := "$ echo $NO_SECRET\nnosecret\n" - s2 := "((%.YY245***pP.><@@}}" - s2Masked := "***" - s3 := "$ echo $SECRET1\n((%.YY245***pP.><@@}}\n$ echo $SECRET2\nlittlesecret\n" - s3Masked := "$ echo $SECRET1\n***\n$ echo $SECRET2\n***\n" - s4 := "SOME_SECRET=((%.YY245***pP.><@@}}" - s4Masked := "SOME_SECRET=***" - s5 := "www.example.com?username=secret&password=extrasecret" - s5Masked := "www.example.com?username=***&password=***" - s6 := "[token: extrasecret]" - s6Masked := "[token: ***]" + sVals := []string{"gh_abc123def456", "((%.YY245***pP.><@@}}", "quick-bear-fox-squid", "SUPERSECRETVALUE"} tests := []struct { - want []byte log []byte + want []byte secrets []string }{ { // no secrets in log - want: []byte(s1), - log: []byte(s1), + log: []byte( + "$ echo hello\nhello\n", + ), + want: []byte( + "$ echo hello\nhello\n", + ), secrets: sVals, }, { // one secret in log - want: []byte(s2Masked), - log: []byte(s2), + log: []byte( + "((%.YY245***pP.><@@}}", + ), + want: []byte( + "***", + ), secrets: sVals, }, { // multiple secrets in log - want: []byte(s3Masked), - log: []byte(s3), + log: []byte( + "$ echo $SECRET1\n((%.YY245***pP.><@@}}\n$ echo $SECRET2\nquick-bear-fox-squid\n", + ), + want: []byte( + "$ echo $SECRET1\n***\n$ echo $SECRET2\n***\n", + ), secrets: sVals, }, { // secret with leading = - want: []byte(s4Masked), - log: []byte(s4), + log: []byte( + "SOME_SECRET=((%.YY245***pP.><@@}}", + ), + want: []byte( + "SOME_SECRET=***", + ), secrets: sVals, }, { // secret baked in URL query params - want: []byte(s5Masked), - log: []byte(s5), + log: []byte( + "www.example.com?username=quick-bear-fox-squid&password=SUPERSECRETVALUE", + ), + want: []byte( + "www.example.com?username=***&password=***", + ), secrets: sVals, }, { // secret in verbose brackets - want: []byte(s6Masked), - log: []byte(s6), + log: []byte( + "[token: gh_abc123def456]", + ), + want: []byte( + "[token: ***]", + ), + secrets: sVals, + }, + { // double secret + log: []byte( + "echo ${GITHUB_TOKEN}${SUPER_SECRET}\ngh_abc123def456SUPERSECRETVALUE\n", + ), + want: []byte( + "echo ${GITHUB_TOKEN}${SUPER_SECRET}\n******\n", + ), secrets: sVals, }, { // empty secrets slice - want: []byte(s3), - log: []byte(s3), + log: []byte( + "echo hello\nhello\n", + ), + want: []byte( + "echo hello\nhello\n", + ), secrets: []string{}, }, } diff --git a/library/secret.go b/library/secret.go index 2566b4cd..04e12497 100644 --- a/library/secret.go +++ b/library/secret.go @@ -14,21 +14,22 @@ import ( // // swagger:model Secret type Secret struct { - ID *int64 `json:"id,omitempty"` - Org *string `json:"org,omitempty"` - Repo *string `json:"repo,omitempty"` - Team *string `json:"team,omitempty"` - Name *string `json:"name,omitempty"` - Value *string `json:"value,omitempty"` - Type *string `json:"type,omitempty"` - Images *[]string `json:"images,omitempty"` - Events *[]string `json:"events,omitempty"` - AllowEvents *Events `json:"allow_events,omitempty"` - AllowCommand *bool `json:"allow_command,omitempty"` - CreatedAt *int64 `json:"created_at,omitempty"` - CreatedBy *string `json:"created_by,omitempty"` - UpdatedAt *int64 `json:"updated_at,omitempty"` - UpdatedBy *string `json:"updated_by,omitempty"` + ID *int64 `json:"id,omitempty"` + Org *string `json:"org,omitempty"` + Repo *string `json:"repo,omitempty"` + Team *string `json:"team,omitempty"` + Name *string `json:"name,omitempty"` + Value *string `json:"value,omitempty"` + Type *string `json:"type,omitempty"` + Images *[]string `json:"images,omitempty"` + Events *[]string `json:"events,omitempty"` + AllowEvents *Events `json:"allow_events,omitempty"` + AllowCommand *bool `json:"allow_command,omitempty"` + AllowSubstitution *bool `json:"allow_substitution,omitempty"` + CreatedAt *int64 `json:"created_at,omitempty"` + CreatedBy *string `json:"created_by,omitempty"` + UpdatedAt *int64 `json:"updated_at,omitempty"` + UpdatedBy *string `json:"updated_by,omitempty"` } // Sanitize creates a duplicate of the Secret without the value. @@ -39,21 +40,22 @@ func (s *Secret) Sanitize() *Secret { value := constants.SecretMask return &Secret{ - ID: s.ID, - Org: s.Org, - Repo: s.Repo, - Team: s.Team, - Name: s.Name, - Value: &value, - Type: s.Type, - Images: s.Images, - Events: s.Events, - AllowEvents: s.AllowEvents, - AllowCommand: s.AllowCommand, - CreatedAt: s.CreatedAt, - CreatedBy: s.CreatedBy, - UpdatedAt: s.UpdatedAt, - UpdatedBy: s.UpdatedBy, + ID: s.ID, + Org: s.Org, + Repo: s.Repo, + Team: s.Team, + Name: s.Name, + Value: &value, + Type: s.Type, + Images: s.Images, + Events: s.Events, + AllowEvents: s.AllowEvents, + AllowCommand: s.AllowCommand, + AllowSubstitution: s.AllowSubstitution, + CreatedAt: s.CreatedAt, + CreatedBy: s.CreatedBy, + UpdatedAt: s.UpdatedAt, + UpdatedBy: s.UpdatedBy, } } @@ -69,6 +71,11 @@ func (s *Secret) Match(from *pipeline.Container) bool { return false } + // check if a custom entrypoint is utilized when not allowed + if !commands && len(from.Commands) == 0 && len(from.Entrypoint) > 0 { + return false + } + eACL = s.GetAllowEvents().Allowed( from.Environment["VELA_BUILD_EVENT"], from.Environment["VELA_BUILD_EVENT_ACTION"], @@ -237,6 +244,19 @@ func (s *Secret) GetAllowCommand() bool { return *s.AllowCommand } +// GetAllowSubstitution returns the AllowSubstitution field. +// +// When the provided Secret type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (s *Secret) GetAllowSubstitution() bool { + // return zero value if Secret type or AllowSubstitution field is nil + if s == nil || s.AllowSubstitution == nil { + return false + } + + return *s.AllowSubstitution +} + // GetCreatedAt returns the CreatedAt field. // // When the provided Secret type is nil, or the field within @@ -432,6 +452,19 @@ func (s *Secret) SetAllowCommand(v bool) { s.AllowCommand = &v } +// SetAllowSubstitution sets the AllowSubstitution field. +// +// When the provided Secret type is nil, it +// will set nothing and immediately return. +func (s *Secret) SetAllowSubstitution(v bool) { + // return if Secret type is nil + if s == nil { + return + } + + s.AllowSubstitution = &v +} + // SetCreatedAt sets the CreatedAt field. // // When the provided Secret type is nil, it @@ -489,6 +522,7 @@ func (s *Secret) String() string { return fmt.Sprintf(`{ AllowCommand: %t, AllowEvents: %s, + AllowSubstitution: %t, Events: %s, ID: %d, Images: %s, @@ -505,6 +539,7 @@ func (s *Secret) String() string { }`, s.GetAllowCommand(), s.GetAllowEvents().List(), + s.GetAllowSubstitution(), s.GetEvents(), s.GetID(), s.GetImages(), diff --git a/library/secret_test.go b/library/secret_test.go index 591c1ac5..ebe14549 100644 --- a/library/secret_test.go +++ b/library/secret_test.go @@ -354,6 +354,27 @@ func TestLibrary_Secret_Match(t *testing.T) { }, want: false, }, + { + name: "no commands allowed - entrypoint provided", + step: &pipeline.Container{ + Image: "alpine:latest", + Environment: map[string]string{"VELA_BUILD_EVENT": "push"}, + Ruleset: pipeline.Ruleset{ + If: pipeline.Rules{ + Event: []string{"push"}, + }, + }, + Entrypoint: []string{"sh", "-c", "echo hi"}, + }, + sec: &Secret{ + Name: &v, + Value: &v, + Images: &[]string{"alpine"}, + AllowEvents: testEvents, + AllowCommand: &fBool, + }, + want: false, + }, } // run tests @@ -428,6 +449,10 @@ func TestLibrary_Secret_Getters(t *testing.T) { t.Errorf("GetAllowCommand is %v, want %v", test.secret.GetAllowCommand(), test.want.GetAllowCommand()) } + if test.secret.GetAllowSubstitution() != test.want.GetAllowSubstitution() { + t.Errorf("GetAllowSubstitution is %v, want %v", test.secret.GetAllowSubstitution(), test.want.GetAllowSubstitution()) + } + if test.secret.GetCreatedAt() != test.want.GetCreatedAt() { t.Errorf("GetCreatedAt is %v, want %v", test.secret.GetCreatedAt(), test.want.GetCreatedAt()) } @@ -478,6 +503,7 @@ func TestLibrary_Secret_Setters(t *testing.T) { test.secret.SetEvents(test.want.GetEvents()) test.secret.SetAllowEvents(test.want.GetAllowEvents()) test.secret.SetAllowCommand(test.want.GetAllowCommand()) + test.secret.SetAllowSubstitution(test.want.GetAllowSubstitution()) test.secret.SetCreatedAt(test.want.GetCreatedAt()) test.secret.SetCreatedBy(test.want.GetCreatedBy()) test.secret.SetUpdatedAt(test.want.GetUpdatedAt()) @@ -527,6 +553,10 @@ func TestLibrary_Secret_Setters(t *testing.T) { t.Errorf("SetAllowCommand is %v, want %v", test.secret.GetAllowCommand(), test.want.GetAllowCommand()) } + if test.secret.GetAllowSubstitution() != test.want.GetAllowSubstitution() { + t.Errorf("SetAllowSubstitution is %v, want %v", test.secret.GetAllowSubstitution(), test.want.GetAllowSubstitution()) + } + if test.secret.GetCreatedAt() != test.want.GetCreatedAt() { t.Errorf("SetCreatedAt is %v, want %v", test.secret.GetCreatedAt(), test.want.GetCreatedAt()) } @@ -552,6 +582,7 @@ func TestLibrary_Secret_String(t *testing.T) { want := fmt.Sprintf(`{ AllowCommand: %t, AllowEvents: %v, + AllowSubstitution: %t, Events: %s, ID: %d, Images: %s, @@ -568,6 +599,7 @@ func TestLibrary_Secret_String(t *testing.T) { }`, s.GetAllowCommand(), s.GetAllowEvents().List(), + s.GetAllowSubstitution(), s.GetEvents(), s.GetID(), s.GetImages(), @@ -610,6 +642,7 @@ func testSecret() *Secret { s.SetEvents([]string{"push", "tag", "deployment"}) s.SetAllowEvents(NewEventsFromMask(1)) s.SetAllowCommand(true) + s.SetAllowSubstitution(true) s.SetCreatedAt(tsCreate) s.SetCreatedBy("octocat") s.SetUpdatedAt(tsUpdate)