Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added stale secret policies for repo and org #306

Merged
merged 14 commits into from
May 15, 2024
34 changes: 27 additions & 7 deletions internal/clients/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ import (
"bytes"
"context"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"sync"

"github.com/Legit-Labs/legitify/internal/clients/github/pagination"
"github.com/Legit-Labs/legitify/internal/clients/github/transport"
"github.com/Legit-Labs/legitify/internal/clients/github/types"
Expand All @@ -18,6 +12,11 @@ import (
"github.com/Legit-Labs/legitify/internal/common/slice_utils"
commontypes "github.com/Legit-Labs/legitify/internal/common/types"
"github.com/Legit-Labs/legitify/internal/screen"
"log"
"net/http"
"regexp"
"strings"
"sync"

githubcollected "github.com/Legit-Labs/legitify/internal/collected/github"
"github.com/Legit-Labs/legitify/internal/common/permissions"
Expand Down Expand Up @@ -83,7 +82,6 @@ func (c *Client) initClients(ctx context.Context, token string) error {

var ghClient *gh.Client
var graphQLClient *githubv4.Client

rawClient, graphQLRawClient, err := newHttpClients(ctx, token)
if err != nil {
return err
Expand Down Expand Up @@ -636,3 +634,25 @@ func (c *Client) GetSecurityAndAnalysisForEnterprise(enterprise string) (*types.
}
return &p, nil
}

func (c *Client) GetRepositorySecrets(repo, owner string) (*gh.Secrets, error) {
secrets, res, err := c.client.Actions.ListRepoSecrets(c.context, owner, repo, nil)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("unexpected HTTP status: %d", res.StatusCode)
}
return secrets, nil
}

func (c *Client) GetOrganizationSecrets(org string) (*gh.Secrets, error) {
secrets, res, err := c.client.Actions.ListOrgSecrets(c.context, org, nil)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("unexpected HTTP status: %d", res.StatusCode)
}
return secrets, nil
}
6 changes: 6 additions & 0 deletions internal/collected/github/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ type Organization struct {
SamlEnabled *bool `json:"saml_enabled,omitempty"`
Hooks []*github.Hook `json:"hooks"`
UserRole permissions.OrganizationRole
OrgSecrets []*OrganizationSecret `json:"organization_secrets,omitempty"`
}

type OrganizationSecret struct {
Name string `json:"name"`
UpdatedAt int `json:"updated_at"`
}

func (o Organization) ViolationEntityType() string {
Expand Down
6 changes: 6 additions & 0 deletions internal/collected/github/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ type Repository struct {
ActionsTokenPermissions *types.TokenPermissions `json:"actions_token_permissions"`
DependencyGraphManifests *GitHubQLDependencyGraphManifests `json:"dependency_graph_manifests"`
RulesSet []*types.RepositoryRule `json:"rules_set,omitempty"`
RepoSecrets []*RepositorySecret `json:"repository_secrets,omitempty"`
}

type RepositorySecret struct {
Name string `json:"name"`
UpdatedAt int `json:"updated_at"`
}

func (r Repository) ViolationEntityType() string {
Expand Down
18 changes: 18 additions & 0 deletions internal/collectors/github/organization_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ func (c *organizationCollector) collectExtraData(org *ghcollected.ExtendedOrg) g
c.IssueMissingPermissions(perm)
}

secrets := c.collectOrgSecrets(org.Name())

return ghcollected.Organization{
Organization: org,
SamlEnabled: samlEnabled,
Hooks: hooks,
OrgSecrets: secrets,
}
}

Expand Down Expand Up @@ -128,3 +131,18 @@ func (c *organizationCollector) collectOrgSamlData(org string) (*bool, error) {
return &samlEnabled, nil

}

func (c *organizationCollector) collectOrgSecrets(org string) []*ghcollected.OrganizationSecret {
Tal-Legit marked this conversation as resolved.
Show resolved Hide resolved
secrets, err := c.Client.GetOrganizationSecrets(org)
if err != nil {
return nil
Tal-Legit marked this conversation as resolved.
Show resolved Hide resolved
}
var orgSecrets []*ghcollected.OrganizationSecret
for i := 0; i < len(secrets.Secrets); i++ {
orgSecrets = append(orgSecrets, &ghcollected.OrganizationSecret{
Name: secrets.Secrets[i].Name,
UpdatedAt: int(secrets.Secrets[i].UpdatedAt.Time.UnixNano())})
}

return orgSecrets
}
23 changes: 20 additions & 3 deletions internal/collectors/github/repository_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package github
import (
"context"
"fmt"
"log"
"net/http"

"github.com/Legit-Labs/legitify/internal/collectors"
"github.com/Legit-Labs/legitify/internal/common/types"
"github.com/Legit-Labs/legitify/internal/context_utils"
"github.com/Legit-Labs/legitify/internal/scorecard"
"log"
"net/http"

"github.com/Legit-Labs/legitify/internal/common/group_waiter"
"github.com/Legit-Labs/legitify/internal/common/permissions"
Expand Down Expand Up @@ -246,6 +245,7 @@ func (rc *repositoryCollector) collectExtraData(login string,
repo = rc.withRepositoryHooks(repo, login)
repo = rc.withRepoCollaborators(repo, login)
repo = rc.withActionsSettings(repo, login)
repo = rc.withSecrets(repo, login)

repo, err = rc.withDependencyGraphManifestsCount(repo, login)
if err != nil {
Expand Down Expand Up @@ -388,6 +388,23 @@ func (rc *repositoryCollector) withRulesSet(repository ghcollected.Repository, o
return repository, nil
}

func (rc *repositoryCollector) withSecrets(repository ghcollected.Repository, login string) ghcollected.Repository {
secrets, err := rc.Client.GetRepositorySecrets(repository.Name(), login)
if err != nil {
//add prerequisite / scope / error
return repository
Tal-Legit marked this conversation as resolved.
Show resolved Hide resolved
}
var repoSecrets []*ghcollected.RepositorySecret
for i := 0; i < len(secrets.Secrets); i++ {
repoSecrets = append(repoSecrets, &ghcollected.RepositorySecret{
Name: secrets.Secrets[i].Name,
UpdatedAt: int(secrets.Secrets[i].UpdatedAt.Time.UnixNano()),
})
}
repository.RepoSecrets = repoSecrets
return repository
}

// fixBranchProtectionInfo fixes the branch protection info for the repository,
// to reflect whether there is no branch protection, or just no permission to fetch the info.
func (rc *repositoryCollector) fixBranchProtectionInfo(repository ghcollected.Repository, org string) (ghcollected.Repository, error) {
Expand Down
1 change: 1 addition & 0 deletions internal/enricher/enricher_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ var mapping = map[string]enrichers.Enricher{
enrichers.Scorecard: enrichers.NewScorecardEnricher(),
enrichers.MembersList: enrichers.NewMembersListEnricher(),
enrichers.HooksList: enrichers.NewHooksListEnricher(),
enrichers.SecretsList: enrichers.NewSecretsListEnricher(),
}

func NewEnricherManager() EnricherManager {
Expand Down
54 changes: 54 additions & 0 deletions internal/enricher/enrichers/secrets_list_enricher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package enrichers

import (
"encoding/json"
"fmt"
"github.com/Legit-Labs/legitify/internal/analyzers"
"github.com/Legit-Labs/legitify/internal/common/map_utils"
"github.com/iancoleman/orderedmap"
"golang.org/x/net/context"
"log"
)

const SecretsList = "secretsList"

func NewSecretsListEnricher() secretsListEnricher {
return secretsListEnricher{}
}

type secretsListEnricher struct {
}

func (e secretsListEnricher) Enrich(_ context.Context, data analyzers.AnalyzedData) (Enrichment, bool) {
result, err := createSecretListEnrichment(data.ExtraData)
if err != nil {
log.Printf("failed to enrich secrets list: %v", err)
return nil, false
}
return result, true
}

func (e secretsListEnricher) Parse(data interface{}) (Enrichment, error) {
return NewGenericListEnrichmentFromInterface(data)
}

func createSecretListEnrichment(extraData interface{}) (GenericListEnrichment, error) {
asMap, ok := extraData.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid hookslist extra data")
}

result := []orderedmap.OrderedMap{}
for k := range asMap {
var secretsEnrichment map[string]string

err := json.Unmarshal([]byte(k), &secretsEnrichment)
if err != nil {
return nil, err
}

result = append(result, *map_utils.ToKeySortedMap(secretsEnrichment))
}

return result, nil
}
2 changes: 1 addition & 1 deletion policies/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ func TestPoliciesBundle(t *testing.T) {
count, err := countBundles()

require.Nilf(t, err, "counting files: %v", err)
require.Equal(t, count, 7, "Expecting 7 files in bundle")
require.Equal(t, count, 8, "Expecting 8 files in bundle")
}
33 changes: 33 additions & 0 deletions policies/github/organization.rego
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,36 @@ default organization_not_using_single_sign_on := true
organization_not_using_single_sign_on := false {
input.saml_enabled
}

# METADATA
# scope: rule
# title: Organization Has Stale Secrets
# description: Some of the organizations secrets have not been updated for over a year. It is recommended to refresh secret values regularly in order to minimize the risk of breach in case of an information leak.
# custom:
# requiredEnrichers: [secretsList]
# remediationSteps:
# - Enter your organization's landing page
# - Go to the settings tab
# - Under the 'Security' title on the left, choose 'Secrets and variables'
# - Click 'Actions'
# - Sort secrets by 'Last Updated'
# - Regenerate every secret older than one year and add the new value to GitHub's secret manager
# severity: MEDIUM
# requiredScopes: [repo]
# threat: Sensitive data may have been inadvertently made public in the past, and an attacker who holds this data may gain access to your current CI and services. In addition, there may be old or unnecessary tokens that have not been inspected and can be used to access sensitive information.
organization_secret_is_stale[stale] := true{
some index
secret := input.organization_secrets[index]
is_stale(secret.updated_at)
stale := {
"name" : secret.name,
"update date" : time.format(secret.updated_at),
}

}

is_stale(date) {
Tal-Legit marked this conversation as resolved.
Show resolved Hide resolved
ns_per_year := 365 * 24 * 60 * 60 * 1000000000
diff_ns := time.now_ns() - date
diff_ns > ns_per_year
}
34 changes: 34 additions & 0 deletions policies/github/repository_two.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package repository

# METADATA
# scope: rule
# title: Repository Has Stale Secrets
# description: Some of the repository secrets have not been updated for over a year. It is recommended to refresh secret values regularly in order to minimize the risk of breach in case of an information leak.
# custom:
# requiredEnrichers: [secretsList]
# remediationSteps:
# - Enter your repository's landing page
# - Go to the settings tab
# - Under the 'Security' title on the left, choose 'Secrets and variables'
# - Click 'Actions'
# - Sort secrets by 'Last Updated'
# - Regenerate every secret older than one year and add the new value to GitHub's secret manager
# severity: MEDIUM
# requiredScopes: [repo]
# threat: Sensitive data may have been inadvertently made public in the past, and an attacker who holds this data may gain access to your current CI and services. In addition, there may be old or unnecessary tokens that have not been inspected and can be used to access sensitive information.
repository_secret_is_stale[stale] := true{
some index
secret := input.repository_secrets[index]
is_stale(secret.updated_at)
stale := {
"name" : secret.name,
"update date" : time.format(secret.updated_at),
}

}

is_stale(date) {
Tal-Legit marked this conversation as resolved.
Show resolved Hide resolved
ns_per_year := 365 * 24 * 60 * 60 * 1000000000
diff_ns := time.now_ns() - date
diff_ns > ns_per_year
}
57 changes: 57 additions & 0 deletions test/organization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/Legit-Labs/legitify/internal/common/scm_type"
"github.com/google/go-github/v53/github"
"testing"
"time"

githubcollected "github.com/Legit-Labs/legitify/internal/collected/github"
"github.com/Legit-Labs/legitify/internal/common/namespace"
Expand All @@ -14,6 +15,7 @@ type organizationMockConfiguration struct {
ssoEnabled *bool
name string
url string
secrets []*githubcollected.OrganizationSecret
}

func newOrganizationMock(config organizationMockConfiguration) githubcollected.Organization {
Expand All @@ -30,11 +32,16 @@ func newOrganizationMock(config organizationMockConfiguration) githubcollected.O
}
hooks = append(hooks, &hook)
}
var orgSecrets []*githubcollected.OrganizationSecret = nil
if config.secrets != nil {
orgSecrets = append(orgSecrets, config.secrets...)
}

return githubcollected.Organization{
Organization: nil,
SamlEnabled: &samlEnabledMockResult,
Hooks: hooks,
OrgSecrets: orgSecrets,
}
}

Expand Down Expand Up @@ -113,6 +120,56 @@ func TestOrganization(t *testing.T) {
ssoEnabled: &boolTrue,
},
},
{
name: "Organization has no stale secrets",
policyName: "organization_secret_is_stale",
shouldBeViolated: false,
args: organizationMockConfiguration{
secrets: []*githubcollected.OrganizationSecret{
{
Name: "test1",
UpdatedAt: int(time.Now().UnixNano()) - 2628000000000000, // one month
},
{
Name: "test2",
UpdatedAt: int(time.Now().UnixNano()) - (3 * 2628000000000000), // three months
},
{
Name: "test3",
UpdatedAt: int(time.Now().UnixNano()) - (6 * 2628000000000000), // six month
},
},
},
},
{
name: "Organization has stale secrets",
policyName: "organization_secret_is_stale",
shouldBeViolated: true,
args: organizationMockConfiguration{
secrets: []*githubcollected.OrganizationSecret{
{
Name: "test1",
UpdatedAt: 1652020546000000000, //08.05.2022
},
{
Name: "test2",
UpdatedAt: 957796546000000000, //08.05.2000
},
{
Name: "test3",
UpdatedAt: int(time.Now().UnixNano()),
},
},
},
},
{
name: "Organization has no secrets",
policyName: "organization_secret_is_stale",
shouldBeViolated: false,
args: organizationMockConfiguration{
secrets: nil,
},
},
}

for _, test := range tests {
Expand Down
Loading
Loading