Skip to content

Commit

Permalink
feat: runner group policies (#31)
Browse files Browse the repository at this point in the history
* added runner group collection and a runner group policy
  • Loading branch information
noamd-legit committed Nov 6, 2022
1 parent b73f153 commit 7ac8190
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 40 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ go run main.go analyze ...
```
admin:org, read:enterprise, admin:org_hook, read:org, repo, read:repo_hook
```
See [Creating a Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) for more info.
See [Creating a Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
or [Introducing fine-grained personal access tokens](https://github.blog/changelog/2022-10-18-introducing-fine-grained-personal-access-tokens/) for more information

## Usage
```
Expand All @@ -56,6 +57,7 @@ Currently, the following namespaces are supported:
2. `actions` - organization GitHub Actions policies (e.g., "GitHub Actions Runs Are Not Limited To Verified Actions")
3. `member` - organization members policies (e.g., "Stale Admin Found")
4. `repository` - repository level policies (e.g., "Code Review By At Least Two Reviewers Is Not Enforced")
5. `runner_group` - runner group policies (e.g, "runner can be used by public repositories")

By default, legitify will analyze all namespaces. You can limit only to selected ones with the `--namespace` flag, and then a comma separated list of the selected namespaces.

Expand Down Expand Up @@ -131,4 +133,4 @@ Here are some resources to help you get started:
- [Contribution Guide](https://github.com/Legit-Labs/legitify/blob/main/CONTRIBUTING.md)
- [Code of Conduct](https://github.com/Legit-Labs/legitify/blob/main/CODE_OF_CONDUCT.md)
- [Open an Issue](https://github.com/Legit-Labs/legitify/issues/new/choose)
- [Open a Pull Request](https://github.com/Legit-Labs/legitify/compare)
- [Open a Pull Request](https://github.com/Legit-Labs/legitify/compare)
4 changes: 2 additions & 2 deletions cmd/list_orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ func executeListOrgsCommand(cmd *cobra.Command, _args []string) error {
if len(owner) > 0 {
fmt.Println("Full analysis available for the following organizations:")
for _, org := range owner {
fmt.Printf(" - %s (%s)\n", *org.Login, org.Role)
fmt.Printf(" - %s (%s)\n", org.Name(), org.Role)
}
}

if len(member) > 0 {
fmt.Println("Partial results available for the following organizations:")
for _, org := range member {
fmt.Printf(" - %s (%s)\n", *org.Login, org.Role)
fmt.Printf(" - %s (%s)\n", org.Name(), org.Role)
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions internal/collected/github/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ func NewExtendedOrg(org *github.Organization, role permissions.OrganizationRole)
return ExtendedOrg{*org, role}
}

func (e ExtendedOrg) CanonicalLink() string {
return e.GetHTMLURL()
}

func (e ExtendedOrg) Name() string {
return *e.Login
}

func (e ExtendedOrg) IsEnterprise() bool {
const orgPlanEnterprise = "enterprise"
if e.Plan == nil {
Expand Down Expand Up @@ -44,11 +52,11 @@ func (o Organization) ViolationEntityType() string {
}

func (o Organization) CanonicalLink() string {
return *o.Organization.HTMLURL
return o.Organization.CanonicalLink()
}

func (o Organization) Name() string {
return *o.Organization.Login
return o.Organization.Name()
}

func (o Organization) ID() int64 {
Expand Down
28 changes: 28 additions & 0 deletions internal/collected/github/runner_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package githubcollected

import (
"fmt"
"github.com/google/go-github/v44/github"
)

type RunnerGroup struct {
Organization ExtendedOrg `json:"organization"`
RunnerGroup *github.RunnerGroup `json:"runner_group"`
}

func (o RunnerGroup) ViolationEntityType() string {
return "runners group"
}

func (o RunnerGroup) CanonicalLink() string {
const linkTemplate = "https://github.com/organizations/%s/settings/actions/runner-groups/%d"
return fmt.Sprintf(linkTemplate, *o.Organization.Login, *o.RunnerGroup.ID)
}

func (o RunnerGroup) Name() string {
return *o.RunnerGroup.Name
}

func (o RunnerGroup) ID() int64 {
return *o.RunnerGroup.ID
}
6 changes: 3 additions & 3 deletions internal/collectors/actions_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ func (c *actionCollector) Collect() subCollectorChannels {
c.totalCollectionChange(len(orgs))

for _, org := range orgs {
actionsData, _, err := c.client.Client().Organizations.GetActionsPermissions(c.context, *org.Login)
actionsData, _, err := c.client.Client().Organizations.GetActionsPermissions(c.context, org.Name())

if err != nil {
entityName := fmt.Sprintf("%s/%s", namespace.Organization, *org.Login)
entityName := fmt.Sprintf("%s/%s", namespace.Organization, org.Name())
perm := newMissingPermission(permissions.OrgAdmin, entityName, orgActionPermEffect, namespace.Organization)
c.issueMissingPermissions(perm)
}
Expand All @@ -74,7 +74,7 @@ func (c *actionCollector) Collect() subCollectorChannels {
Organization: org,
ActionsPermissions: actionsData,
},
*org.HTMLURL,
org.CanonicalLink(),
[]permissions.Role{org.Role})
}
})
Expand Down
29 changes: 12 additions & 17 deletions internal/collectors/collectors_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ type CollectorManager interface {
}

type manager struct {
collectorCreators []newCollectorFunc
ctx context.Context
client github.Client
collectors []collector
ctx context.Context
client github.Client
}

type newCollectorFunc func(ctx context.Context, client github.Client) collector
Expand All @@ -31,18 +31,19 @@ var collectorsMapping = map[namespace.Namespace]newCollectorFunc{
namespace.Organization: newOrganizationCollector,
namespace.Member: newMemberCollector,
namespace.Actions: newActionCollector,
namespace.RunnerGroup: newRunnersCollector,
}

func NewCollectorsManager(ctx context.Context, ns []namespace.Namespace, client github.Client) CollectorManager {
var selected []newCollectorFunc
var collectors []collector
for _, n := range ns {
selected = append(selected, collectorsMapping[n])
collectors = append(collectors, collectorsMapping[n](ctx, client))
}

return &manager{
collectorCreators: selected,
ctx: ctx,
client: client,
ctx: ctx,
client: client,
collectors: collectors,
}
}

Expand All @@ -53,9 +54,8 @@ func (m *manager) CollectMetadata() map[namespace.Namespace]Metadata {
}

gw := group_waiter.New()
ch := make(chan metaDataPair, len(m.collectorCreators))
for _, creator := range m.collectorCreators {
c := m.createCollector(creator)
ch := make(chan metaDataPair, len(m.collectors))
for _, c := range m.collectors {
gw.Do(func() {
ch <- metaDataPair{Namespace: c.Namespace(), Metadata: c.CollectMetadata()}
})
Expand All @@ -71,10 +71,6 @@ func (m *manager) CollectMetadata() map[namespace.Namespace]Metadata {
return res
}

func (m *manager) createCollector(creator newCollectorFunc) collector {
return creator(m.ctx, m.client)
}

func (m *manager) Collect() CollectorChannels {
collectedChan := make(chan CollectedData)
progressChan := make(chan CollectionMetric)
Expand All @@ -90,8 +86,7 @@ func (m *manager) Collect() CollectorChannels {
})

gw := group_waiter.New()
for _, creator := range m.collectorCreators {
c := m.createCollector(creator)
for _, c := range m.collectors {
collectionChannels := c.Collect()

gw.Do(func() {
Expand Down
12 changes: 6 additions & 6 deletions internal/collectors/members_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (c *memberCollector) CollectMetadata() Metadata {
org := org
gw.Do(func() {
variables := map[string]interface{}{
"login": githubv4.String(*org.Login),
"login": githubv4.String(org.Name()),
}

totalCountQuery := totalCountMembersQuery{}
Expand Down Expand Up @@ -97,7 +97,7 @@ func (c *memberCollector) Collect() subCollectorChannels {
c.issueMissingPermissions(missingPermissions...)

for _, memberType := range []string{"member", "admin"} {
res := c.collectMembers(*org.Login, memberType)
res := c.collectMembers(org.Name(), memberType)
c.collectionChange(len(res))

if !hasLastActive {
Expand All @@ -117,7 +117,7 @@ func (c *memberCollector) Collect() subCollectorChannels {
Members: enrichedMembers,
HasLastActive: hasLastActive,
},
*org.HTMLURL,
org.CanonicalLink(),
[]permissions.Role{org.Role})
}
})
Expand All @@ -130,7 +130,7 @@ func (c *memberCollector) enrichMembers(org *ghcollected.ExtendedOrg, members []
for _, member := range members {
localMember := member
gw.Do(func() {
memberLastActive, err := c.collectMemberLastActiveTime(*org.Login, *localMember.Login)
memberLastActive, err := c.collectMemberLastActiveTime(org.Name(), *localMember.Login)
if err != nil {
perm := c.memberMissingPermission(org, localMember)
c.issueMissingPermissions(perm)
Expand Down Expand Up @@ -210,13 +210,13 @@ const (
)

func (c *memberCollector) memberMissingPermission(org *ghcollected.ExtendedOrg, member *github.User) missingPermission {
entityName := fmt.Sprintf("%s (%s)", *member.Login, *org.Login)
entityName := fmt.Sprintf("%s (%s)", *member.Login, org.Name())
return newMissingPermission(permissions.OrgAdmin, entityName, orgMemberLastActiveEffect, namespace.Member)
}

func (c *memberCollector) checkOrgMissingPermissions(org ghcollected.ExtendedOrg) []missingPermission {
missingPermissions := make([]missingPermission, 0)
entityName := *org.Login
entityName := org.Name()

if org.Plan == nil {
perm := newMissingPermission(permissions.OrgRead, entityName, orgInfoEffect, namespace.Organization)
Expand Down
8 changes: 4 additions & 4 deletions internal/collectors/organization_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,17 @@ func (c *organizationCollector) Collect() subCollectorChannels {
}

func (c *organizationCollector) collectExtraData(org *ghcollected.ExtendedOrg) ghcollected.Organization {
samlEnabled, err := c.collectOrgSamlData(*org.Login)
samlEnabled, err := c.collectOrgSamlData(org.Name())

if err != nil {
samlEnabled = nil
log.Printf("failed to collect saml data for %s, %s", *org.Login, err)
log.Printf("failed to collect saml data for %s, %s", org.Name(), err)
}

hooks, err := c.collectOrgWebhooks(*org.Login)
hooks, err := c.collectOrgWebhooks(org.Name())
if err != nil {
hooks = nil
log.Printf("failed to collect webhooks data for %s, %s", *org.Login, err)
log.Printf("failed to collect webhooks data for %s, %s", org.Name(), err)
}

return ghcollected.Organization{
Expand Down
6 changes: 3 additions & 3 deletions internal/collectors/repository_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (rc *repositoryCollector) CollectMetadata() Metadata {
org := org
gw.Do(func() {
variables := map[string]interface{}{
"login": githubv4.String(*org.Login),
"login": githubv4.String(org.Name()),
}

totalCountQuery := totalCountRepoQuery{}
Expand Down Expand Up @@ -206,7 +206,7 @@ type repoQuery struct {

func (rc *repositoryCollector) collectRepositories(org *ghcollected.ExtendedOrg) error {
variables := map[string]interface{}{
"login": githubv4.String(*org.Login),
"login": githubv4.String(org.Name()),
"repositoryCursor": (*githubv4.String)(nil),
}

Expand All @@ -225,7 +225,7 @@ func (rc *repositoryCollector) collectRepositories(org *ghcollected.ExtendedOrg)
for i := range nodes {
node := &(nodes[i])
extraGw.Do(func() {
rc.collectRepository(node, *org.Login, rc.contextFactory.newRepositoryContextForExtendedOrg(org, node))
rc.collectRepository(node, org.Name(), rc.contextFactory.newRepositoryContextForExtendedOrg(org, node))
})
}
extraGw.Wait()
Expand Down
105 changes: 105 additions & 0 deletions internal/collectors/runner_group_collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package collectors

import (
ghclient "github.com/Legit-Labs/legitify/internal/clients/github"
ghcollected "github.com/Legit-Labs/legitify/internal/collected/github"
"github.com/Legit-Labs/legitify/internal/common/group_waiter"
"github.com/Legit-Labs/legitify/internal/common/namespace"
"github.com/Legit-Labs/legitify/internal/common/permissions"
"github.com/google/go-github/v44/github"
"golang.org/x/net/context"
"log"
"sync"
)

type runnersCollector struct {
baseCollector
client ghclient.Client
context context.Context
cache map[string][]*github.RunnerGroup
}

func newRunnersCollector(ctx context.Context, client ghclient.Client) collector {
c := &runnersCollector{
client: client,
context: ctx,
cache: make(map[string][]*github.RunnerGroup),
}
initBaseCollector(&c.baseCollector, c)
return c
}

func (c *runnersCollector) Namespace() namespace.Namespace {
return namespace.RunnerGroup
}

func (c *runnersCollector) CollectMetadata() Metadata {
gw := group_waiter.New()
orgs, err := c.client.CollectOrganizations()
if err != nil {
log.Printf("failed to collection organizations %s", err)
return Metadata{}
}

totalCount := 0
var mutex = &sync.RWMutex{}
for _, org := range orgs {
gw.Do(func() {
org := org
result := make([]*github.RunnerGroup, 0)
err := ghclient.PaginateResults(func(opts *github.ListOptions) (*github.Response, error) {
runners, resp, err := c.client.Client().Actions.ListOrganizationRunnerGroups(c.context, org.Name(), opts)

if err != nil {
log.Printf("error collecting runner groups for %s - %v", org.Name(), err)
return nil, err
}

result = append(result, runners.RunnerGroups...)
return resp, nil
})

if err != nil {
c.issueMissingPermissions(newMissingPermission(permissions.OrgAdmin, org.Name(),
"Cannot read organization runner groups", namespace.RunnerGroup))
} else {
mutex.Lock()
c.cache[org.Name()] = result
totalCount = totalCount + len(result)
mutex.Unlock()
}
})
}

gw.Wait()
return Metadata{
totalCount,
}
}

func (c *runnersCollector) Collect() subCollectorChannels {
return c.wrappedCollection(func() {
orgs, err := c.client.CollectOrganizations()

if err != nil {
log.Printf("failed to collect organizations %s", err)
return
}

for _, org := range orgs {
cached := c.cache[org.Name()]

for _, rg := range cached {
c.collectionChangeByOne()

c.collectData(org,
ghcollected.RunnerGroup{
Organization: org,
RunnerGroup: rg,
},
org.CanonicalLink(),
[]permissions.Role{org.Role})
}
}
})
}

0 comments on commit 7ac8190

Please sign in to comment.