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 Feb 12, 2024
1 parent a69d0b8 commit 1ace587
Show file tree
Hide file tree
Showing 29 changed files with 1,361 additions and 123 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ You can test the scanner against example [vulnerability challenges](https://gith

Run `vulnapi -h` or `vulnapi help`.

This projects support two way to scan APIs:
- CURL Like scan
- OpenAPI based Scan (experimental)

## Disclaimer

This scanner is provided for educational and informational purposes only. It should not be used for malicious purposes or to attack any system without proper authorization. Always respect the security and privacy of others.
Expand Down
70 changes: 70 additions & 0 deletions cmd/scan/curl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package scan

import (
"log"
"net/http"
"strings"

"github.com/cerberauth/vulnapi/scan"
"github.com/spf13/cobra"
)

var (
url string
method string
headers []string
cookies []string
)

func NewCURLScanCmd() (scanCmd *cobra.Command) {
scanCmd = &cobra.Command{
Use: "curl [URL]",
Short: "URL Scan in CURL style",
Args: cobra.ExactArgs(1),
FParseErrWhitelist: cobra.FParseErrWhitelist{
UnknownFlags: true,
},
Run: func(cmd *cobra.Command, args []string) {
url = args[0]

httpHeaders := http.Header{}
for _, h := range headers {
parts := strings.SplitN(h, ":", 2)
httpHeaders.Add(parts[0], strings.TrimLeft(parts[1], " "))
}

var httpCookies []http.Cookie
for _, c := range cookies {
parts := strings.SplitN(c, ":", 2)
httpCookies = append(httpCookies, http.Cookie{
Name: parts[0],
Value: parts[1],
})
}

scan, err := scan.NewURLScan(method, url, &httpHeaders, httpCookies, nil)
if err != nil {
log.Fatal(err)
}

rpr, _, err := scan.WithAllVulnsScans().Execute()
if err != nil {
log.Fatal(err)
}

if !rpr.HasVulnerability() {
log.Println("Congratulations! No vulnerability has been discovered!")
}

for _, r := range rpr.GetVulnerabilityReports() {
log.Fatalln(r)
}
},
}

scanCmd.PersistentFlags().StringVarP(&method, "request", "X", "GET", "Specify request method to use")
scanCmd.PersistentFlags().StringArrayVarP(&headers, "header", "H", nil, "Pass custom header(s) to target API")
scanCmd.PersistentFlags().StringArrayVarP(&cookies, "cookie", "b", nil, "Send cookies from string")

return scanCmd
}
62 changes: 62 additions & 0 deletions cmd/scan/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package scan

import (
"bufio"
"log"
"os"

"github.com/cerberauth/vulnapi/scan"
"github.com/spf13/cobra"
)

func isStdinOpen() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0
}

func readStdin() *string {
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
t := scanner.Text()
return &t
}

return nil
}

func NewOpenAPIScanCmd() (scanCmd *cobra.Command) {
scanCmd = &cobra.Command{
Use: "openapi [OpenAPIPAth]",
Short: "Full OpenAPI operations scan",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
openapiUrlOrPath := args[0]

var validToken *string
if isStdinOpen() {
stdin := readStdin()
validToken = stdin
}

scan, err := scan.NewOpenAPIScan(openapiUrlOrPath, validToken, nil)
if err != nil {
log.Fatal(err)
}

rpr, _, err := scan.WithAllVulnsScans().Execute()
if err != nil {
log.Fatal(err)
}

if !rpr.HasVulnerability() {
log.Println("Congratulations! No vulnerability has been discovered!")
}

for _, r := range rpr.GetVulnerabilityReports() {
log.Fatalln(r)
}
},
}

return scanCmd
}
44 changes: 3 additions & 41 deletions cmd/scan/root.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,16 @@
package scan

import (
"bufio"
"fmt"
"log"

"github.com/cerberauth/vulnapi/scan"
"github.com/spf13/cobra"
)

var (
url string
jwt string
)

func NewScanCmd() (scanCmd *cobra.Command) {
scanCmd = &cobra.Command{
Use: "scan [URL]",
Use: "scan [type]",
Short: "API Scan",
// Full API scan coming (not only one URL)
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
url = args[0]
}

if jwt == "" {
stdin, err := bufio.NewReader(cmd.InOrStdin()).ReadString('\n')
if err != nil {
log.Fatal(fmt.Errorf("failed process input: %v", err))
}
jwt = stdin
}

rpr, _, err := scan.NewScanner(url, &jwt).WithAllScans().Execute()
if err != nil {
log.Fatal(err)
}

if !rpr.HasVulnerability() {
println("Congratulations! No vulnerability has been discovered!")
}

for _, r := range rpr.GetVulnerabilityReports() {
log.Println(r)
}
},
}

scanCmd.PersistentFlags().StringVarP(&url, "url", "u", "", "URL")
scanCmd.PersistentFlags().StringVarP(&jwt, "jwt", "j", "", "Valid JWT")
scanCmd.AddCommand(NewCURLScanCmd())
scanCmd.AddCommand(NewOpenAPIScanCmd())

return scanCmd
}
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@ module github.com/cerberauth/vulnapi
go 1.22

require (
github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/getkin/kin-openapi v0.120.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/jarcoal/httpmock v1.3.1
github.com/spf13/cobra v1.8.0
github.com/std-uritemplate/std-uritemplate/go v0.0.52
github.com/stretchr/testify v1.8.4
)

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
)
54 changes: 54 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
github.com/cpuguy83/go-md2man/v2 v2.0.3/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.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.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.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
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/std-uritemplate/std-uritemplate/go v0.0.52 h1:2r8rdugq0WZlRDkLlwH/9sKZG2iYXvFCEcKFIKmfSQQ=
github.com/std-uritemplate/std-uritemplate/go v0.0.52/go.mod h1:CLZ1543WRCuUQQjK0BvPM4QrG2toY8xNZUm8Vbt7vTc=
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
29 changes: 29 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package auth

import "net/http"

type Type string

const (
HttpType Type = "http"
OAuth2 Type = "oauth2"
OpenIdConnect Type = "openIdConnect"
ApiKey Type = "apiKey"
)

type SecurityScheme interface {
GetHeaders() http.Header
GetCookies() []*http.Cookie
GetValidValue() interface{}
SetAttackValue(v interface{})
GetAttackValue() interface{}
}

type Operation struct {
Url string
Method string
Headers *http.Header
Cookies []http.Cookie

SecuritySchemes []SecurityScheme
}
53 changes: 53 additions & 0 deletions internal/auth/bearer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package auth

import (
"fmt"
"net/http"
)

type BearerSecurityScheme struct {
Type Type
Scheme SchemeName
In SchemeIn
Name string
ValidValue *string
AttackValue string
}

var _ SecurityScheme = (*BearerSecurityScheme)(nil)

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(AuthorizationHeader, fmt.Sprintf("%s %s", BearerPrefix, *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
}
4 changes: 4 additions & 0 deletions internal/auth/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package auth

const AuthorizationHeader = "Authorization"
const BearerPrefix = "Bearer"
Loading

0 comments on commit 1ace587

Please sign in to comment.