diff --git a/main.go b/main.go index ebddfaa..0ca1939 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/base64" + "encoding/json" "errors" "flag" "fmt" @@ -21,10 +22,11 @@ import ( ) var ( - token string - orgs stringSlice - repo string - owner bool + token string + orgs stringSlice + repo string + owner bool + jsonOut bool debug bool ) @@ -57,6 +59,7 @@ func main() { p.FlagSet.Var(&orgs, "orgs", "specific orgs to check (e.g. 'genuinetools')") p.FlagSet.StringVar(&repo, "repo", "", "specific repo to test (e.g. 'genuinetools/audit')") p.FlagSet.BoolVar(&owner, "owner", false, "only audit repos the token owner owns") + p.FlagSet.BoolVar(&jsonOut, "json", false, "output as json") p.FlagSet.BoolVar(&debug, "d", false, "enable debug logging") p.FlagSet.BoolVar(&debug, "debug", false, "enable debug logging") @@ -243,49 +246,50 @@ func getRepositories(ctx context.Context, restClient *github.Client, graphqlClie return nil } -// handleRepo will return nil error if the user does not have access to something. -func handleRepo(ctx context.Context, restClient *github.Client, repo ghrepo) error { - opt := &github.ListOptions{ - PerPage: 100, - } - - logrus.Debugf("Executing REST query to list teams for %s", repo.NameWithOwner) - teams, resp, err := restClient.Repositories.ListTeams(ctx, repo.Owner.Login, repo.Name, opt) - if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden || err != nil { - if _, ok := err.(*github.RateLimitError); ok { - return err - } - - return nil - } - if err != nil { - return err - } +type outCollaborator struct { + Login string `json:"login"` + Teams []string `json:"teams"` +} - logrus.Debugf("Executing REST query to list hooks for %s", repo.NameWithOwner) - hooks, resp, err := restClient.Repositories.ListHooks(ctx, repo.Owner.Login, repo.Name, opt) - if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden || err != nil { - if _, ok := err.(*github.RateLimitError); ok { - return err - } +type outCollaborators struct { + TotalCount int `json:"totalCount"` + Admin []outCollaborator `json:"admin"` + Write []outCollaborator `json:"write"` + Read []outCollaborator `json:"read"` +} - return nil - } - if err != nil { - return err - } +type outDeployKey struct { + Title string `json:"title"` + ReadOnly bool `json:"readOnly"` + URL string `json:"url"` +} - // only print whole status if we have more that one collaborator - if repo.Collaborators.TotalCount <= 1 && repo.DeployKeys.TotalCount < 1 && len(hooks) < 1 && repo.BranchProtectionRules.TotalCount < 1 && repo.Refs.TotalCount < 1 { - return nil - } +type outHook struct { + Name string `json:"name"` + Active bool `json:"active"` + URL string `json:"url"` +} - output := fmt.Sprintf("%s -> \n", repo.NameWithOwner) +type output struct { + Name string `json:"name"` + Collaborators outCollaborators `json:"collaborators"` + DeployKeys []outDeployKey `json:"deployKeys"` + Hooks []outHook `json:"hooks"` + ProtectedBranches []string `json:"protectedBranches"` + UnprotectedBranches []string `json:"unprotectedBranches"` + MergeMethods []string `json:"mergeMethods"` +} +func audit(ctx context.Context, restClient *github.Client, repo ghrepo, teams []*github.Team, ghhooks []*github.Hook) output { + admin := []outCollaborator{} + write := []outCollaborator{} + read := []outCollaborator{} + deployKeys := []outDeployKey{} + hooks := []outHook{} + protectedBranches := []string{} + unprotectedBranches := []string{} + mergeMethods := []string{} if repo.Collaborators.TotalCount > 1 { - push := []string{} - pull := []string{} - admin := []string{} logrus.Debugf("Executing REST query to check collaborators' team memberships for %s", repo.NameWithOwner) for _, c := range repo.Collaborators.Edges { userTeams := []github.Team{} @@ -304,72 +308,183 @@ func handleRepo(ctx context.Context, restClient *github.Client, repo ghrepo) err permTeams = append(permTeams, t.GetName()) } } - admin = append(admin, fmt.Sprintf("\t\t\t%s (teams: %s)", c.Node.Login, strings.Join(permTeams, ", "))) + admin = append(admin, outCollaborator{c.Node.Login, permTeams}) case "WRITE": - push = append(push, fmt.Sprintf("\t\t\t%s", c.Node.Login)) + permTeams := []string{} + for _, t := range userTeams { + if t.GetPermission() == "push" { + permTeams = append(permTeams, t.GetName()) + } + } + write = append(write, outCollaborator{c.Node.Login, permTeams}) case "READ": - pull = append(pull, fmt.Sprintf("\t\t\t%s", c.Node.Login)) + permTeams := []string{} + for _, t := range userTeams { + if t.GetPermission() == "pull" { + permTeams = append(permTeams, t.GetName()) + } + } + read = append(read, outCollaborator{c.Node.Login, permTeams}) } } - output += fmt.Sprintf("\tCollaborators (%d):\n", repo.Collaborators.TotalCount) - output += fmt.Sprintf("\t\tAdmin (%d):\n%s\n", len(admin), strings.Join(admin, "\n")) - output += fmt.Sprintf("\t\tWrite (%d):\n%s\n", len(push), strings.Join(push, "\n")) - output += fmt.Sprintf("\t\tRead (%d):\n%s\n", len(pull), strings.Join(pull, "\n")) } if repo.DeployKeys.TotalCount > 0 { - kstr := []string{} for _, k := range repo.DeployKeys.Nodes { keyURL, err := buildDeployKeyURL(repo.Owner.Login, repo.Name, k.ID) if err != nil { - kstr = append(kstr, fmt.Sprintf("\t\t%s - ro:%t", k.Title, k.ReadOnly)) + deployKeys = append(deployKeys, outDeployKey{k.Title, k.ReadOnly, ""}) } else { - kstr = append(kstr, fmt.Sprintf("\t\t%s - ro:%t (%s)", k.Title, k.ReadOnly, keyURL)) + deployKeys = append(deployKeys, outDeployKey{k.Title, k.ReadOnly, keyURL}) } } - output += fmt.Sprintf("\tKeys (%d):\n%s\n", repo.DeployKeys.TotalCount, strings.Join(kstr, "\n")) } - if len(hooks) > 0 { - hstr := []string{} - for _, h := range hooks { - hstr = append(hstr, fmt.Sprintf("\t\t%s - active:%t (%s)", h.GetName(), h.GetActive(), h.GetURL())) + if len(ghhooks) > 0 { + for _, h := range ghhooks { + hooks = append(hooks, outHook{h.GetName(), h.GetActive(), h.GetURL()}) } - output += fmt.Sprintf("\tHooks (%d):\n%s\n", len(hstr), strings.Join(hstr, "\n")) } if repo.BranchProtectionRules.TotalCount > 0 { - protectedBranches := []string{} for _, r := range repo.BranchProtectionRules.Nodes { protectedBranches = append(protectedBranches, r.Pattern) } - output += fmt.Sprintf("\tProtected Branches (%d): %s\n", len(protectedBranches), strings.Join(protectedBranches, ", ")) } if repo.Refs.TotalCount > 0 { - unprotectedBranches := []string{} for _, r := range repo.Refs.Nodes { unprotectedBranches = append(unprotectedBranches, r.Name) } - output += fmt.Sprintf("\tUnprotected Branches (%d): %s\n", len(unprotectedBranches), strings.Join(unprotectedBranches, ", ")) } - mergeMethods := "\tMerge Methods:" if repo.MergeCommitAllowed { - mergeMethods += " mergeCommit" + mergeMethods = append(mergeMethods, "mergeCommit") } if repo.SquashMergeAllowed { - mergeMethods += " squash" + mergeMethods = append(mergeMethods, "squash") } if repo.RebaseMergeAllowed { - mergeMethods += " rebase" + mergeMethods = append(mergeMethods, "rebase") + } + + return output{ + repo.NameWithOwner, + outCollaborators{repo.Collaborators.TotalCount, admin, write, read}, + deployKeys, + hooks, + protectedBranches, + unprotectedBranches, + mergeMethods, + } +} + +func outputText(o output) { + text := fmt.Sprintf("%s -> \n", o.Name) + + if o.Collaborators.TotalCount > 1 { + write := []string{} + read := []string{} + admin := []string{} + for _, c := range o.Collaborators.Admin { + admin = append(admin, fmt.Sprintf("\t\t\t%s (teams: %s)", c.Login, strings.Join(c.Teams, ", "))) + } + for _, c := range o.Collaborators.Write { + write = append(write, fmt.Sprintf("\t\t\t%s", c.Login)) + } + for _, c := range o.Collaborators.Read { + read = append(read, fmt.Sprintf("\t\t\t%s", c.Login)) + } + text += fmt.Sprintf("\tCollaborators (%d):\n", o.Collaborators.TotalCount) + text += fmt.Sprintf("\t\tAdmin (%d):\n%s\n", len(admin), strings.Join(admin, "\n")) + text += fmt.Sprintf("\t\tWrite (%d):\n%s\n", len(write), strings.Join(write, "\n")) + text += fmt.Sprintf("\t\tRead (%d):\n%s\n", len(read), strings.Join(read, "\n")) + } + + if len(o.DeployKeys) > 0 { + kstr := []string{} + for _, k := range o.DeployKeys { + if k.URL == "" { + kstr = append(kstr, fmt.Sprintf("\t\t%s - ro:%t", k.Title, k.ReadOnly)) + } else { + kstr = append(kstr, fmt.Sprintf("\t\t%s - ro:%t (%s)", k.Title, k.ReadOnly, k.URL)) + } + } + text += fmt.Sprintf("\tKeys (%d):\n%s\n", len(kstr), strings.Join(kstr, "\n")) + } + + if len(o.Hooks) > 0 { + hstr := []string{} + for _, h := range o.Hooks { + hstr = append(hstr, fmt.Sprintf("\t\t%s - active:%t (%s)", h.Name, h.Active, h.URL)) + } + text += fmt.Sprintf("\tHooks (%d):\n%s\n", len(hstr), strings.Join(hstr, "\n")) + } + + if len(o.ProtectedBranches) > 0 { + text += fmt.Sprintf("\tProtected Branches (%d): %s\n", len(o.ProtectedBranches), strings.Join(o.ProtectedBranches, ", ")) + } + + if len(o.UnprotectedBranches) > 0 { + text += fmt.Sprintf("\tUnprotected Branches (%d): %s\n", len(o.UnprotectedBranches), strings.Join(o.UnprotectedBranches, ", ")) + } + text += fmt.Sprintf("\tMerge Methods: %s\n", strings.Join(o.MergeMethods, " ")) + + logrus.Debugf("Printing details for %s", o.Name) + + fmt.Printf("%s--\n\n", text) +} + +func outputJSON(o output) { + b, err := json.Marshal(o) + if err == nil { + fmt.Printf("%s\n", b) + } +} + +// handleRepo will return nil error if the user does not have access to something. +func handleRepo(ctx context.Context, restClient *github.Client, repo ghrepo) error { + opt := &github.ListOptions{ + PerPage: 100, + } + + logrus.Debugf("Executing REST query to list teams for %s", repo.NameWithOwner) + teams, resp, err := restClient.Repositories.ListTeams(ctx, repo.Owner.Login, repo.Name, opt) + if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden || err != nil { + if _, ok := err.(*github.RateLimitError); ok { + return err + } + + return nil + } + if err != nil { + return err } - output += mergeMethods + "\n" - logrus.Debugf("Printing details for %s", repo.NameWithOwner) + logrus.Debugf("Executing REST query to list hooks for %s", repo.NameWithOwner) + hooks, resp, err := restClient.Repositories.ListHooks(ctx, repo.Owner.Login, repo.Name, opt) + if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden || err != nil { + if _, ok := err.(*github.RateLimitError); ok { + return err + } + + return nil + } + if err != nil { + return err + } - fmt.Printf("%s--\n\n", output) + // only print whole status if we have more that one collaborator + if repo.Collaborators.TotalCount <= 1 && repo.DeployKeys.TotalCount < 1 && len(hooks) < 1 && repo.BranchProtectionRules.TotalCount < 1 && repo.Refs.TotalCount < 1 { + return nil + } + output := audit(ctx, restClient, repo, teams, hooks) + if jsonOut { + outputJSON(output) + } else { + outputText(output) + } return nil }