From d3bd63790ec33da8438b0557744e8f9397246ca1 Mon Sep 17 00:00:00 2001 From: Ivan Milchev Date: Tue, 21 May 2024 17:14:27 +0200 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=EF=B8=8F=20github=20app=20authenticat?= =?UTF-8?q?ion=20(#4041)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⭐️ github app authentication Signed-off-by: Ivan Milchev * add support for enterprise url Signed-off-by: Ivan Milchev --------- Signed-off-by: Ivan Milchev --- providers/github/config/config.go | 24 ++++ providers/github/connection/connection.go | 134 +++++++++++++++++----- providers/github/go.mod | 3 + providers/github/go.sum | 6 + providers/github/provider/provider.go | 24 +++- 5 files changed, 158 insertions(+), 33 deletions(-) diff --git a/providers/github/config/config.go b/providers/github/config/config.go index e6484f11a9..8a8b4719c7 100644 --- a/providers/github/config/config.go +++ b/providers/github/config/config.go @@ -41,6 +41,30 @@ var Config = plugin.Provider{ Default: "", Desc: "Only include repositories with matching names.", }, + { + Long: "app-id", + Type: plugin.FlagType_String, + Default: "", + Desc: "GitHub Application ID.", + }, + { + Long: "app-installation-id", + Type: plugin.FlagType_String, + Default: "", + Desc: "GitHub Application installation ID.", + }, + { + Long: "app-private-key", + Type: plugin.FlagType_String, + Default: "", + Desc: "GitHub Application private key.", + }, + { + Long: "enterprise-url", + Type: plugin.FlagType_String, + Default: "", + Desc: "GitHub Enterprise URL.", + }, }, }, }, diff --git a/providers/github/connection/connection.go b/providers/github/connection/connection.go index d5533cf996..fd4769ad03 100644 --- a/providers/github/connection/connection.go +++ b/providers/github/connection/connection.go @@ -6,11 +6,13 @@ package connection import ( "context" "net/http" + "net/url" "os" + "strconv" + "github.com/bradleyfalzon/ghinstallation/v2" "github.com/cockroachdb/errors" "github.com/google/go-github/v61/github" - "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v11/providers-sdk/v1/vault" @@ -18,8 +20,12 @@ import ( ) const ( - OPTION_REPOS = "repos" - OPTION_REPOS_EXCLUDE = "repos-exclude" + OPTION_REPOS = "repos" + OPTION_REPOS_EXCLUDE = "repos-exclude" + OPTION_APP_ID = "app-id" + OPTION_APP_INSTALLATION_ID = "app-installation-id" + OPTION_APP_PRIVATE_KEY = "app-private-key" + OPTION_ENTERPRISE_URL = "enterprise-url" ) type GithubConnection struct { @@ -30,41 +36,35 @@ type GithubConnection struct { func NewGithubConnection(id uint32, asset *inventory.Asset) (*GithubConnection, error) { conf := asset.Connections[0] - token := conf.Options["token"] - // if no token was provided, lets read the env variable - if token == "" { - token = os.Getenv("GITHUB_TOKEN") + var client *github.Client + var err error + appIdStr := conf.Options[OPTION_APP_ID] + if appIdStr != "" { + client, err = newGithubAppClient(conf) + } else { + client, err = newGithubTokenClient(conf) } - - // if a secret was provided, it always overrides the env variable since it has precedence - if len(conf.Credentials) > 0 { - for i := range conf.Credentials { - cred := conf.Credentials[i] - if cred.Type == vault.CredentialType_password { - token = string(cred.Secret) - } else { - log.Warn().Str("credential-type", cred.Type.String()).Msg("unsupported credential type for GitHub provider") - } - } + if err != nil { + return nil, err } - if token == "" { - return nil, errors.New("a valid GitHub token is required, pass --token '' or set GITHUB_TOKEN environment variable") - } + if enterpriseUrl := conf.Options[OPTION_ENTERPRISE_URL]; enterpriseUrl != "" { + parsedUrl, err := url.Parse(enterpriseUrl) + if err != nil { + return nil, err + } - var oauthClient *http.Client - if token != "" { - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - ctx := context.Background() - oauthClient = oauth2.NewClient(ctx, ts) + baseUrl := parsedUrl.JoinPath("api/v3/") + uploadUrl := parsedUrl.JoinPath("api/uploads/") + client, err = client.WithEnterpriseURLs(baseUrl.String(), uploadUrl.String()) + if err != nil { + return nil, err + } } - client := github.NewClient(oauthClient) // perform a quick call to verify the token's validity. - _, resp, err := client.Zen(context.Background()) + _, resp, err := client.Meta.Zen(context.Background()) if err != nil { if resp != nil && resp.StatusCode == 401 { return nil, errors.New("invalid GitHub token provided. check the value passed with the --token flag or the GITHUB_TOKEN environment variable") @@ -89,3 +89,77 @@ func (c *GithubConnection) Asset() *inventory.Asset { func (c *GithubConnection) Client() *github.Client { return c.client } + +func newGithubAppClient(conf *inventory.Config) (*github.Client, error) { + appIdStr := conf.Options[OPTION_APP_ID] + if appIdStr == "" { + return nil, errors.New("app-id is required for GitHub App authentication") + } + appId, err := strconv.ParseInt(appIdStr, 10, 32) + if err != nil { + return nil, err + } + + appInstallationIdStr := conf.Options[OPTION_APP_INSTALLATION_ID] + if appInstallationIdStr == "" { + return nil, errors.New("app-installation-id is required for GitHub App authentication") + } + appInstallationId, err := strconv.ParseInt(appInstallationIdStr, 10, 32) + if err != nil { + return nil, err + } + + var itr *ghinstallation.Transport + if pk := conf.Options[OPTION_APP_PRIVATE_KEY]; pk != "" { + itr, err = ghinstallation.NewKeyFromFile(http.DefaultTransport, appId, appInstallationId, pk) + } else { + for _, cred := range conf.Credentials { + switch cred.Type { + case vault.CredentialType_private_key: + itr, err = ghinstallation.New(http.DefaultTransport, appId, appInstallationId, cred.Secret) + if err != nil { + return nil, err + } + } + } + } + if err != nil { + return nil, err + } + + if itr == nil { + return nil, errors.New("app-private-key is required for GitHub App authentication") + } + + return github.NewClient(&http.Client{Transport: itr}), nil +} + +func newGithubTokenClient(conf *inventory.Config) (*github.Client, error) { + token := "" + for i := range conf.Credentials { + cred := conf.Credentials[i] + switch cred.Type { + case vault.CredentialType_password: + token = string(cred.Secret) + } + } + + if token == "" { + token = conf.Options["token"] + if token == "" { + token = os.Getenv("GITHUB_TOKEN") + } + } + + // if we still have no token, error out + if token == "" { + return nil, errors.New("a valid GitHub token is required, pass --token '' or set GITHUB_TOKEN environment variable") + } + + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + return github.NewClient(tc), nil +} diff --git a/providers/github/go.mod b/providers/github/go.mod index 3be3cfeba1..827f1139ff 100644 --- a/providers/github/go.mod +++ b/providers/github/go.mod @@ -7,6 +7,7 @@ go 1.22 toolchain go1.22.0 require ( + github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 github.com/cockroachdb/errors v1.11.1 github.com/gobwas/glob v0.2.3 github.com/google/go-github/v61 v61.0.0 @@ -153,6 +154,7 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -166,6 +168,7 @@ require ( github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-containerregistry v0.19.1 // indirect + github.com/google/go-github/v60 v60.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.7 // indirect diff --git a/providers/github/go.sum b/providers/github/go.sum index e948f33a97..407bec4b90 100644 --- a/providers/github/go.sum +++ b/providers/github/go.sum @@ -217,6 +217,8 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bombsimon/wsl/v4 v4.2.1 h1:Cxg6u+XDWff75SIFFmNsqnIOgob+Q9hG6y/ioKbRFiM= github.com/bombsimon/wsl/v4 v4.2.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= +github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 h1:XWuWBRFEpqVrHepQob9yPS3Xg4K3Wr9QCx4fu8HbUNg= +github.com/bradleyfalzon/ghinstallation/v2 v2.10.0/go.mod h1:qoGA4DxWPaYTgVCrmEspVSjlTu4WYAiSxMIhorMRXXc= github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= @@ -429,6 +431,8 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -502,6 +506,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8= +github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= diff --git a/providers/github/provider/provider.go b/providers/github/provider/provider.go index 78abbefd9d..2627c38217 100644 --- a/providers/github/provider/provider.go +++ b/providers/github/provider/provider.go @@ -46,6 +46,22 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) Discover: &inventory.Discovery{}, } + if x, ok := flags["enterprise-url"]; ok && len(x.Value) != 0 { + conf.Options[connection.OPTION_ENTERPRISE_URL] = string(x.Value) + } + + isAppAuth := false + if appId, ok := req.Flags[connection.OPTION_APP_ID]; ok && len(appId.Value) > 0 { + conf.Options[connection.OPTION_APP_ID] = string(appId.Value) + + installId := req.Flags[connection.OPTION_APP_INSTALLATION_ID] + conf.Options[connection.OPTION_APP_INSTALLATION_ID] = string(installId.Value) + + pk := req.Flags[connection.OPTION_APP_PRIVATE_KEY] + conf.Options[connection.OPTION_APP_PRIVATE_KEY] = string(pk.Value) + isAppAuth = true + } + token := "" if x, ok := flags["token"]; ok && len(x.Value) != 0 { token = string(x.Value) @@ -53,10 +69,12 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) if token == "" { token = os.Getenv("GITHUB_TOKEN") } - if token == "" { - return nil, errors.New("a valid GitHub token is required, pass --token '' or set GITHUB_TOKEN environment variable") + if token == "" && !isAppAuth { + return nil, errors.New("a valid GitHub authentication is required, pass --token '', set GITHUB_TOKEN environment variable or provider GitHub App credentials") + } + if token != "" { + conf.Credentials = append(conf.Credentials, vault.NewPasswordCredential("", token)) } - conf.Credentials = append(conf.Credentials, vault.NewPasswordCredential("", token)) // discovery flags discoverTargets := []string{}