From 05fd1ad39591b93617439db369e7720d69dd5ee1 Mon Sep 17 00:00:00 2001 From: Emmanuel Gautier Date: Fri, 3 Nov 2023 19:08:56 +0100 Subject: [PATCH] feat: add openapi scan support --- cmd/scan/root.go | 30 +++++-- go.mod | 13 +++ go.sum | 50 +++++++++++ internal/auth/auth.go | 34 +++++++ internal/auth/bearer.go | 51 +++++++++++ internal/request/request.go | 29 +++--- internal/request/request_test.go | 150 +++++++++++++++++++++++++++++++ internal/rest_api/loader.go | 34 +++++++ internal/rest_api/request.go | 36 ++++++++ scan/jwt/alg_none.go | 9 +- scan/jwt/not_verified.go | 12 ++- scan/jwt/null_signature.go | 9 +- scan/jwt/weak_secret.go | 11 ++- scan/rest_api/request.go | 25 ------ scan/scan.go | 39 ++++---- 15 files changed, 455 insertions(+), 77 deletions(-) create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/bearer.go create mode 100644 internal/request/request_test.go create mode 100644 internal/rest_api/loader.go create mode 100644 internal/rest_api/request.go delete mode 100644 scan/rest_api/request.go diff --git a/cmd/scan/root.go b/cmd/scan/root.go index 68107e3..877a5d6 100644 --- a/cmd/scan/root.go +++ b/cmd/scan/root.go @@ -5,13 +5,15 @@ import ( "fmt" "log" + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" "github.com/cerberauth/vulnapi/scan" "github.com/spf13/cobra" ) var ( - url string - jwt string + openapiUrlOrPath string + url string ) func NewScanCmd() (scanCmd *cobra.Command) { @@ -24,15 +26,27 @@ func NewScanCmd() (scanCmd *cobra.Command) { url = args[0] } - if jwt == "" { - stdin, err := bufio.NewReader(cmd.InOrStdin()).ReadString('\n') + opts := scan.ScanOptions{ + Url: url, + } + scanner := scan.NewScanner(opts) + + stdin, err := bufio.NewReader(cmd.InOrStdin()).ReadString('\n') + if err != nil { + log.Fatal(fmt.Errorf("failed process input: %v", err)) + } + bearerSecurityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &stdin) + scanner.AddSecurityScheme(bearerSecurityScheme) + + if openapiUrlOrPath != "" { + doc, err := restapi.LoadOpenAPI(openapiUrlOrPath) if err != nil { - log.Fatal(fmt.Errorf("failed process input: %v", err)) + log.Fatal(err) } - jwt = stdin + opts.OpenAPIDoc = doc } - rpr, _, err := scan.NewScanner(url, &jwt).WithAllScans().Execute() + rpr, _, err := scanner.WithAllScans().Execute() if err != nil { log.Fatal(err) } @@ -47,8 +61,8 @@ func NewScanCmd() (scanCmd *cobra.Command) { }, } + scanCmd.PersistentFlags().StringVarP(&openapiUrlOrPath, "openapi", "", "", "OpenAPI URL or Path. The scan will be performed against the operations listed in OpenAPI file.") scanCmd.PersistentFlags().StringVarP(&url, "url", "u", "", "URL") - scanCmd.PersistentFlags().StringVarP(&jwt, "jwt", "j", "", "Valid JWT") return scanCmd } diff --git a/go.mod b/go.mod index f210da7..b3b57e9 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,24 @@ module github.com/cerberauth/vulnapi go 1.21 require ( + github.com/getkin/kin-openapi v0.120.0 github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/jarcoal/httpmock v1.3.1 github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.8.2 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a342d71..985e549 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,62 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= +github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..133fd12 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,34 @@ +package auth + +import "net/http" + +type Type string +type Scheme string +type SchemeIn string + +const ( + HttpType Type = "http" + OAuth2 Type = "oauth2" + OpenIdConnect Type = "openIdConnect" + ApiKey Type = "apiKey" +) + +const ( + BasicScheme Scheme = "basic" + BearerScheme Scheme = "bearer" + NoneScheme Scheme = "none" +) + +const ( + InHeader SchemeIn = "header" + InCookie SchemeIn = "cookie" + InUnknown SchemeIn = "unknown" +) + +type SecurityScheme interface { + GetHeaders() http.Header + GetCookies() []*http.Cookie + GetValidValue() interface{} + SetAttackValue(v interface{}) + GetAttackValue() interface{} +} diff --git a/internal/auth/bearer.go b/internal/auth/bearer.go new file mode 100644 index 0000000..8aefa21 --- /dev/null +++ b/internal/auth/bearer.go @@ -0,0 +1,51 @@ +package auth + +import ( + "fmt" + "net/http" +) + +type BearerSecurityScheme struct { + Type Type + Scheme Scheme + In SchemeIn + Name string + ValidValue *string + AttackValue string +} + +func NewAuthorizationBearerSecurityScheme(name string, value *string) *BearerSecurityScheme { + return &BearerSecurityScheme{ + Type: HttpType, + Scheme: BearerScheme, + In: InHeader, + Name: name, + ValidValue: value, + AttackValue: "", + } +} + +func (ss *BearerSecurityScheme) GetHeaders() http.Header { + header := http.Header{} + if ss.ValidValue != nil { + header.Set("Authorization", fmt.Sprintf("Bearer %s", *ss.ValidValue)) + } + + return header +} + +func (ss *BearerSecurityScheme) GetCookies() []*http.Cookie { + return []*http.Cookie{} +} + +func (ss *BearerSecurityScheme) GetValidValue() interface{} { + return ss.ValidValue +} + +func (ss *BearerSecurityScheme) SetAttackValue(v interface{}) { + ss.AttackValue = v.(string) +} + +func (ss *BearerSecurityScheme) GetAttackValue() interface{} { + return ss.AttackValue +} diff --git a/internal/request/request.go b/internal/request/request.go index db40481..b3cd7de 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -1,11 +1,12 @@ package request import ( - "fmt" "net/http" + + "github.com/cerberauth/vulnapi/internal/auth" ) -func prepareVulnAPIRequest(method string, url string) (*http.Request, error) { +func NewRequest(method string, url string) (*http.Request, error) { req, err := http.NewRequest(method, url, nil) if err != nil { return nil, err @@ -16,20 +17,22 @@ func prepareVulnAPIRequest(method string, url string) (*http.Request, error) { return req, nil } -func SendRequestWithBearerAuth(url string, token string) (*http.Request, *http.Response, error) { - req, err := prepareVulnAPIRequest("GET", url) - if err != nil { - return req, nil, err - } +func DoRequest(client *http.Client, req *http.Request, ss auth.SecurityScheme) (*http.Request, *http.Response, error) { + if ss != nil { + for _, c := range ss.GetCookies() { + req.AddCookie(c) + } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + for n, h := range ss.GetHeaders() { + req.Header.Add(n, h[0]) + } + } - client := &http.Client{} - resp, err := client.Do(req) + res, err := client.Do(req) if err != nil { - return req, resp, err + return req, res, err } - defer resp.Body.Close() + defer res.Body.Close() - return req, resp, nil + return req, res, nil } diff --git a/internal/request/request_test.go b/internal/request/request_test.go new file mode 100644 index 0000000..32ee094 --- /dev/null +++ b/internal/request/request_test.go @@ -0,0 +1,150 @@ +package request + +import ( + "net/http" + "net/url" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type SecuritySchemeMock struct { + Cookies []*http.Cookie + Headers http.Header + ValidValue interface{} + AttackValue interface{} +} + +func NewSecuritySchemeMock() *SecuritySchemeMock { + return &SecuritySchemeMock{ + Cookies: []*http.Cookie{}, + Headers: http.Header{}, + ValidValue: nil, + AttackValue: nil, + } +} + +func (ss *SecuritySchemeMock) GetCookies() []*http.Cookie { + return ss.Cookies +} + +func (ss *SecuritySchemeMock) GetHeaders() http.Header { + return ss.Headers +} + +func (ss *SecuritySchemeMock) GetValidValue() interface{} { + return ss.ValidValue +} + +func (ss *SecuritySchemeMock) SetAttackValue(v interface{}) { + ss.AttackValue = v +} + +func (ss *SecuritySchemeMock) GetAttackValue() interface{} { + return ss.AttackValue +} + +var reqMethod = "GET" +var reqUrl = "http://localhost:8080" + +func setupSuite(tb testing.TB) func(tb testing.TB) { + httpmock.Activate() + httpmock.RegisterResponder(reqMethod, reqUrl, httpmock.NewBytesResponder(204, nil)) + + return func(tb testing.TB) { + defer httpmock.DeactivateAndReset() + } +} + +func TestNewRequestUserMethodAndUrl(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + req, err := NewRequest(reqMethod, reqUrl) + require.NoError(t, err) + assert.Equal(t, reqMethod, req.Method) + assert.Equal(t, &url.URL{Scheme: "http", Host: "localhost:8080"}, req.URL) + + reqMethod2 := "PUT" + + req2, err2 := NewRequest(reqMethod2, reqUrl) + require.NoError(t, err2) + assert.Equal(t, reqMethod2, req2.Method) + assert.Equal(t, &url.URL{Scheme: "http", Host: "localhost:8080"}, req2.URL) +} + +func TestNewRequestAddUserAgent(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + req, err := NewRequest(reqMethod, reqUrl) + require.NoError(t, err) + assert.Equal(t, "vulnapi/0.1", req.UserAgent()) +} + +func TestNewRequestWithWrongUrl(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + _, err := NewRequest(reqMethod, "://localhost:8080") + require.Error(t, err) +} + +func TestDoRequestWithoutSecurityScheme(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + client := &http.Client{} + req, _ := NewRequest(reqMethod, reqUrl) + + req, res, err := DoRequest(client, req, nil) + require.NoError(t, err) + assert.Equal(t, 0, len(req.Cookies())) + assert.Equal(t, req, req) + assert.NotNil(t, res) + + assert.Equal(t, 1, httpmock.GetTotalCallCount()) +} + +func TestDoRequestWithSecuritySchemeAndCookies(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + ss := NewSecuritySchemeMock() + ss.Cookies = append(ss.Cookies, &http.Cookie{ + Name: "cookie", + Value: "cookie value", + }) + client := &http.Client{} + req, _ := NewRequest(reqMethod, reqUrl) + + req, res, err := DoRequest(client, req, ss) + require.NoError(t, err) + assert.Equal(t, 1, len(req.Cookies())) + assert.Equal(t, ss.Cookies[0].Name, req.Cookies()[0].Name) + assert.Equal(t, ss.Cookies[0].Value, req.Cookies()[0].Value) + assert.NotNil(t, res) + + assert.Equal(t, 1, httpmock.GetTotalCallCount()) +} + +func TestDoRequestWithSecuritySchemeAndHeaders(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + ss := NewSecuritySchemeMock() + ss.Headers = http.Header{} + ss.Headers.Add("header1", "value1") + client := &http.Client{} + req, _ := NewRequest(reqMethod, reqUrl) + + req, res, err := DoRequest(client, req, ss) + require.NoError(t, err) + assert.Equal(t, 0, len(req.Cookies())) + assert.Equal(t, "value1", req.Header.Get("header1")) + assert.NotNil(t, res) + + assert.Equal(t, 1, httpmock.GetTotalCallCount()) +} diff --git a/internal/rest_api/loader.go b/internal/rest_api/loader.go new file mode 100644 index 0000000..19ee2c7 --- /dev/null +++ b/internal/rest_api/loader.go @@ -0,0 +1,34 @@ +package restapi + +import ( + "errors" + "fmt" + "net/url" + "os" + "regexp" + + "github.com/getkin/kin-openapi/openapi3" +) + +var urlPatternRe = regexp.MustCompile(`^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/|\/|\/\/)?[A-z0-9_-]*?[:]?[A-z0-9_-]*?[@]?[A-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$`) + +func LoadOpenAPI(urlOrPath string) (*openapi3.T, error) { + if urlOrPath == "" { + return nil, errors.New("url or path must not be empty") + } + + if urlPatternRe.MatchString(urlOrPath) { + uri, urlerr := url.Parse(urlOrPath) + if urlerr != nil { + return nil, urlerr + } + + return openapi3.NewLoader().LoadFromURI(uri) + } + + if _, err := os.Stat(urlOrPath); err != nil { + return nil, fmt.Errorf("the openapi file has not been found on %s", urlOrPath) + } + + return openapi3.NewLoader().LoadFromFile(urlOrPath) +} diff --git a/internal/rest_api/request.go b/internal/rest_api/request.go new file mode 100644 index 0000000..6e7373d --- /dev/null +++ b/internal/rest_api/request.go @@ -0,0 +1,36 @@ +package restapi + +import ( + "fmt" + "net/http" + + "github.com/cerberauth/vulnapi/internal/auth" + "github.com/cerberauth/vulnapi/internal/request" + "github.com/cerberauth/vulnapi/report" +) + +func ScanRestAPI(url string, ss auth.SecurityScheme) *report.VulnerabilityScanAttempt { + var req *http.Request + var res *http.Response + var err error = nil + + client := &http.Client{} + req, err = request.NewRequest("GET", url) + if err != nil { + err = fmt.Errorf("request with url %s has an unexpected error", err) + } else { + req, res, err = request.DoRequest(client, req, nil) + } + + if err != nil { + err = fmt.Errorf("request with url %s has an unexpected error", err) + } else if res.StatusCode < 200 && res.StatusCode >= 300 { + err = fmt.Errorf("unexpected status code %d during test request", res.StatusCode) + } + + return &report.VulnerabilityScanAttempt{ + Request: req, + Response: res, + Err: err, + } +} diff --git a/scan/jwt/alg_none.go b/scan/jwt/alg_none.go index 4e5fba7..0ff5d10 100644 --- a/scan/jwt/alg_none.go +++ b/scan/jwt/alg_none.go @@ -1,8 +1,9 @@ package jwt import ( + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" "github.com/cerberauth/vulnapi/report" - restapi "github.com/cerberauth/vulnapi/scan/rest_api" "github.com/golang-jwt/jwt/v5" ) @@ -12,14 +13,16 @@ const ( AlgNoneVulnerabilityDescription = "JWT accepts none algorithm and does verify jwt." ) -func AlgNoneJwtScanHandler(url string, token string) (*report.ScanReport, error) { +func AlgNoneJwtScanHandler(url string, ss auth.SecurityScheme) (*report.ScanReport, error) { r := report.NewScanReport() + token := ss.GetValidValue().(string) newToken, err := createNewJWTWithClaimsAndMethod(token, jwt.SigningMethodNone, jwt.UnsafeAllowNoneSignatureType) if err != nil { return r, err } - vsa := restapi.ScanRestAPI(url, newToken) + ss.SetAttackValue(newToken) + vsa := restapi.ScanRestAPI(url, ss) r.AddScanAttempt(vsa).End() if vsa.Response.StatusCode < 300 { diff --git a/scan/jwt/not_verified.go b/scan/jwt/not_verified.go index aa05f34..282641f 100644 --- a/scan/jwt/not_verified.go +++ b/scan/jwt/not_verified.go @@ -1,8 +1,9 @@ package jwt import ( + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" "github.com/cerberauth/vulnapi/report" - restapi "github.com/cerberauth/vulnapi/scan/rest_api" "github.com/golang-jwt/jwt/v5" ) @@ -12,8 +13,9 @@ const ( NotVerifiedVulnerabilityDescription = "JWT is not verified." ) -func NotVerifiedScanHandler(url string, token string) (*report.ScanReport, error) { +func NotVerifiedScanHandler(url string, ss auth.SecurityScheme) (*report.ScanReport, error) { r := report.NewScanReport() + token := ss.GetValidValue().(string) newTokenA, err := createNewJWTWithClaimsAndMethod(token, jwt.SigningMethodHS256, []byte("a")) if err != nil { @@ -25,10 +27,12 @@ func NotVerifiedScanHandler(url string, token string) (*report.ScanReport, error return r, err } - vsa1 := restapi.ScanRestAPI(url, newTokenA) + ss.SetAttackValue(newTokenA) + vsa1 := restapi.ScanRestAPI(url, ss) r.AddScanAttempt(vsa1) - vsa2 := restapi.ScanRestAPI(url, newTokenB) + ss.SetAttackValue(newTokenB) + vsa2 := restapi.ScanRestAPI(url, ss) r.AddScanAttempt(vsa2) r.End() diff --git a/scan/jwt/null_signature.go b/scan/jwt/null_signature.go index 5da2dd8..c0d07bb 100644 --- a/scan/jwt/null_signature.go +++ b/scan/jwt/null_signature.go @@ -3,8 +3,9 @@ package jwt import ( "strings" + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" "github.com/cerberauth/vulnapi/report" - restapi "github.com/cerberauth/vulnapi/scan/rest_api" ) const ( @@ -23,14 +24,16 @@ func createNewJWTWithoutSignature(originalTokenString string) (string, error) { return strings.Join([]string{parts[0], parts[1], ""}, "."), nil } -func NullSignatureScanHandler(url string, token string) (*report.ScanReport, error) { +func NullSignatureScanHandler(url string, ss auth.SecurityScheme) (*report.ScanReport, error) { r := report.NewScanReport() + token := ss.GetValidValue().(string) newToken, err := createNewJWTWithoutSignature(token) if err != nil { return r, err } - vsa := restapi.ScanRestAPI(url, newToken) + ss.SetAttackValue(newToken) + vsa := restapi.ScanRestAPI(url, ss) r.AddScanAttempt(vsa).End() if vsa.Response.StatusCode < 300 { diff --git a/scan/jwt/weak_secret.go b/scan/jwt/weak_secret.go index 2094f90..1a9a3e6 100644 --- a/scan/jwt/weak_secret.go +++ b/scan/jwt/weak_secret.go @@ -1,8 +1,9 @@ package jwt import ( + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" "github.com/cerberauth/vulnapi/report" - restapi "github.com/cerberauth/vulnapi/scan/rest_api" ) const ( @@ -11,14 +12,16 @@ const ( WeakSecretVulnerabilityDescription = "JWT is signed with a weak secret allowing attackers to issue valid JWT." ) -func BlankSecretScanHandler(url string, token string) (*report.ScanReport, error) { +func BlankSecretScanHandler(url string, ss auth.SecurityScheme) (*report.ScanReport, error) { r := report.NewScanReport() + token := ss.GetValidValue().(string) newToken, err := createNewJWTWithClaims(token, []byte("")) if err != nil { return r, err } - vsa := restapi.ScanRestAPI(url, newToken) + ss.SetAttackValue(newToken) + vsa := restapi.ScanRestAPI(url, ss) r.AddScanAttempt(vsa).End() if vsa.Response.StatusCode < 300 { @@ -32,7 +35,7 @@ func BlankSecretScanHandler(url string, token string) (*report.ScanReport, error return r, nil } -func DictSecretScanHandler(url string, token string) (*report.ScanReport, error) { +func DictSecretScanHandler(url string, ss auth.SecurityScheme) (*report.ScanReport, error) { r := report.NewScanReport() // Use a dictionary attack to try finding the secret diff --git a/scan/rest_api/request.go b/scan/rest_api/request.go deleted file mode 100644 index cd5b1d7..0000000 --- a/scan/rest_api/request.go +++ /dev/null @@ -1,25 +0,0 @@ -package restapi - -import ( - "fmt" - - "github.com/cerberauth/vulnapi/internal/request" - "github.com/cerberauth/vulnapi/report" -) - -func ScanRestAPI(url string, token string) *report.VulnerabilityScanAttempt { - req, resp, err := request.SendRequestWithBearerAuth(url, token) - if err != nil { - err = fmt.Errorf("request with url %s has an unexpected error", err) - } - - if resp.StatusCode < 200 && resp.StatusCode >= 300 { - err = fmt.Errorf("unexpected status code %d during test request", resp.StatusCode) - } - - return &report.VulnerabilityScanAttempt{ - Request: req, - Response: resp, - Err: err, - } -} diff --git a/scan/scan.go b/scan/scan.go index b47efe1..404d16d 100644 --- a/scan/scan.go +++ b/scan/scan.go @@ -1,29 +1,38 @@ package scan import ( - "errors" - + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" "github.com/cerberauth/vulnapi/report" - restapi "github.com/cerberauth/vulnapi/scan/rest_api" + "github.com/getkin/kin-openapi/openapi3" ) -type ScanHandler func(url string, jwt string) (*report.ScanReport, error) +type ScanHandler func(url string, ss auth.SecurityScheme) (*report.ScanReport, error) + +type ScanOptions struct { + Url string + OpenAPIDoc *openapi3.T +} type Scan struct { - url string - validJwt *string - pendingScans []ScanHandler - reporter *report.Reporter + opts ScanOptions + securitySchemes []auth.SecurityScheme + pendingScans []ScanHandler + reporter *report.Reporter } -func NewScanner(url string, valid_jwt *string) *Scan { +func NewScanner(opts ScanOptions) *Scan { return &Scan{ + opts: opts, + reporter: report.NewReporter(), - url: url, - validJwt: valid_jwt, } } +func (s *Scan) AddSecurityScheme(ss auth.SecurityScheme) { + s.securitySchemes = append(s.securitySchemes, ss) +} + func (s *Scan) AddPendingScanHandler(sh ScanHandler) *Scan { s.pendingScans = append(s.pendingScans, sh) @@ -37,7 +46,7 @@ func (s *Scan) Execute() (*report.Reporter, []error, error) { var errors []error for i := 0; i < len(s.pendingScans); i++ { - rep, err := s.pendingScans[i](s.url, *s.validJwt) + rep, err := s.pendingScans[i](s.opts.Url, s.securitySchemes[0]) if err != nil { errors = append(errors, err) @@ -50,11 +59,7 @@ func (s *Scan) Execute() (*report.Reporter, []error, error) { } func (s *Scan) ValidateRequest() error { - if s.validJwt == nil { - return errors.New("no valid JWT provided") - } - - r := restapi.ScanRestAPI(s.url, *s.validJwt) + r := restapi.ScanRestAPI(s.opts.Url, s.securitySchemes[0]) if r.Err != nil { return r.Err }