Skip to content

Commit

Permalink
BED-4768 feat: add login handler for OIDC (#897)
Browse files Browse the repository at this point in the history
  • Loading branch information
mistahj67 authored Oct 25, 2024
1 parent 348beb2 commit 9203347
Show file tree
Hide file tree
Showing 24 changed files with 208 additions and 53 deletions.
31 changes: 31 additions & 0 deletions cmd/api/src/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"io"
"net/http"
"strconv"
"strings"
"time"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -299,6 +300,36 @@ func (s authenticator) ValidateRequestSignature(tokenID uuid.UUID, request *http
}
}

func SetSecureBrowserCookie(request *http.Request, response http.ResponseWriter, name, value string, expires time.Time, httpOnly bool) {
var (
hostURL = *ctx.FromRequest(request).Host
sameSiteValue = http.SameSiteDefaultMode
domainValue string
)

if hostURL.Scheme == "https" {
sameSiteValue = http.SameSiteStrictMode
}

// NOTE: Set-Cookie should generally have the Domain field blank to ensure the cookie is only included with requests against the host, excluding subdomains; however,
// most browsers will ignore Set-Cookie headers from localhost responses if the Domain field is not set explicitly.
if strings.Contains(hostURL.Hostname(), "localhost") {
domainValue = hostURL.Hostname()
}

// Set the token cookie
http.SetCookie(response, &http.Cookie{
Name: name,
Value: value,
Expires: expires,
Secure: hostURL.Scheme == "https",
HttpOnly: httpOnly,
SameSite: sameSiteValue,
Path: "/",
Domain: domainValue,
})
}

func (s authenticator) CreateSession(ctx context.Context, user model.User, authProvider any) (string, error) {
if user.IsDisabled {
return "", ErrUserDisabled
Expand Down
3 changes: 3 additions & 0 deletions cmd/api/src/api/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const (

// Cookie Keys
AuthTokenCookieName = "token"
AuthStateCookieName = "state"
AuthPKCECookieName = "pkce"

// UserInterfacePath is the static path to the UI landing page
UserInterfacePath = "/ui"
Expand Down Expand Up @@ -60,4 +62,5 @@ const (
URIPathVariableUserID = "user_id"
URIPathVariableSavedQueryID = "saved_query_id"
URIPathVariableSSOProviderID = "sso_provider_id"
URIPathVariableSSOProviderSlug = "sso_provider_slug"
)
1 change: 1 addition & 0 deletions cmd/api/src/api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const (
ErrorResponseAGDuplicateName = "asset group name must be unique"
ErrorResponseAGDuplicateTag = "asset group tag must be unique"
ErrorResponseDetailsUniqueViolation = "unique constraint was violated"
ErrorResponseDetailsNotImplemented = "All good things to those who wait. Not implemented."

FmtErrorResponseDetailsBadQueryParameters = "there are errors in the query parameters: %v"
)
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func registerV2Auth(cfg config.Configuration, db database.Database, permissions
routerInst.GET("/api/v2/sso-providers", managementResource.ListAuthProviders).CheckFeatureFlag(db, appcfg.FeatureOIDCSupport),
routerInst.POST("/api/v2/sso-providers/oidc", managementResource.CreateOIDCProvider).CheckFeatureFlag(db, appcfg.FeatureOIDCSupport).RequirePermissions(permissions.AuthManageProviders),
routerInst.DELETE(fmt.Sprintf("/api/v2/sso-providers/{%s}", api.URIPathVariableSSOProviderID), managementResource.DeleteSSOProvider).CheckFeatureFlag(db, appcfg.FeatureOIDCSupport).RequirePermissions(permissions.AuthManageProviders),
routerInst.GET(fmt.Sprintf("/api/v2/sso/{%s}/login", api.URIPathVariableSSOProviderSlug), managementResource.SSOLoginHandler).CheckFeatureFlag(db, appcfg.FeatureOIDCSupport),

// Permissions
routerInst.GET("/api/v2/permissions", managementResource.ListPermissions).RequirePermissions(permissions.AuthManageSelf),
Expand Down
43 changes: 43 additions & 0 deletions cmd/api/src/api/v2/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@
package auth

import (
"fmt"
"net/http"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/specterops/bloodhound/headers"
"github.com/specterops/bloodhound/src/api"
"github.com/specterops/bloodhound/src/config"
"github.com/specterops/bloodhound/src/ctx"
"github.com/specterops/bloodhound/src/model"
"github.com/specterops/bloodhound/src/utils/validation"
"golang.org/x/oauth2"
)

// CreateOIDCProviderRequest represents the body of the CreateOIDCProvider endpoint
Expand Down Expand Up @@ -48,3 +56,38 @@ func (s ManagementResource) CreateOIDCProvider(response http.ResponseWriter, req
}
}
}

func getRedirectURL(request *http.Request, provider model.SSOProvider) string {
hostUrl := *ctx.FromRequest(request).Host
return fmt.Sprintf("%s/api/v2/sso/%s/callback", hostUrl.String(), provider.Slug)
}

func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider, oidcProvider model.OIDCProvider) {
if state, err := config.GenerateRandomBase64String(77); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response)
} else if provider, err := oidc.NewProvider(request.Context(), oidcProvider.Issuer); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response)
} else {
conf := &oauth2.Config{
ClientID: oidcProvider.ClientID,
Endpoint: provider.Endpoint(),
RedirectURL: getRedirectURL(request, ssoProvider),
}

// use PKCE to protect against CSRF attacks
// https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6
verifier := oauth2.GenerateVerifier()

// Store PKCE on web browser in secure cookie for retrieval in callback
api.SetSecureBrowserCookie(request, response, api.AuthPKCECookieName, verifier, time.Now().UTC().Add(time.Minute*7), true)

// Store State on web browser in secure cookie for retrieval in callback
api.SetSecureBrowserCookie(request, response, api.AuthStateCookieName, state, time.Now().UTC().Add(time.Minute*7), true)

// Redirect user to consent page to ask for permission for the scopes specified above.
redirectURL := conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))

response.Header().Add(headers.Location.String(), redirectURL)
response.WriteHeader(http.StatusFound)
}
}
22 changes: 22 additions & 0 deletions cmd/api/src/api/v2/auth/sso.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,25 @@ func (s ManagementResource) DeleteSSOProvider(response http.ResponseWriter, requ
}, http.StatusOK, response)
}
}

func (s ManagementResource) SSOLoginHandler(response http.ResponseWriter, request *http.Request) {
ssoProviderSlug := mux.Vars(request)[api.URIPathVariableSSOProviderSlug]

if ssoProvider, err := s.db.GetSSOProviderBySlug(request.Context(), ssoProviderSlug); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
switch ssoProvider.Type {
case model.SessionAuthProviderSAML:
//todo handle saml login
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotImplemented, api.ErrorResponseDetailsNotImplemented, request), response)
case model.SessionAuthProviderOIDC:
if ssoProvider.OIDCProvider == nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response)
} else {
s.OIDCLoginHandler(response, request, ssoProvider, *ssoProvider.OIDCProvider)
}
default:
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotImplemented, api.ErrorResponseDetailsNotImplemented, request), response)
}
}
}
15 changes: 15 additions & 0 deletions cmd/api/src/database/mocks/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 34 additions & 26 deletions cmd/api/src/database/sso_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,37 +30,13 @@ const (

// SSOProviderData defines the methods required to interact with the sso_providers table
type SSOProviderData interface {
GetAllSSOProviders(ctx context.Context, order string, sqlFilter model.SQLFilter) ([]model.SSOProvider, error)
CreateSSOProvider(ctx context.Context, name string, authProvider model.SessionAuthProvider) (model.SSOProvider, error)
DeleteSSOProvider(ctx context.Context, id int) error
GetAllSSOProviders(ctx context.Context, order string, sqlFilter model.SQLFilter) ([]model.SSOProvider, error)
GetSSOProviderBySlug(ctx context.Context, slug string) (model.SSOProvider, error)
GetSSOProviderUsers(ctx context.Context, id int) (model.Users, error)
}

func (s *BloodhoundDB) GetAllSSOProviders(ctx context.Context, order string, sqlFilter model.SQLFilter) ([]model.SSOProvider, error) {
var providers []model.SSOProvider

query := s.db.WithContext(ctx).Model(&model.SSOProvider{})

// Apply SQL filter if provided
if sqlFilter.SQLString != "" {
query = query.Where(sqlFilter.SQLString, sqlFilter.Params...)
}

// Apply sorting order if provided
if order != "" {
query = query.Order(order)
} else {
// Default ordering by created_at if no order is specified
query = query.Order("created_at")
}

// Preload the associated OIDC and SAML providers
query = query.Preload("OIDCProvider").Preload("SAMLProvider")

result := query.Find(&providers)
return providers, CheckError(result)
}

// CreateSSOProvider creates an entry in the sso_providers table
// A slug will be created for the SSO Provider using the name argument as a base. The name will be lower cased and all spaces are replaced with `-`
func (s *BloodhoundDB) CreateSSOProvider(ctx context.Context, name string, authProvider model.SessionAuthProvider) (model.SSOProvider, error) {
Expand Down Expand Up @@ -113,6 +89,38 @@ func (s *BloodhoundDB) DeleteSSOProvider(ctx context.Context, id int) error {
return err
}

func (s *BloodhoundDB) GetAllSSOProviders(ctx context.Context, order string, sqlFilter model.SQLFilter) ([]model.SSOProvider, error) {
var providers []model.SSOProvider

query := s.db.WithContext(ctx).Model(&model.SSOProvider{})

// Apply SQL filter if provided
if sqlFilter.SQLString != "" {
query = query.Where(sqlFilter.SQLString, sqlFilter.Params...)
}

// Apply sorting order if provided
if order != "" {
query = query.Order(order)
} else {
// Default ordering by created_at if no order is specified
query = query.Order("created_at")
}

// Preload the associated OIDC and SAML providers
query = query.Preload("OIDCProvider").Preload("SAMLProvider")

result := query.Find(&providers)
return providers, CheckError(result)
}

func (s *BloodhoundDB) GetSSOProviderBySlug(ctx context.Context, slug string) (model.SSOProvider, error) {
var provider model.SSOProvider
result := s.db.WithContext(ctx).Preload("OIDCProvider").Preload("SAMLProvider").Where("slug = ?", slug).Find(&provider)

return provider, CheckError(result)
}

// GetSSOProviderUsers returns all the users associated with a given sso provider
func (s *BloodhoundDB) GetSSOProviderUsers(ctx context.Context, id int) (model.Users, error) {
var (
Expand Down
20 changes: 20 additions & 0 deletions cmd/api/src/database/sso_providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,26 @@ func TestBloodhoundDB_GetAllSSOProviders(t *testing.T) {
})
}

func TestBloodhoundDB_GetSSOProviderBySlug(t *testing.T) {
var (
testCtx = context.Background()
dbInst = integration.SetupDB(t)
)
defer dbInst.Close(testCtx)

t.Run("successfully get sso provider by slug", func(t *testing.T) {
newProvider, err := dbInst.CreateOIDCProvider(testCtx, "Gotham Net", "https://test.localhost.com/auth", "gotham-net")
require.Nil(t, err)

provider, err := dbInst.GetSSOProviderBySlug(testCtx, "gotham-net")
require.Nil(t, err)
require.EqualValues(t, newProvider.SSOProviderID, provider.ID)
require.NotNil(t, provider.OIDCProvider)
require.Equal(t, newProvider.ClientID, provider.OIDCProvider.ClientID)
require.Equal(t, newProvider.Issuer, provider.OIDCProvider.Issuer)
})
}

func TestBloodhoundDB_GetSSOProviderUsers(t *testing.T) {
var (
testCtx = context.Background()
Expand Down
7 changes: 5 additions & 2 deletions cmd/api/src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/beevik/etree v1.2.0
github.com/bloodhoundad/azurehound/v2 v2.0.1
github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61
github.com/coreos/go-oidc/v3 v3.11.0
github.com/crewjam/saml v0.4.14
github.com/didip/tollbooth/v6 v6.1.2
github.com/go-chi/chi/v5 v5.0.8
Expand All @@ -45,7 +46,8 @@ require (
github.com/unrolled/secure v1.13.0
github.com/zenazn/goji v1.0.1
go.uber.org/mock v0.2.0
golang.org/x/crypto v0.24.0
golang.org/x/crypto v0.25.0
golang.org/x/oauth2 v0.23.0
gorm.io/driver/postgres v1.3.8
gorm.io/gorm v1.23.8
)
Expand All @@ -58,6 +60,7 @@ require (
github.com/crewjam/httperr v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand All @@ -80,7 +83,7 @@ require (
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
Expand Down
16 changes: 12 additions & 4 deletions cmd/api/src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 h1:o6
github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61/go.mod h1:Rp8e0DCtEKwXFOC6JPJQVTz8tuGoGvw6Xfexggh/ed0=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
Expand All @@ -36,6 +38,8 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
Expand Down Expand Up @@ -211,6 +215,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
Expand Down Expand Up @@ -241,8 +246,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
Expand All @@ -253,6 +258,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -272,8 +279,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand All @@ -283,6 +290,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
Loading

0 comments on commit 9203347

Please sign in to comment.