Skip to content

Commit

Permalink
feat: add openapi scan support
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelgautier committed Nov 3, 2023
1 parent 2f79e0e commit 40610b7
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 44 deletions.
30 changes: 22 additions & 8 deletions cmd/scan/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand All @@ -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
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ 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/spf13/cobra v1.7.0
)

require (
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/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
46 changes: 46 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,58 @@
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/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/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=
39 changes: 39 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 Header struct {
Name string
Value string
}

type SecurityScheme interface {
GetHeaders() []Header
GetCookies() []*http.Cookie
GetValidValue() interface{}
SetAttackValue(v interface{})
GetAttackValue() interface{}
}
51 changes: 51 additions & 0 deletions internal/auth/bearer.go
Original file line number Diff line number Diff line change
@@ -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() []Header {
return []Header{
{
Name: "Authorization",
Value: fmt.Sprintf("Bearer %s", *ss.ValidValue),
},
}
}

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
}
13 changes: 10 additions & 3 deletions internal/request/request.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package request

import (
"fmt"
"net/http"

"github.com/cerberauth/vulnapi/internal/auth"
)

func prepareVulnAPIRequest(method string, url string) (*http.Request, error) {
Expand All @@ -16,13 +17,19 @@ func prepareVulnAPIRequest(method string, url string) (*http.Request, error) {
return req, nil
}

func SendRequestWithBearerAuth(url string, token string) (*http.Request, *http.Response, error) {
func SendRequestWithBearerAuth(url string, ss auth.SecurityScheme) (*http.Request, *http.Response, error) {
req, err := prepareVulnAPIRequest("GET", url)
if err != nil {
return req, nil, err
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
for _, c := range ss.GetCookies() {
req.AddCookie(c)
}

for _, h := range ss.GetHeaders() {
req.Header.Add(h.Name, h.Value)
}

client := &http.Client{}
resp, err := client.Do(req)
Expand Down
34 changes: 34 additions & 0 deletions internal/rest_api/loader.go
Original file line number Diff line number Diff line change
@@ -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)
}
5 changes: 3 additions & 2 deletions scan/rest_api/request.go → internal/rest_api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package restapi
import (
"fmt"

"github.com/cerberauth/vulnapi/internal/auth"
"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)
func ScanRestAPI(url string, ss auth.SecurityScheme) *report.VulnerabilityScanAttempt {
req, resp, err := request.SendRequestWithBearerAuth(url, nil)
if err != nil {
err = fmt.Errorf("request with url %s has an unexpected error", err)
}
Expand Down
9 changes: 6 additions & 3 deletions scan/jwt/alg_none.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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 {
Expand Down
12 changes: 8 additions & 4 deletions scan/jwt/not_verified.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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 {
Expand All @@ -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()
Expand Down
Loading

0 comments on commit 40610b7

Please sign in to comment.