diff --git a/iq/server.go b/iq/server.go index f60ec2b..209247a 100644 --- a/iq/server.go +++ b/iq/server.go @@ -156,12 +156,14 @@ func (s *NxiqServer) ApplyOrgContents(orgContent scm.OrgContents, rootOrganizati func (s *NxiqServer) createAppsInOrg(org *sonatypeiq.ApiOrganizationDTO, apps []scm.Application) error { if len(apps) > 0 { for _, a := range apps { - app, err := s.CreateApplication(a, *org.Id) + app, scm, err := s.CreateApplication(a, *org.Id) if err != nil { return err } log.Debug(fmt.Sprintf("Created Application %s - %s", a.SafeName(), *app.Id)) - s.scheduleSourceStageScan(app, a.DefaultBranch) + if scm != nil { + s.scheduleSourceStageScan(app, a.DefaultBranch) + } } } return nil @@ -321,16 +323,52 @@ func (s *NxiqServer) UpdateOrganizationScmConfiguration(org *sonatypeiq.ApiOrgan return nil } -func (s *NxiqServer) CreateApplication(app scm.Application, parentOrgId string) (*sonatypeiq.ApiApplicationDTO, error) { +func (s *NxiqServer) CreateApplication(app scm.Application, parentOrgId string) (*sonatypeiq.ApiApplicationDTO, *sonatypeiq.ApiSourceControlDTO, error) { existingApp, err := s.ApplicationExists(app, parentOrgId) if err != nil { log.Debug(fmt.Sprintf("Failed to determine if Application %s already exists", app.Name)) - return nil, err + return nil, nil, err } + var scmDto *sonatypeiq.ApiSourceControlDTO if existingApp != nil { // Update SCM Configuration - _, r, err := s.apiClient.SourceControlAPI.UpdateSourceControl(*s.apiContext, "application", *existingApp.Id).ApiSourceControlDTO(sonatypeiq.ApiSourceControlDTO{ + if app.IsRepositoryUrlPermitted() && app.IsBranchNamePermitted() { + scmDto, r, err := s.apiClient.SourceControlAPI.UpdateSourceControl(*s.apiContext, "application", *existingApp.Id).ApiSourceControlDTO(sonatypeiq.ApiSourceControlDTO{ + RepositoryUrl: &app.RepositoryUrl, + BaseBranch: app.DefaultBranch, + EnablePullRequests: nil, + RemediationPullRequestsEnabled: nil, + PullRequestCommentingEnabled: nil, + SourceControlEvaluationsEnabled: nil, + SshEnabled: nil, + }).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `SourceControlAPI.UpdateSourceControl``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + return nil, nil, err + } + return existingApp, scmDto, nil + } else { + log.Warn(fmt.Sprintf("Application %s has an unsupported Default Branch or Repository URL '%s' and will not have SCM configuration saved into Sonatype", app.Name, app.RepositoryUrl)) + } + } + + createdApp, err := s.createApplication(app, parentOrgId) + if err != nil { + return nil, nil, err + } + log.Debug(fmt.Sprintf("Created App: %s (%s)", *createdApp.Name, *createdApp.Id)) + + // Set SCM Configuration + if app.IsRepositoryUrlPermitted() && app.IsBranchNamePermitted() { + log.Debug( + fmt.Sprintf( + "APP Source Control. URL: '%s' %v, Branch: '%s' %v", + app.RepositoryUrl, app.IsRepositoryUrlPermitted(), *app.DefaultBranch, app.IsBranchNamePermitted(), + ), + ) + scmDto, r, err := s.apiClient.SourceControlAPI.AddSourceControl(*s.apiContext, "application", *createdApp.Id).ApiSourceControlDTO(sonatypeiq.ApiSourceControlDTO{ RepositoryUrl: &app.RepositoryUrl, BaseBranch: app.DefaultBranch, EnablePullRequests: nil, @@ -340,36 +378,16 @@ func (s *NxiqServer) CreateApplication(app scm.Application, parentOrgId string) SshEnabled: nil, }).Execute() if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `SourceControlAPI.UpdateSourceControl``: %v\n", err) + fmt.Fprintf(os.Stderr, "Error when calling `SourceControlAPI.AddSourceControl``: %v\n", err) fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) - return nil, err + return nil, nil, err } - return existingApp, nil + return createdApp, scmDto, nil + } else { + log.Warn(fmt.Sprintf("Application %s has an unsupported Default Branch or Repository URL '%s' and will not have SCM configuration saved into Sonatype", app.Name, app.RepositoryUrl)) } - createdApp, err := s.createApplication(app, parentOrgId) - if err != nil { - return nil, err - } - log.Debug(fmt.Sprintf("Created App: %v", createdApp)) - - // Set SCM Configuration - _, r, err := s.apiClient.SourceControlAPI.AddSourceControl(*s.apiContext, "application", *createdApp.Id).ApiSourceControlDTO(sonatypeiq.ApiSourceControlDTO{ - RepositoryUrl: &app.RepositoryUrl, - BaseBranch: app.DefaultBranch, - EnablePullRequests: nil, - RemediationPullRequestsEnabled: nil, - PullRequestCommentingEnabled: nil, - SourceControlEvaluationsEnabled: nil, - SshEnabled: nil, - }).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `SourceControlAPI.AddSourceControl``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) - return nil, err - } - - return createdApp, nil + return createdApp, scmDto, nil } func (s *NxiqServer) ApplicationExists(app scm.Application, parentOrgId string) (*sonatypeiq.ApiApplicationDTO, error) { diff --git a/scm/types.go b/scm/types.go index 8d4f76b..9674682 100644 --- a/scm/types.go +++ b/scm/types.go @@ -18,8 +18,11 @@ package scm import ( "fmt" + "net/url" "regexp" "strings" + + log "github.com/sirupsen/logrus" ) const ( @@ -28,7 +31,10 @@ const ( SCM_TYPE_AZURE = "azure" ) -var MULTIPLE_SPACES = regexp.MustCompile(`\s(\s+)`) +var ( + INVALID_BRANCH_NAME = regexp.MustCompile(`^\.|([;$!*&|\(\)\[\]<>#?~%'])|[\./]$`) + MULTIPLE_SPACES = regexp.MustCompile(`\s(\s+)`) +) type ScmConfiguration struct { Type string @@ -60,6 +66,33 @@ func (a *Application) SafeName() string { return safeName(a.Name) } +func (a *Application) IsBranchNamePermitted() bool { + if a.DefaultBranch != nil { + return safeBranchName(*a.DefaultBranch) + } + return false +} + +func (a *Application) IsRepositoryUrlPermitted() bool { + return safeRepositoryUrl(a.RepositoryUrl) +} + +func safeBranchName(in string) bool { + if strings.TrimSpace(in) == "" { + return false + } + return !INVALID_BRANCH_NAME.MatchString(in) +} + +func safeRepositoryUrl(in string) bool { + decoded, err := url.QueryUnescape(in) + if err != nil { + log.Warn(fmt.Sprintf("Failed to URL decode Repository URL: %s", in)) + return false + } + return !INVALID_BRANCH_NAME.MatchString(decoded) +} + type Organization struct { Name string ScmProvider string diff --git a/scm/types_test.go b/scm/types_test.go index 55b0923..7b292c8 100644 --- a/scm/types_test.go +++ b/scm/types_test.go @@ -23,6 +23,103 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSafeBranchNameNil(t *testing.T) { + var nil string + assert.Equal(t, false, safeBranchName(nil)) +} + +func TestSafeBranchName(t *testing.T) { + + cases := []struct { + input string + permitted bool + }{ + { + input: "main", + permitted: true, + }, + { + input: "master", + permitted: true, + }, + { + input: "with(bracket", + permitted: false, + }, + { + input: "with&mpersand", + permitted: false, + }, + { + input: "ma?in", + permitted: false, + }, + { + input: "pi|pe", + permitted: false, + }, + { + input: "give;injection", + permitted: false, + }, + { + input: "give~injection", + permitted: false, + }, + { + input: "give%injection", + permitted: false, + }, + { + input: "give'injection", + permitted: false, + }, + { + input: ".start-period", + permitted: false, + }, + { + input: "end-period.", + permitted: false, + }, + { + input: "end-slash/", + permitted: false, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("TestSafeBranchName-%d-%s", i, tc.input), func(t *testing.T) { + assert.Equal(t, tc.permitted, safeBranchName(tc.input)) + }) + } +} + +func TestSafeRepositoryUrl(t *testing.T) { + + cases := []struct { + input string + permitted bool + }{ + { + input: "https://REDACTED@dev.azure.com/REDACTED/Scan-Test-1/_git/main", + permitted: true, + }, { + input: "https://REDACTED@dev.azure.com/REDACTED/Scan-Test-1/_git/Craz%28%29y%20Repo", + permitted: false, + }, { + input: "https://dev.azure.com/PHorton0655/Scan-Test-1/_git/Craz%28%29y%20Repo", + permitted: false, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("TestSafeRepositoryUrl-%d-%s", i, tc.input), func(t *testing.T) { + assert.Equal(t, tc.permitted, safeRepositoryUrl(tc.input)) + }) + } +} + func TestScmSafeName(t *testing.T) { cases := []struct {