mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-08 17:09:46 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eea0f8a77 | ||
|
|
bdea5822e8 | ||
|
|
7bb228d81c | ||
|
|
3258ca341f | ||
|
|
58d4e91760 | ||
|
|
1f5a946011 | ||
|
|
143fdfda1b | ||
|
|
a33eef47f4 | ||
|
|
4c466112ee | ||
|
|
1195ff7fbf | ||
|
|
bd3265d0a4 |
No files matched your search
@@ -37,28 +37,72 @@ $ export PATH=$PATH:~/go/bin
|
||||
> Use the `automate` command to send a series of requests to each defined endpoint and analyze the status code of each response.
|
||||
|
||||
```bash
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
|
||||
go run . automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
|
||||
|
||||
INFO[0000] Gathering API details.
|
||||
Gathering API details.
|
||||
Title: Swagger Petstore
|
||||
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/pet/findByTags
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=/v2/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/store/order
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/user/createWithList
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/user/login
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/user/logout
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=/v2/v2/pet/1/uploadImage
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/pet
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=/v2/v2/pet
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/pet/findByStatus
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/store/inventory
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/store/order/1
|
||||
ERRO Endpoint not found. Method=GET Status=404 Target=/v2/v2/user/bishopfox
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=/v2/v2/user/bishopfox
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/user/createWithArray
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/user
|
||||
✓ GET 200 /v2/pet/findByStatus
|
||||
✓ GET 200 /v2/user/logout
|
||||
⚠ POST 400 /v2/user/createWithArray
|
||||
⚠ POST 400 /v2/store/order
|
||||
✗ GET 404 /v2/store/order/1
|
||||
⚠ POST 400 /v2/pet
|
||||
⚠ PUT 415 /v2/pet
|
||||
⚠ POST 400 /v2/user/createWithList
|
||||
✗ GET 404 /v2/user/bishopfox
|
||||
⚠ PUT 415 /v2/user/bishopfox
|
||||
⚠ POST 400 /v2/user
|
||||
⚠ POST 415 /v2/pet/1/uploadImage
|
||||
✓ GET 200 /v2/pet/findByTags
|
||||
✗ GET 404 /v2/pet/1
|
||||
⚠ POST 415 /v2/pet/1
|
||||
✓ GET 200 /v2/store/inventory
|
||||
✓ GET 200 /v2/user/login
|
||||
```
|
||||
|
||||
You can also request verbose output to see the partial (or full) response:
|
||||
|
||||
```bash
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080 -v
|
||||
|
||||
Gathering API details.
|
||||
Title: Swagger Petstore
|
||||
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
|
||||
✗ GET 404 /v2/user/bishopfox
|
||||
{"code":1,"type":"error","message":"User not found
|
||||
⚠ PUT 415 /v2/user/bishopfox
|
||||
{"code":415,"type":"unknown","message":"com.sun.je
|
||||
✓ GET 200 /v2/user/logout
|
||||
{"code":200,"type":"unknown","message":"ok"}
|
||||
⚠ POST 400 /v2/user/createWithArray
|
||||
{"code":400,"type":"unknown","message":"bad input"
|
||||
⚠ POST 400 /v2/user/createWithList
|
||||
{"code":400,"type":"unknown","message":"bad input"
|
||||
✗ GET 404 /v2/pet/1
|
||||
{"code":1,"type":"error","message":"Pet not found"
|
||||
⚠ POST 415 /v2/pet/1
|
||||
{"code":415,"type":"unknown"}
|
||||
✓ GET 200 /v2/store/inventory
|
||||
{"sold":117,"string":26,"invalidStatus":1,"-1":1,"
|
||||
⚠ POST 400 /v2/store/order
|
||||
{"code":400,"type":"unknown","message":"bad input"
|
||||
✓ GET 200 /v2/user/login
|
||||
{"code":200,"type":"unknown","message":"logged in
|
||||
⚠ POST 400 /v2/pet
|
||||
{"code":400,"type":"unknown","message":"bad input"
|
||||
⚠ PUT 415 /v2/pet
|
||||
{"code":415,"type":"unknown","message":"com.sun.je
|
||||
✓ GET 200 /v2/pet/findByStatus
|
||||
[]
|
||||
✓ GET 200 /v2/pet/findByTags
|
||||
[]
|
||||
✗ GET 404 /v2/store/order/1
|
||||
{"code":1,"type":"error","message":"Order not foun
|
||||
⚠ POST 400 /v2/user
|
||||
{"code":400,"type":"unknown","message":"bad input"
|
||||
⚠ POST 415 /v2/pet/1/uploadImage
|
||||
{"code":415,"type":"unknown"}
|
||||
```
|
||||
|
||||
> Use the `prepare` command to prepare a list of commands for manual testing. Currently supports both `curl` and `sqlmap`. You will likely have to modify these slightly.
|
||||
|
||||
+24
-20
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -45,30 +44,35 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
if schemeType := scheme["scheme"]; schemeType != nil {
|
||||
switch schemeType {
|
||||
case "basic":
|
||||
fmt.Println("Basic Authentication is accepted. Supply a username and password? (y/N)")
|
||||
fmt.Scanln(&autoApplyBasicAuth)
|
||||
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
|
||||
if autoApplyBasicAuth == "y" {
|
||||
fmt.Printf("Enter a username.")
|
||||
fmt.Scanln(&basicAuthUser)
|
||||
fmt.Printf("Enter a password.")
|
||||
fmt.Scanln(&basicAuthPass)
|
||||
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
|
||||
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
|
||||
log.Infof("Using %s as the Basic Auth value.", basicAuthString)
|
||||
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
|
||||
if quiet {
|
||||
autoApplyBasicAuth = "n"
|
||||
printWarn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
} else {
|
||||
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
fmt.Println("Basic Authentication is accepted. Supply a username and password? (y/N)")
|
||||
fmt.Scanln(&autoApplyBasicAuth)
|
||||
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
|
||||
if autoApplyBasicAuth == "y" {
|
||||
fmt.Printf("Enter a username.")
|
||||
fmt.Scanln(&basicAuthUser)
|
||||
fmt.Printf("Enter a password.")
|
||||
fmt.Scanln(&basicAuthPass)
|
||||
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
|
||||
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
|
||||
printInfo("Using %s as the Basic Auth value.\n", basicAuthString)
|
||||
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
|
||||
} else {
|
||||
printWarn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
}
|
||||
}
|
||||
case "bearer":
|
||||
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
|
||||
printWarn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
|
||||
}
|
||||
}
|
||||
case "apiKey":
|
||||
if inVal, ok := scheme["in"].(string); ok {
|
||||
switch inVal {
|
||||
case "query":
|
||||
log.Infof("An API key can be provided via a parameter string. Would you like to apply one? (y/N)")
|
||||
printInfo("An API key can be provided via a parameter string. Would you like to apply one? (y/N)\n")
|
||||
if quiet {
|
||||
autoApplyAPIKey = "n"
|
||||
} else {
|
||||
@@ -82,11 +86,11 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
}
|
||||
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
|
||||
fmt.Scanln(&apiKey)
|
||||
log.Infof("Using %s=%s as the API key in all requests.", apiKeyName, apiKey)
|
||||
printInfo("Using %s=%s as the API key in all requests.\n", apiKeyName, apiKey)
|
||||
}
|
||||
case "header":
|
||||
if mechanism == "bearer" {
|
||||
log.Infof("A bearer token is accepted. Would you like to provide one? (y/N)")
|
||||
printInfo("A bearer token is accepted. Would you like to provide one? (y/N)\n")
|
||||
if quiet {
|
||||
autoApplyBearer = "n"
|
||||
} else {
|
||||
@@ -98,11 +102,11 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
fmt.Scanln(&bearerToken)
|
||||
Headers = append(Headers, "Authorization: Bearer "+bearerToken)
|
||||
} else {
|
||||
log.Warn("A bearer token is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
printWarn("A bearer token is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
}
|
||||
} else {
|
||||
if nameVal, ok := scheme["name"].(string); ok {
|
||||
log.Infof("An API key can be provided via the header %s. Would you like to apply one? (y/N)", nameVal)
|
||||
printInfo("An API key can be provided via the header %s. Would you like to apply one? (y/N)\n", nameVal)
|
||||
if quiet {
|
||||
autoApplyAPIKey = "n"
|
||||
} else {
|
||||
|
||||
+11
-6
@@ -5,10 +5,9 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var getAccessibleEndpoints bool
|
||||
@@ -26,7 +25,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if outfile != "" && strings.ToLower(outputFormat) != "" {
|
||||
if !strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) != "json" {
|
||||
log.Fatal("Only the JSON output format is supported at the moment.")
|
||||
die("Only the JSON output format is supported at the moment.")
|
||||
} else if strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) == "console" {
|
||||
outputFormat = "json"
|
||||
}
|
||||
@@ -40,17 +39,23 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.Parse("2006-01-02", customDate)
|
||||
if err != nil {
|
||||
fmt.Println("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n")
|
||||
printInfo("Gathering API details.\n")
|
||||
}
|
||||
|
||||
if swaggerURL != "" {
|
||||
@@ -58,7 +63,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening file:", err)
|
||||
die("Error opening file: %v", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
|
||||
+21
-20
@@ -16,8 +16,6 @@ import (
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var endpointOnly bool
|
||||
@@ -36,13 +34,13 @@ var bruteCmd = &cobra.Command{
|
||||
|
||||
/* // NEED TO RE-IMPLEMENT RATE LIMIT
|
||||
if rateLimit <= 0 {
|
||||
log.Fatal("Invalid rate supplied. Must be a positive number")
|
||||
die("Invalid rate supplied. Must be a positive number")
|
||||
}
|
||||
*/
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +49,7 @@ var bruteCmd = &cobra.Command{
|
||||
var allURLs []string
|
||||
u, err := url.Parse(swaggerURL)
|
||||
if err != nil {
|
||||
log.Warnf("Error parsing URL:%s\n", err)
|
||||
printWarn("Error parsing URL: %s", err)
|
||||
}
|
||||
target := u.Scheme + "://" + u.Host
|
||||
if endpointWordlist == "" {
|
||||
@@ -63,7 +61,7 @@ var bruteCmd = &cobra.Command{
|
||||
} else {
|
||||
endpointList, err := os.Open(endpointWordlist)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
die("failed to open file: %s", err)
|
||||
}
|
||||
defer endpointList.Close()
|
||||
|
||||
@@ -75,37 +73,37 @@ var bruteCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("failed to read words from file: %s", err)
|
||||
die("failed to read words from file: %s", err)
|
||||
}
|
||||
}
|
||||
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
|
||||
log.Infof("Sending %d requests at a rate of %d requests per second. This could take a while...\n", len(allURLs), rateLimit)
|
||||
printInfo("Sending %d requests at a rate of %d requests per second. This could take a while...\n", len(allURLs), rateLimit)
|
||||
} else {
|
||||
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
|
||||
printInfo("Sending %d requests. This could take a while...\n", len(allURLs))
|
||||
}
|
||||
|
||||
specFound, definitionFile := findDefinitionFile(allURLs, client)
|
||||
if specFound {
|
||||
definedOperations, err := json.Marshal(definitionFile)
|
||||
if err != nil {
|
||||
log.Errorf("Error parsing definition file:%s\n", err)
|
||||
printErr("Error parsing definition file: %s", err)
|
||||
}
|
||||
|
||||
if outfile != "" {
|
||||
|
||||
file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("Error opening file: %s\n", err)
|
||||
printErr("Error opening file: %s", err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(definedOperations)
|
||||
if err != nil {
|
||||
log.Errorf("Error writing file: %s\n", err)
|
||||
printErr("Error writing file: %s", err)
|
||||
} else {
|
||||
f, _ := filepath.Abs(outfile)
|
||||
log.Infof("Wrote file to %s\n", f)
|
||||
printInfo("Wrote file to %s\n", f)
|
||||
}
|
||||
} else {
|
||||
if endpointOnly {
|
||||
@@ -116,7 +114,7 @@ var bruteCmd = &cobra.Command{
|
||||
}
|
||||
// TODO: Check if (future implementation) automate flag is true and if so than call the 'sj automate' command with the discovered definition file.
|
||||
} else {
|
||||
log.Errorf("\nNo definition file found for:\t%s\n", swaggerURL)
|
||||
printErr("\nNo definition file found for:\t%s", swaggerURL)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -156,7 +154,7 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
checkSpec := UnmarshalSpec(bodyBytes)
|
||||
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
|
||||
fmt.Println("")
|
||||
log.Infof("Definition file found: %s\n", url)
|
||||
printInfo("Definition file found: %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
@@ -166,10 +164,13 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
log.Infof("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
printInfo("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
@@ -212,7 +213,7 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
bodyBytes = []byte(spec[openApiIndex:specClose])
|
||||
_ = json.Unmarshal(bodyBytes, &doc2)
|
||||
if !strings.Contains(doc2.Swagger, "2") {
|
||||
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"openapi":"3`) {
|
||||
@@ -228,11 +229,11 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
bodyBytes = []byte(spec[openApiIndex:specClose])
|
||||
_ = json.Unmarshal(bodyBytes, &doc3)
|
||||
if !strings.Contains(doc3.OpenAPI, "3") {
|
||||
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
}
|
||||
|
||||
return bodyBytes
|
||||
@@ -266,7 +267,7 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
|
||||
var noDoc openapi3.T
|
||||
return &noDoc
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.")
|
||||
die("Error parsing definition file.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+11
-12
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/getkin/kin-openapi/openapi2"
|
||||
"github.com/getkin/kin-openapi/openapi2conv"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -26,7 +25,7 @@ var convertCmd = &cobra.Command{
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +33,7 @@ var convertCmd = &cobra.Command{
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
printInfo("Gathering API details.\n\n")
|
||||
}
|
||||
|
||||
if swaggerURL != "" {
|
||||
@@ -42,7 +41,7 @@ var convertCmd = &cobra.Command{
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening definition file:", err)
|
||||
die("Error opening definition file: %v", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
@@ -61,7 +60,7 @@ var convertCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if strings.HasPrefix(doc3.OpenAPI, "3") {
|
||||
log.Warnln("Definition file is already version 3.")
|
||||
printWarn("Definition file is already version 3.")
|
||||
WriteConvertedDefinitionFile(bodyBytes)
|
||||
} else if strings.HasPrefix(doc.Swagger, "2") {
|
||||
newDoc, err := openapi2conv.ToV3(&doc)
|
||||
@@ -72,11 +71,11 @@ var convertCmd = &cobra.Command{
|
||||
switch format {
|
||||
case "json":
|
||||
if strings.HasSuffix(outfile, "yaml") || strings.HasSuffix(outfile, "yml") {
|
||||
log.Warn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
|
||||
printWarn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
|
||||
}
|
||||
converted, err := json.Marshal(newDoc)
|
||||
if err != nil {
|
||||
log.Fatal("Error converting definition file to v3:", err)
|
||||
die("Error converting definition file to v3: %v", err)
|
||||
}
|
||||
if !strings.HasSuffix(string(converted), "}") {
|
||||
if outfile == "" {
|
||||
@@ -95,7 +94,7 @@ var convertCmd = &cobra.Command{
|
||||
case "yaml", "yml":
|
||||
converted, err := yaml.Marshal(newDoc)
|
||||
if err != nil {
|
||||
log.Fatal("Error converting definition file to v3:", err)
|
||||
die("Error converting definition file to v3: %v", err)
|
||||
}
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted))
|
||||
@@ -105,7 +104,7 @@ var convertCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.")
|
||||
die("Error parsing definition file.")
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -113,16 +112,16 @@ var convertCmd = &cobra.Command{
|
||||
func WriteConvertedDefinitionFile(data []byte) {
|
||||
file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("Error opening file: %s\n", err)
|
||||
printErr("Error opening file: %s", err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
log.Errorf("Error writing file: %s\n", err)
|
||||
printErr("Error writing file: %s", err)
|
||||
} else {
|
||||
f, _ := filepath.Abs(outfile)
|
||||
log.Infof("Wrote file to %s\n", f)
|
||||
printInfo("Wrote file to %s\n", f)
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -17,7 +16,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +25,15 @@ This list contains the raw endpoints (parameter values will not be appended or m
|
||||
var bodyBytes []byte
|
||||
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering endpoints.\n\n")
|
||||
printInfo("Gathering endpoints.\n\n")
|
||||
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening file:", err)
|
||||
die("Error opening file: %v", err)
|
||||
}
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
|
||||
+105
-180
@@ -1,11 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
@@ -22,11 +23,35 @@ type VerboseResult struct {
|
||||
Curl string `json:"curl"`
|
||||
}
|
||||
|
||||
var tempLogger *log.Logger
|
||||
// Diagnostic helpers — all write to stderr so stdout stays clean for piping.
|
||||
|
||||
var green = color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
var yellow = color.New(color.FgYellow, color.Bold).SprintFunc()
|
||||
var red = color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
var faint = color.New(color.Faint).SprintFunc()
|
||||
|
||||
func printInfo(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
|
||||
func printWarn(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", yellow("[!]"), msg)
|
||||
}
|
||||
|
||||
func printErr(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", red("[✗]"), msg)
|
||||
}
|
||||
|
||||
func die(format string, args ...interface{}) {
|
||||
printErr(format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// writeLog is the main dispatch function for endpoint results.
|
||||
func writeLog(sc int, target, method, errorMsg, response string) {
|
||||
var file *os.File
|
||||
tempLogger = log.New()
|
||||
var out io.Writer = os.Stdout
|
||||
tempResponsePreviewLength := responsePreviewLength
|
||||
|
||||
if len(response) < responsePreviewLength {
|
||||
@@ -36,200 +61,100 @@ func writeLog(sc int, target, method, errorMsg, response string) {
|
||||
if outfile != "" {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Output file does not exist or cannot be created")
|
||||
fmt.Fprintln(os.Stderr, "Output file does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tempLogger.SetOutput(file)
|
||||
}
|
||||
|
||||
if outputFormat == "console" {
|
||||
if strings.Contains(response, "\"") {
|
||||
tempLogger.SetFormatter(&log.TextFormatter{DisableQuote: true, DisableTimestamp: true})
|
||||
} else {
|
||||
tempLogger.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
|
||||
}
|
||||
} else {
|
||||
tempLogger.SetFormatter(&log.JSONFormatter{DisableHTMLEscape: true, DisableTimestamp: true, PrettyPrint: true})
|
||||
defer file.Close()
|
||||
out = file
|
||||
// Disable color when writing to a file.
|
||||
color.NoColor = true
|
||||
defer func() { color.NoColor = false }()
|
||||
}
|
||||
|
||||
preview := ""
|
||||
if verbose {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logVerboseRedirect(sc, target, method, response, tempLogger)
|
||||
} else if sc == 0 {
|
||||
logVerboseBad(sc, target, method, response, tempLogger)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg, tempLogger)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method, tempLogger)
|
||||
} else if sc == 8899 {
|
||||
logVerboseJSON(specTitle, specDescription)
|
||||
} else {
|
||||
logVerboseManual(sc, target, method, errorMsg, response, tempLogger)
|
||||
}
|
||||
} else {
|
||||
logVerboseAccessible(sc, target, method, response, tempLogger)
|
||||
}
|
||||
} else {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logUnauth(sc, target, method, errorMsg, tempLogger)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method, tempLogger)
|
||||
} else if sc == 0 {
|
||||
logBad(sc, target, method, tempLogger)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg, tempLogger)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method, tempLogger)
|
||||
} else if sc == 8899 {
|
||||
logJSON(specTitle, specDescription)
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg, tempLogger)
|
||||
}
|
||||
} else {
|
||||
if sc == 8899 {
|
||||
logJSON(specTitle, specDescription)
|
||||
} else {
|
||||
logAccessible(sc, target, method, tempLogger)
|
||||
}
|
||||
}
|
||||
preview = response[:responsePreviewLength]
|
||||
}
|
||||
|
||||
switch sc {
|
||||
case 8899:
|
||||
if verbose {
|
||||
logVerboseJSON(specTitle, specDescription, out)
|
||||
} else {
|
||||
logJSON(specTitle, specDescription, out)
|
||||
}
|
||||
default:
|
||||
logResult(sc, target, method, errorMsg, preview, out)
|
||||
}
|
||||
|
||||
responsePreviewLength = tempResponsePreviewLength
|
||||
file.Close()
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
// logResult renders a single endpoint result line.
|
||||
func logResult(sc int, target, method, errorMsg, preview string, out io.Writer) {
|
||||
var sym string
|
||||
var painter func(a ...interface{}) string
|
||||
|
||||
func logVerboseAccessible(status int, target, method, response string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
|
||||
func logDangerous(target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": "skipped",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn("Endpoint skipped due to dangerous keyword (or request cancelled due to timeout).")
|
||||
}
|
||||
|
||||
func logManual(status int, target, method, errorMsg string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Manual testing may be required."
|
||||
switch sc {
|
||||
case 200:
|
||||
sym = "✓"
|
||||
painter = green
|
||||
case 301, 302, 0, 1:
|
||||
sym = "⚠"
|
||||
painter = yellow
|
||||
case 401, 403, 404:
|
||||
sym = "✗"
|
||||
painter = red
|
||||
default:
|
||||
sym = "⚠"
|
||||
painter = yellow
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logVerboseManual(status int, target, method, errorMsg, response string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Manual testing may be required."
|
||||
statusStr := fmt.Sprintf("%d", sc)
|
||||
switch sc {
|
||||
case 0:
|
||||
statusStr = "N/A"
|
||||
case 1:
|
||||
statusStr = "---"
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logNotFound(status int, target, method, errorMsg string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Endpoint not found."
|
||||
line := fmt.Sprintf("%s %-7s %-3s %s\n",
|
||||
painter(sym),
|
||||
painter(method),
|
||||
painter(statusStr),
|
||||
target,
|
||||
)
|
||||
fmt.Fprint(out, line)
|
||||
|
||||
if preview != "" {
|
||||
fmt.Fprintf(out, " %s\n", faint(preview))
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logRedirect(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logVerboseRedirect(status int, target, method, response string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[0:responsePreviewLength],
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logBad(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": "N/A",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logVerboseBad(status int, target, method, response string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": "N/A",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logUnauth(status int, target, method, errorMsg string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
func logJSON(title, description string, out io.Writer) {
|
||||
output := struct {
|
||||
APITitle string `json:"apiTitle"`
|
||||
Description string `json:"description"`
|
||||
Results []Result `json:"results"`
|
||||
}{
|
||||
APITitle: title,
|
||||
Description: description,
|
||||
Results: jsonResultArray,
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
data, _ := json.MarshalIndent(output, "", " ")
|
||||
fmt.Fprintln(out, string(data))
|
||||
}
|
||||
|
||||
func logVerboseUnauth(status int, target, method, errorMsg, response string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
func logVerboseJSON(title, description string, out io.Writer) {
|
||||
output := struct {
|
||||
APITitle string `json:"apiTitle"`
|
||||
Description string `json:"description"`
|
||||
Results []VerboseResult `json:"results"`
|
||||
}{
|
||||
APITitle: title,
|
||||
Description: description,
|
||||
Results: jsonVerboseResultArray,
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logJSON(title, description string) {
|
||||
tempLogger.WithFields(log.Fields{
|
||||
"apiTitle": title,
|
||||
"description": description,
|
||||
"results": jsonResultArray,
|
||||
}).Println("Done")
|
||||
}
|
||||
|
||||
func logVerboseJSON(title, description string) {
|
||||
tempLogger.WithFields(log.Fields{
|
||||
"apiTitle": title,
|
||||
"description": description,
|
||||
"results": jsonVerboseResultArray,
|
||||
}).Println("Done")
|
||||
data, _ := json.MarshalIndent(output, "", " ")
|
||||
fmt.Fprintln(out, string(data))
|
||||
}
|
||||
+10
-4
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -18,21 +18,27 @@ This enables you to test specific API functions for common vulnerabilities or mi
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.Parse("2006-01-02", customDate)
|
||||
if err != nil {
|
||||
fmt.Println("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
printInfo("Gathering API details.\n\n")
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening file:", err)
|
||||
die("Error opening file: %v", err)
|
||||
}
|
||||
|
||||
specBytes, _ := io.ReadAll(specFile)
|
||||
|
||||
+35
-19
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,6 +19,7 @@ var (
|
||||
avoidDangerousRequests string
|
||||
contentType string
|
||||
dangerousStrings []string = []string{"add", "block", "build", "buy", "change", "clear", "create", "delete", "deploy", "destroy", "drop", "edit", "emergency", "erase", "execute", "insert", "modify", "order", "overwrite", "pause", "purchase", "rebuild", "remove", "replace", "reset", "restart", "revoke", "run", "sell", "send", "set", "start", "stop", "update", "upload", "write"}
|
||||
depth int = 0
|
||||
Headers []string
|
||||
requestStatus int
|
||||
riskSurveyed bool = false
|
||||
@@ -59,7 +59,11 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
}
|
||||
|
||||
// Handling of dangerous keywords
|
||||
u, _ := url.Parse(target)
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || u == nil {
|
||||
printWarn("Error parsing URL '%s': %v - skipping request.", target, err)
|
||||
return nil, "", 0
|
||||
}
|
||||
endpoint := u.RawPath + "?" + u.RawQuery
|
||||
for _, v := range dangerousStrings {
|
||||
if os.Args[1] == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
|
||||
@@ -67,12 +71,12 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
if avoidDangerousRequests == "y" {
|
||||
return nil, "", 0
|
||||
} else {
|
||||
log.Warnf("Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
|
||||
printWarn("Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
|
||||
fmt.Scanln(&userChoice)
|
||||
if strings.ToLower(userChoice) != "y" {
|
||||
if !riskSurveyed {
|
||||
avoidDangerousRequests = "y"
|
||||
log.Warnf("Do you want to avoid all dangerous requests? (Y/n)")
|
||||
printWarn("Do you want to avoid all dangerous requests? (Y/n)")
|
||||
fmt.Scanln(&avoidDangerousRequests)
|
||||
avoidDangerousRequests = strings.ToLower(avoidDangerousRequests)
|
||||
riskSurveyed = true
|
||||
@@ -87,14 +91,17 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequest(method, target, reqData)
|
||||
if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
if err != nil {
|
||||
if err != context.Canceled && err != io.EOF {
|
||||
die("Error: could not create HTTP request - %v", err)
|
||||
}
|
||||
return nil, "", 0
|
||||
}
|
||||
|
||||
for i := range Headers {
|
||||
delimIndex := strings.Index(Headers[i], ":")
|
||||
if delimIndex == -1 {
|
||||
log.Warnf("Header provided (%s) cannot be used. Headers must be in 'Key: Value' format (this may be caused by a header declared within the definition file).\n", Headers[i])
|
||||
printWarn("Header provided (%s) cannot be used. Headers must be in 'Key: Value' format (this may be caused by a header declared within the definition file).", Headers[i])
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -138,17 +145,17 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err == context.DeadlineExceeded {
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
printWarn("Error: %s - skipping request.", err)
|
||||
return nil, "", 0
|
||||
} else if err != nil && err != context.Canceled && err != io.EOF {
|
||||
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
|
||||
log.Fatal("Try supplying the --insecure flag.")
|
||||
die("Try supplying the --insecure flag.")
|
||||
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
|
||||
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
|
||||
die("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
|
||||
} else if strings.Contains(fmt.Sprint(err), "user canceled") {
|
||||
return nil, "skipped", 1
|
||||
} else {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
printErr("Error: response not received.\n%v", err)
|
||||
}
|
||||
return nil, "", 0
|
||||
}
|
||||
@@ -156,26 +163,35 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") {
|
||||
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") && depth < 10 {
|
||||
depth += 1
|
||||
redirect, _ := resp.Location()
|
||||
bodyBytes, bodyString, requestStatus = MakeRequest(client, method, redirect.Scheme+"://"+redirect.Host+redirect.Path, timeout, reqData)
|
||||
return bodyBytes, bodyString, requestStatus
|
||||
}
|
||||
|
||||
requestStatus = resp.StatusCode
|
||||
depth = 0
|
||||
|
||||
return bodyBytes, bodyString, requestStatus
|
||||
}
|
||||
|
||||
func CheckContentType(client http.Client, target string) string {
|
||||
u, _ := url.Parse(target)
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || u == nil {
|
||||
printWarn("Error parsing URL '%s': %v - skipping request.", target, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequest("GET", target, nil)
|
||||
if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
if err != nil {
|
||||
if err != context.Canceled && err != io.EOF {
|
||||
die("Error: could not create HTTP request - %v", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// User-Agent handling
|
||||
@@ -189,15 +205,15 @@ func CheckContentType(client http.Client, target string) string {
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err == context.DeadlineExceeded {
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
printWarn("Error: %s - skipping request.", err)
|
||||
return ""
|
||||
} else if err != nil && err != context.Canceled && err != io.EOF {
|
||||
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
|
||||
log.Fatal("Try supplying the --insecure flag.")
|
||||
die("Try supplying the --insecure flag.")
|
||||
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
|
||||
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
|
||||
die("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
|
||||
} else {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
printErr("Error: response not received.\n%v", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,12 +1,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var apiTarget string
|
||||
var basePath string
|
||||
var customDate string
|
||||
var customEmail string
|
||||
var customURL string
|
||||
var format string
|
||||
var insecure bool
|
||||
var localFile string
|
||||
@@ -46,10 +48,10 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
log.Error("Command not specified. See the --help flag for usage.")
|
||||
printErr("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "2.2.0",
|
||||
Version: "2.4.0",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -64,6 +66,9 @@ func init() {
|
||||
rootCmd.AddCommand(convertCmd)
|
||||
rootCmd.PersistentFlags().StringVarP(&UserAgent, "agent", "A", "Swagger Jacker (github.com/BishopFox/sj)", "Set the User-Agent string.")
|
||||
rootCmd.PersistentFlags().StringVarP(&basePath, "base-path", "b", "", "Set the API base path if not defined in the definition file (i.e. /V2/).")
|
||||
rootCmd.PersistentFlags().StringVarP(&customURL, "custom-url", "c", "https://bishopfox.com", "Set a custom URL to test discovered URL parameters.")
|
||||
rootCmd.PersistentFlags().StringVarP(&customDate, "custom-date", "d", "1990-01-01", "A custom date to test discovered date parameters.")
|
||||
rootCmd.PersistentFlags().StringVar(&customEmail, "custom-email", "[email protected]", "A custom email address to test discovered email parameters.")
|
||||
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the definition file (json/yaml/yml/js).")
|
||||
rootCmd.PersistentFlags().StringArrayVarP(&Headers, "headers", "H", nil, "Add custom headers, separated by a colon (\"Name: Value\"). Multiple flags are accepted.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "i", false, "Ignores server certificate validation.")
|
||||
|
||||
+294
-216
@@ -3,7 +3,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -22,113 +20,20 @@ var jsonVerboseResultArray []VerboseResult
|
||||
var specTitle string
|
||||
var specDescription string
|
||||
|
||||
func PrintSpecInfo(spec map[string]interface{}) {
|
||||
info, ok := spec["info"].(map[string]interface{})
|
||||
if !ok || info == nil {
|
||||
log.Info("No information defined in the documentation.")
|
||||
} else {
|
||||
title, ok := info["title"].(string)
|
||||
if ok && title != "" {
|
||||
if outputFormat == "json" {
|
||||
specTitle = title
|
||||
} else {
|
||||
fmt.Printf("Title: %s\n", title)
|
||||
}
|
||||
}
|
||||
|
||||
description, ok := info["description"].(string)
|
||||
if ok && description != "" {
|
||||
if outputFormat == "json" {
|
||||
specDescription = description
|
||||
} else {
|
||||
fmt.Printf("Description: %s\n", description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) {
|
||||
// Ingests the specification file
|
||||
spec := SafelyUnmarshalSpec(bodyBytes)
|
||||
|
||||
// Checks defined security schemes and prompts for authentication
|
||||
CheckSecuritySchemes(spec)
|
||||
|
||||
u, _ := url.Parse(swaggerURL)
|
||||
|
||||
// Gets the target server and base path from the specification file
|
||||
if apiTarget == "" {
|
||||
if v, ok := spec["swagger"].(string); ok && strings.HasPrefix(v, "2") {
|
||||
// Swagger (v2)
|
||||
host, _ := spec["host"].(string)
|
||||
bp, _ := spec["basePath"].(string)
|
||||
if bp == "/" {
|
||||
basePath = ""
|
||||
} else if bp != "" {
|
||||
basePath = bp
|
||||
}
|
||||
|
||||
if host != "" && strings.Contains(host, "://") {
|
||||
apiTarget = host
|
||||
} else {
|
||||
if host != "" {
|
||||
apiTarget = u.Scheme + "://" + host
|
||||
}
|
||||
}
|
||||
} else if v, ok := spec["openapi"].(string); ok && strings.HasPrefix(v, "3") {
|
||||
// OpenAPI (v3)
|
||||
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
|
||||
if len(servers) > 1 {
|
||||
if !quiet && (os.Args[1] != "endpoints") && apiTarget == "" {
|
||||
log.Warn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
|
||||
for i := range servers {
|
||||
if srv, ok := servers[i].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
fmt.Println(serverURL)
|
||||
} else {
|
||||
fmt.Println(apiTarget + serverURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if srv, ok := servers[0].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
apiTarget = serverURL
|
||||
} else if serverURL == "/" {
|
||||
basePath = ""
|
||||
} else {
|
||||
basePath = serverURL
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original host at the target if no server found from specification.
|
||||
if apiTarget == "" {
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
if os.Args[1] != "endpoints" {
|
||||
// Prints Title/Description/Version values if they exist
|
||||
PrintSpecInfo(spec)
|
||||
}
|
||||
|
||||
// Reviews all defined API routes and builds requests as defined
|
||||
BuildRequestsFromPaths(spec, client)
|
||||
type SchemaNode struct {
|
||||
Type string
|
||||
Properties map[string]*SchemaNode
|
||||
Items *SchemaNode
|
||||
Required map[string]bool
|
||||
Enum []interface{}
|
||||
Example interface{}
|
||||
Ref string
|
||||
}
|
||||
|
||||
func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
paths, ok := spec["paths"].(map[string]interface{})
|
||||
if !ok || paths == nil {
|
||||
log.Fatalf("Could not find any defined operations. Review the file manually.")
|
||||
die("Could not find any defined operations. Review the file manually.")
|
||||
}
|
||||
|
||||
var errorDescriptions = make(map[any]string)
|
||||
@@ -271,94 +176,19 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
|
||||
if ct, ok := contentTypes[cType].(map[string]interface{}); ok {
|
||||
if schema, ok := ct["schema"].(map[string]interface{}); ok {
|
||||
if ref, ok := schema["$ref"].(string); ok {
|
||||
if components, ok := spec["components"].(map[string]interface{}); ok {
|
||||
if schemas, ok := components["schemas"].(map[string]interface{}); ok {
|
||||
schemaName := strings.TrimPrefix(ref, "#/components/schemas/")
|
||||
if s, ok := schemas[schemaName].(map[string]interface{}); ok {
|
||||
if properties, ok := s["properties"].(map[string]interface{}); ok {
|
||||
expanded := ExpandSchema(spec, schema, map[string]bool{})
|
||||
example := GenerateExample(expanded)
|
||||
|
||||
for propertyItem := range properties {
|
||||
var pValue string
|
||||
if propertyName, ok := properties[propertyItem].(map[string]interface{}); ok {
|
||||
if propertyType, ok := propertyName["type"].(string); ok {
|
||||
switch propertyType {
|
||||
case "string":
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if propertyName["name"] != "version" {
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
case "int":
|
||||
pValue = "1"
|
||||
default:
|
||||
pValue = "1"
|
||||
}
|
||||
|
||||
switch cType {
|
||||
case "application/json":
|
||||
if strings.Contains(curl, "-d") && strings.Contains(curl, "application/json") {
|
||||
bodyData = ""
|
||||
curl = strings.TrimSuffix(curl, "}'")
|
||||
if propertyType == "string" {
|
||||
bodyData += fmt.Sprintf(",\"%s\":\"%s\"}'", propertyItem, pValue)
|
||||
} else {
|
||||
bodyData += fmt.Sprintf(",\"%s\":%s}'", propertyItem, pValue)
|
||||
}
|
||||
curl += bodyData
|
||||
} else if !strings.Contains(curl, "Content-Type") {
|
||||
if propertyType == "string" {
|
||||
bodyData += fmt.Sprintf("{\"%s\":\"%s\"}", propertyItem, pValue)
|
||||
} else {
|
||||
bodyData += fmt.Sprintf("{\"%s\":%s}", propertyItem, pValue)
|
||||
}
|
||||
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", contentType, bodyData)
|
||||
}
|
||||
case "application/xml", "text/xml":
|
||||
bodyData = ""
|
||||
type Element struct {
|
||||
XMLName xml.Name
|
||||
Content any `xml:",chardata"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Elements []Element `xml:",any"`
|
||||
}
|
||||
|
||||
var elements []Element
|
||||
elements = append(elements, Element{
|
||||
XMLName: xml.Name{Local: propertyItem},
|
||||
Content: pValue,
|
||||
})
|
||||
|
||||
root := Root{
|
||||
Elements: elements,
|
||||
}
|
||||
|
||||
xmlData, err := xml.Marshal(root)
|
||||
if err != nil {
|
||||
log.Warn("Error marshalling XML data.")
|
||||
}
|
||||
|
||||
if strings.Contains(curl, "-d") && strings.Contains(curl, "xml") {
|
||||
curl = strings.TrimSuffix(curl, "</root>'")
|
||||
bodyData = strings.TrimSuffix(bodyData, "</root>'")
|
||||
bodyData += strings.TrimPrefix(string(xmlData), "<root>")
|
||||
curl += bodyData + "'"
|
||||
} else if !strings.Contains(curl, "Content-Type") {
|
||||
bodyData = string(xmlData)
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", contentType, bodyData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cType == "application/json" {
|
||||
bodyBytes, err := json.Marshal(example)
|
||||
if err == nil {
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: application/json\" -d '%s'\"", bodyBytes)
|
||||
}
|
||||
}
|
||||
if cType == "application/xml" || cType == "text/xml" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
xml := XmlFromObject(obj)
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, xml)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,7 +197,11 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
logURL, _ := url.Parse(targetURL)
|
||||
logURL, parseErr := url.Parse(targetURL)
|
||||
if parseErr != nil || logURL == nil {
|
||||
printWarn("Error parsing URL '%s': %v - skipping endpoint.", targetURL, parseErr)
|
||||
continue
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "automate":
|
||||
var postBodyData string
|
||||
@@ -443,13 +277,13 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
if verbose {
|
||||
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &verboseResult)
|
||||
if err != nil {
|
||||
log.Fatal("Error marshalling JSON:", err)
|
||||
die("Error marshalling JSON: %v", err)
|
||||
}
|
||||
jsonVerboseResultArray = append(jsonVerboseResultArray, verboseResult)
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &result)
|
||||
if err != nil {
|
||||
log.Fatal("Error marshalling JSON:", err)
|
||||
die("Error marshalling JSON: %v", err)
|
||||
}
|
||||
jsonResultArray = append(jsonResultArray, result)
|
||||
}
|
||||
@@ -458,6 +292,239 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
newContentType = strings.TrimSpace(newContentType)
|
||||
|
||||
// Remove old 'Content-Type' header
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(h), "content-type:")
|
||||
})
|
||||
|
||||
Headers = append(Headers, "Content-Type: "+newContentType)
|
||||
|
||||
// Remove empty elements to avoid repetitions of "-H ''"
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.TrimSpace(h) == ""
|
||||
})
|
||||
}
|
||||
|
||||
func ExpandSchema(
|
||||
spec map[string]interface{},
|
||||
schema map[string]interface{},
|
||||
visited map[string]bool,
|
||||
) *SchemaNode {
|
||||
if schema == nil {
|
||||
return &SchemaNode{Type: "object"}
|
||||
}
|
||||
|
||||
if ref, ok := schema["$ref"].(string); ok {
|
||||
if visited[ref] {
|
||||
return &SchemaNode{Type: "object"} // break cycle
|
||||
}
|
||||
visited[ref] = true
|
||||
|
||||
resolved := ResolveRef(spec, ref)
|
||||
if resolved == nil {
|
||||
return &SchemaNode{Type: "object"}
|
||||
}
|
||||
return ExpandSchema(spec, resolved, visited)
|
||||
}
|
||||
|
||||
node := &SchemaNode{
|
||||
Properties: map[string]*SchemaNode{},
|
||||
Required: map[string]bool{},
|
||||
}
|
||||
|
||||
if t, ok := schema["type"].(string); ok {
|
||||
node.Type = t
|
||||
}
|
||||
|
||||
if props, ok := schema["properties"].(map[string]interface{}); ok {
|
||||
for name, raw := range props {
|
||||
if m, ok := raw.(map[string]interface{}); ok {
|
||||
node.Properties[name] = ExpandSchema(spec, m, visited)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if items, ok := schema["items"].(map[string]interface{}); ok {
|
||||
node.Items = ExpandSchema(spec, items, visited)
|
||||
}
|
||||
|
||||
if allOf, ok := schema["allOf"].([]interface{}); ok {
|
||||
merged := &SchemaNode{Type: "object", Properties: map[string]*SchemaNode{}}
|
||||
for _, entry := range allOf {
|
||||
if m, ok := entry.(map[string]interface{}); ok {
|
||||
sub := ExpandSchema(spec, m, visited)
|
||||
for k, v := range sub.Properties {
|
||||
merged.Properties[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func GenerateExample(node *SchemaNode) interface{} {
|
||||
switch node.Type {
|
||||
case "object", "":
|
||||
obj := map[string]interface{}{}
|
||||
for k, v := range node.Properties {
|
||||
if strings.Contains(strings.ToLower(k), "date") {
|
||||
obj[k] = customDate
|
||||
} else if strings.Contains(strings.ToLower(k), "url") {
|
||||
obj[k] = customURL
|
||||
} else if strings.Contains(strings.ToLower(k), "email") {
|
||||
obj[k] = customEmail
|
||||
} else {
|
||||
obj[k] = GenerateExample(v)
|
||||
}
|
||||
}
|
||||
return obj
|
||||
case "array":
|
||||
if node.Items != nil {
|
||||
return []interface{}{GenerateExample(node.Items)}
|
||||
}
|
||||
return []interface{}{}
|
||||
case "string":
|
||||
return testString
|
||||
case "integer", "number":
|
||||
return 1
|
||||
case "boolean":
|
||||
return true
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) {
|
||||
// Ingests the specification file
|
||||
spec := SafelyUnmarshalSpec(bodyBytes)
|
||||
|
||||
// Checks defined security schemes and prompts for authentication
|
||||
CheckSecuritySchemes(spec)
|
||||
|
||||
u, parseErr := url.Parse(swaggerURL)
|
||||
if parseErr != nil {
|
||||
u = &url.URL{}
|
||||
}
|
||||
|
||||
// Gets the target server and base path from the specification file
|
||||
if apiTarget == "" {
|
||||
if v, ok := spec["swagger"].(string); ok && strings.HasPrefix(v, "2") {
|
||||
// Swagger (v2)
|
||||
host, _ := spec["host"].(string)
|
||||
bp, _ := spec["basePath"].(string)
|
||||
if bp == "/" {
|
||||
basePath = ""
|
||||
} else if bp != "" {
|
||||
basePath = bp
|
||||
}
|
||||
|
||||
if host != "" && strings.Contains(host, "://") {
|
||||
apiTarget = host
|
||||
} else {
|
||||
if host != "" {
|
||||
apiTarget = u.Scheme + "://" + host
|
||||
}
|
||||
}
|
||||
} else if v, ok := spec["openapi"].(string); ok && strings.HasPrefix(v, "3") {
|
||||
// OpenAPI (v3)
|
||||
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
|
||||
if len(servers) > 1 {
|
||||
if !quiet && (os.Args[1] != "endpoints") && apiTarget == "" {
|
||||
printWarn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
|
||||
for i := range servers {
|
||||
if srv, ok := servers[i].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
fmt.Println(serverURL)
|
||||
} else {
|
||||
fmt.Println(apiTarget + serverURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if srv, ok := servers[0].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
apiTarget = serverURL
|
||||
} else if serverURL == "/" {
|
||||
basePath = ""
|
||||
} else {
|
||||
basePath = serverURL
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original host at the target if no server found from specification.
|
||||
if apiTarget == "" {
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
if os.Args[1] != "endpoints" {
|
||||
// Prints Title/Description/Version values if they exist
|
||||
PrintSpecInfo(spec)
|
||||
}
|
||||
|
||||
// Reviews all defined API routes and builds requests as defined
|
||||
BuildRequestsFromPaths(spec, client)
|
||||
}
|
||||
|
||||
func ResolveRef(spec map[string]interface{}, ref string) map[string]interface{} {
|
||||
if !strings.HasPrefix(ref, "#") {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(ref[2:], "/")
|
||||
var cur interface{} = spec
|
||||
|
||||
for _, p := range parts {
|
||||
m, ok := cur.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cur = m[p]
|
||||
}
|
||||
|
||||
resolved, _ := cur.(map[string]interface{})
|
||||
return resolved
|
||||
}
|
||||
|
||||
func PrintSpecInfo(spec map[string]interface{}) {
|
||||
info, ok := spec["info"].(map[string]interface{})
|
||||
if !ok || info == nil {
|
||||
printInfo("No information defined in the documentation.\n")
|
||||
} else {
|
||||
title, ok := info["title"].(string)
|
||||
if ok && title != "" {
|
||||
if outputFormat == "json" {
|
||||
specTitle = title
|
||||
} else {
|
||||
fmt.Printf("Title: %s\n", title)
|
||||
}
|
||||
}
|
||||
|
||||
description, ok := info["description"].(string)
|
||||
if ok && description != "" {
|
||||
if outputFormat == "json" {
|
||||
specDescription = description
|
||||
} else {
|
||||
fmt.Printf("Description: %s\n", description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetScheme(swaggerURL string) (scheme string) {
|
||||
if strings.HasPrefix(swaggerURL, "http://") {
|
||||
scheme = "http"
|
||||
@@ -469,6 +536,17 @@ func SetScheme(swaggerURL string) (scheme string) {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
/*
|
||||
TrimHostScheme trims the scheme from the provided URL if the '-T' flag is supplied to sj.
|
||||
*/
|
||||
@@ -487,29 +565,29 @@ func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
|
||||
return host
|
||||
}
|
||||
|
||||
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
|
||||
func XmlFromObject(obj map[string]interface{}) string {
|
||||
var b strings.Builder
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
|
||||
os.Exit(1)
|
||||
for k, v := range obj {
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
b.WriteString("<" + k + ">")
|
||||
b.WriteString(XmlFromObject(val))
|
||||
b.WriteString("</" + k + ">")
|
||||
case []interface{}:
|
||||
for _, item := range val {
|
||||
b.WriteString("<" + k + ">")
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
b.WriteString(XmlFromObject(m))
|
||||
} else {
|
||||
b.WriteString(XmlFromObject(m))
|
||||
}
|
||||
b.WriteString("</" + k + ">")
|
||||
}
|
||||
default:
|
||||
b.WriteString(fmt.Sprintf("<%s>%v</%s>", k, val, k))
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
newContentType = strings.TrimSpace(newContentType)
|
||||
|
||||
// Remove old 'Content-Type' header
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(h), "content-type:")
|
||||
})
|
||||
|
||||
Headers = append(Headers, "Content-Type: "+newContentType)
|
||||
|
||||
// Remove empty elements to avoid repetitions of "-H ''"
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.TrimSpace(h) == ""
|
||||
})
|
||||
return b.String()
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.22.5
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/getkin/kin-openapi v0.132.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -17,10 +17,12 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
)
|
||||
@@ -1,7 +1,8 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
@@ -20,6 +21,11 @@ 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
@@ -33,23 +39,20 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Reference in new issue
Block a user