Compare commits

..
50 Commits
Author SHA1 Message Date
Tony West bdea5822e8 Incremented version string 2026-02-26 10:34:50 -05:00
Tony West 7bb228d81c Fixed infinite loop when encountering circular redirects 2026-02-26 10:18:37 -05:00
Tony West 3258ca341f Fixed missing length check issue 2026-02-26 10:12:36 -05:00
Tony West 58d4e91760 Fixed variable shadowing issue 2026-02-26 10:05:56 -05:00
Tony West 1f5a946011 Converted status code logic to switch for readability 2026-02-26 10:02:32 -05:00
Tony West 143fdfda1b Merge pull request #50 from aringo-bf/fix/httpbasic-segfault-and-quiet-flag
Fix HTTPBasic segfault and --quiet flag issues
2026-02-26 09:10:36 -05:00
aringo-bf a33eef47f4 Fix HTTPBasic segfault and --quiet flag issues
This commit fixes three critical bugs that caused segmentation faults:

1. HTTPBasic auth ignores --quiet flag
   - HTTPBasic was the only auth type that did not respect --quiet
   - Added quiet flag check to skip prompts and show warning instead
   - Brings HTTPBasic in line with other auth types

2. Nil pointer dereference in http.NewRequest error handling
   - When http.NewRequest failed with context.Canceled or io.EOF,
     code continued with nil req object
   - Restructured error handling to return early when req is nil

3. Unchecked url.Parse errors causing nil pointer panics
   - Multiple url.Parse calls ignored errors, allowing nil derefs
   - Added proper error checking before accessing parsed URL fields

Fixes segmentation fault when processing OpenAPI specs with HTTPBasic
auth in automated/quiet mode.

Bumps version to 2.3.1
2026-02-25 21:57:34 -06:00
Tony West 4c466112ee Added custom date, email, and URL flags 2026-02-03 17:10:52 -05:00
Tony West 1195ff7fbf Incremented version string 2026-02-03 13:09:21 -05:00
Tony West bd3265d0a4 Improved handling of schema references 2026-02-03 13:08:53 -05:00
Tony West d9fac2e9fe Updated version string 2026-01-28 14:30:49 -05:00
Tony West 10fe0376c0 Added the ability to customize the value used for testing identified string parameters 2026-01-28 14:30:25 -05:00
Tony West baf4ef6c51 Added prioritized list of endpoints for the brute command and fixed base path handling for v2 2026-01-15 18:37:16 -05:00
Tony West 8e2e5fbebc Fixed improper handling of base path 2026-01-15 10:54:22 -05:00
Tony West a77f9f46bb Merge pull request #49 from altperfect/main
Prevent header-processing logic from dropping custom headers in 'automate' mode
2026-01-15 10:37:28 -05:00
altperfect 5554e2bd30 fix: prevent header-processing logic from dropping custom headers 2026-01-15 19:08:39 +05:00
Tony West 09c460e768 Fixed issue that occurred when running against subdomains that contain '-d' in the subdomain name 2025-11-25 16:36:39 -05:00
Tony West ba45c6da26 Removed duplicate base path from output, minor changes to value type handling 2025-11-21 17:17:50 -05:00
Tony West 7417f9f917 Fixed bug where the base path was sometimes applied to the URL multiple times 2025-09-19 17:24:33 -04:00
Tony West 70a3d0b8c2 Updated the documentation 2025-09-16 21:40:16 -04:00
Tony West 77b11f40e6 Made some improvements for greater flexibility when encountering improperly defined API specifications 2025-09-16 21:40:04 -04:00
Tony West fd602b9ca8 Fixed a bug that prevented use of header values containing a colon 2025-04-15 13:24:23 -04:00
Tony West 83d1b57aef Merge pull request #41 from altperfect/fix/enforce-single-content-type
Fix EnforceSingleContentType function to not remove all custom headers
2025-03-18 16:06:26 -04:00
Tony West 6f903a7f78 Merge pull request #42 from altperfect/feat/add-curl-command-to-verbose-output
Add curl command in verbose output for 'automate' mode
2025-03-18 16:03:33 -04:00
Tony West 0d59e6c34f Additional fix to output format check 2025-03-18 15:57:32 -04:00
Tony West ceb99e4ddd Fixed insufficient output format check when displaying rate limit information 2025-03-18 15:54:55 -04:00
altperfect 8310c8995d feat: add curl command in verbose output for 'automate' mode 2025-03-13 04:52:43 +05:00
altperfect 017eb9af50 chore: bump version 2025-03-13 04:42:44 +05:00
altperfect 025f3f18cf fix: refactor EnforceSingleContentType to not remove all custom headers except Content-Type 2025-03-13 04:24:03 +05:00
Tony West 585100037a Updated documentation 2024-11-15 16:30:15 -05:00
Tony West f4b2634f42 Fixed improper handling of parameter values in the URL path 2024-11-15 16:28:29 -05:00
Tony West 2e9da2e810 Merge pull request #38 from six2dez/patch-1
Added failsafe check for files with no security definitions
2024-11-15 15:48:15 -05:00
Tony West 0fd8b6e6ec Updated documentation 2024-11-15 15:37:39 -05:00
Tony West 8acc15aad1 Added rate limit functionality to the automate and brute commands 2024-11-15 15:37:24 -05:00
six2dez 25d12a52e3 Added failsafe check for Swaggers with no security definitions
When a swagger file does not provide a security definition the tool crashes, now it gives a warning and continues
2024-11-08 10:20:21 +01:00
Tony West a9f907e2f1 Modified convert command to enable saved output even when definition file is already v3 2024-10-16 14:27:48 -04:00
Tony West 3b3fed2c99 Merge branch 'main' of https://github.com/BishopFox/sj 2024-10-09 18:07:14 -04:00
Tony West 1571b14873 Updated version string 2024-10-09 18:06:38 -04:00
Tony West 94e65fd96f Fixed missing check for 'formData' in the 'in' parameter object value 2024-10-09 18:06:26 -04:00
Tony West 5274b1b0ab Fixed null pointer bug 2024-10-09 13:06:50 -04:00
Tony West 49d6867ebf Now requires Go >= 1.21 2024-10-04 18:44:51 -04:00
Tony West 65d424c6a7 Added support for 'oneOf', 'anyOf', and 'allOf' schema keywords. 2024-10-03 22:47:27 -04:00
Tony West d7b34598d2 Fixed bug in verbose JSON output 2024-10-03 14:52:47 -04:00
Tony West 4526ddbf23 Fixed improper JSON output structure 2024-10-01 13:37:23 -04:00
Tony West 31e286235b Merge pull request #27 from mr-pmillz/main
Resolved nil pointer dereference
2024-09-20 15:27:05 -04:00
Tony West c84e679ac4 Merge branch 'main' into main 2024-09-20 15:26:18 -04:00
Tony West 41ba000755 Updated the version string. 2024-09-20 15:24:30 -04:00
Tony West cc99aef9ae Adds automatic sorting of results when output to the console 2024-09-20 15:13:14 -04:00
mr-pmillz b98e8c7d25 resolved nil pointer dereference 2 2024-09-16 18:23:45 -04:00
mr-pmillz 2879da6f87 resolved nil pointer dereference 2024-09-16 18:06:07 -04:00
14 changed files with 1013 additions and 976 deletions

No files matched your search

+58 -67
View File
@@ -13,7 +13,7 @@ It does this by parsing the definition file for paths, parameters, and accepted
## Build
To compile from source, ensure you have Go version `>= 1.20` installed and run `go build` from within the repository:
To compile from source, ensure you have Go version `>= 1.22.5` installed and run `go build` from within the repository:
```bash
$ git clone https://github.com/BishopFox/sj.git
@@ -37,106 +37,97 @@ $ 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 -q
INFO[0000] Gathering API details.
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
INFO[0000] 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[0000] Available authentication mechanisms:
- api_key
- petstore_auth
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByTags?tags=1"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/store/order"
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithList"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/user"
ERRO[0000] User not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/user/test"
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/user/test"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/login?password=test&username=test"
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithArray"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/logout"
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test"
ERRO[0000] Pet not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/pet/test"
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test/uploadImage"
ERRO[0000] Order not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/store/order/test"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/store/inventory"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/pet"
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/pet"
INFO[0001] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByStatus?status=1"
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
```
> 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.
```bash
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -q
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
INFO[0000] 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[0000] Available authentication mechanisms:
- api_key
- petstore_auth
curl -sk -X PUT 'https://petstore.swagger.io/v2/user/test' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/test'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/test'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/store/order' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
curl -sk -X PUT 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/order/test'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByTags?tags=1'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithList' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/user' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/logout'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/login?password=test&username=test'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test/uploadImage' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByStatus?status=1'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/inventory'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithArray' -d '{"test":"test"}'
$ curl -X POST "https://petstore.swagger.io/v2/pet/{petId}"
$ curl -X GET "https://petstore.swagger.io/v2/pet/{petId}"
$ curl -X GET "https://petstore.swagger.io/v2/store/inventory"
$ curl -X POST "https://petstore.swagger.io/v2/user/createWithList" -d 'body=1'
$ curl -X GET "https://petstore.swagger.io/v2/user/logout"
$ curl -X POST "https://petstore.swagger.io/v2/user/createWithArray" -d 'body=1'
$ curl -X GET "https://petstore.swagger.io/v2/pet/findByStatus"
$ curl -X GET "https://petstore.swagger.io/v2/pet/findByTags"
$ curl -X POST "https://petstore.swagger.io/v2/store/order" -d 'petId=1&quantity=1&shipDate=bishopfox&status=bishopfox&complete=1&id=1&body='
$ curl -X POST "https://petstore.swagger.io/v2/pet/{petId}/uploadImage"
$ curl -X POST "https://petstore.swagger.io/v2/pet" -d 'photoUrls=1&tags=1&status=bishopfox&id=1&category=&name=doggie&body='
$ curl -X PUT "https://petstore.swagger.io/v2/pet" -d 'id=1&category=&name=doggie&photoUrls=1&tags=1&status=bishopfox&body='
$ curl -X GET "https://petstore.swagger.io/v2/user/{username}"
$ curl -X PUT "https://petstore.swagger.io/v2/user/{username}" -d 'email=bishopfox&password=bishopfox&phone=bishopfox&userStatus=1&id=1&username=bishopfox&firstName=bishopfox&lastName=bishopfox&body='
$ curl -X GET "https://petstore.swagger.io/v2/user/login"
$ curl -X POST "https://petstore.swagger.io/v2/user" -d 'phone=bishopfox&userStatus=1&id=1&username=bishopfox&firstName=bishopfox&lastName=bishopfox&email=bishopfox&password=bishopfox&body='
$ curl -X GET "https://petstore.swagger.io/v2/store/order/{orderId}"
```
> Use the `endpoints` command to generate a list of raw endpoints from the provided definition file.
```bash
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
INFO[0000] Gathering endpoints.
/v2/store/inventory
/v2/store/order/{orderId}
/v2/pet
/v2/pet/findByStatus
/v2/pet
/v2/store/order
/v2/user/createWithList
/v2/pet/{petId}/uploadImage
/v2/pet/findByTags
/v2/pet/{petId}
/v2/pet/{petId}/uploadImage
/v2/store/inventory
/v2/store/order
/v2/store/order/{orderId}
/v2/user
/v2/pet/{petId}
/v2/user/{username}
/v2/user/{username}
/v2/user/createWithArray
/v2/user/createWithList
/v2/pet/findByStatus
/v2/user/login
/v2/user/logout
/v2/user/{username}
/v2/user
```
> Use the `brute` command to send a series of requests in an attempt to find a definition file on the target.
```bash
$ sj brute -u https://petstore.swagger.io/v2/swagger.json
INFO[0000] Sending 2045 requests. This could take a while...
$ sj brute -u https://petstore.swagger.io -qi -p http://127.0.0.1:8080 -e
INFO[0000] Sending 2173 requests. This could take a while...
Request: 343
INFO[0015] Definition file found: https://petstore.swagger.io/v2/swagger
{"...SNIP..."}
INFO[0033] Definition file found: https://petstore.swagger.io/v2/swagger
```
> Use the `convert` command to convert a definition file from version 2 to version 3.
```bash
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080 -o openapi.json
INFO[0000] Gathering API details.
@@ -184,7 +175,7 @@ Available Commands:
prepare Prepares a set of commands for manual testing of each endpoint.
Flags:
-a, --agent string Set the User-Agent string. (default "Swagger Jacker (github.com/BishopFox/sj)")
-A, --agent string Set the User-Agent string. (default "Swagger Jacker (github.com/BishopFox/sj)")
-b, --base-path string Set the API base path if not defined in the definition file (i.e. /V2/).
-f, --format string Declare the format of the definition file (json/yaml/yml/js). (default "json")
-H, --headers stringArray Add custom headers, separated by a colon ("Name: Value"). Multiple flags are accepted.
@@ -194,7 +185,7 @@ Flags:
-o, --outfile string Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.
-p, --proxy string Proxy host and port. Example: http://127.0.0.1:8080 (default "NOPROXY")
-q, --quiet Do not prompt for user input - uses default values for all requests.
-r, --randomize-user-agent Randomizes the user agent string. Default is 'false'.
--randomize-user-agent Randomizes the user agent string. Default is 'false'.
-s, --safe-word stringArray Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.
-T, --target string Manually set a target for the requests to be made if separate from the host the documentation resides on.
-t, --timeout int Set the request timeout period. (default 30)
+106 -79
View File
@@ -5,8 +5,6 @@ import (
"fmt"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
@@ -21,89 +19,118 @@ var (
bearerToken string
)
func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName string) {
if outputFormat == "json" && !quiet {
if len(doc3.Components.SecuritySchemes) != 0 {
log.Warnf("The following authentication mechanisms are supported. If necessary, supply these manually when using the JSON output format:\n")
}
for mechanism := range doc3.Components.SecuritySchemes {
log.Infof(`Type: %s, Scheme: %s`, doc3.Components.SecuritySchemes[mechanism].Value.Type, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
}
log.Warnln("Note: To avoid these messages, pass the quiet (-q) flag.")
} else if !quiet {
if len(doc3.Components.SecuritySchemes) > 0 {
log.Info("Available authentication mechanisms: ")
}
for mechanism := range doc3.Components.SecuritySchemes {
if doc3.Components.SecuritySchemes[mechanism].Value == nil {
log.Error("Authorization schema structure unsupported. Manual testing is required.")
return false, "", ""
}
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme != "" {
fmt.Printf(" - %s (%s)\n", mechanism, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
} else if outputFormat != "json" {
fmt.Printf(" - %s\n", mechanism)
func CheckSecuritySchemes(spec map[string]interface{}) {
components, ok := spec["components"].(map[string]interface{})
if ok && components != nil {
securitySchemes, ok := components["securitySchemes"].(map[string]interface{})
if !ok || len(securitySchemes) == 0 {
fmt.Println("No security schemes defined.")
} else {
if outputFormat != "json" {
fmt.Println("Found security schemes:")
}
var apiKey string
var apiKeyName string
if doc3.Components.SecuritySchemes[mechanism].Value.Type == "http" {
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme == "basic" {
log.Infof("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)
} else {
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
}
} else if strings.ToLower(doc3.Components.SecuritySchemes[mechanism].Value.Scheme) == "bearer" {
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
for mechanism, value := range securitySchemes {
fmt.Printf(" - %s\n", mechanism)
scheme, ok := value.(map[string]interface{})
if !ok {
continue
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "query" {
apiInQuery = true
log.Infof("An API key can be provided via a parameter string. Would you like to apply one? (y/N)")
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
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)
if typ, ok := scheme["type"].(string); ok {
switch typ {
case "http":
if schemeType := scheme["scheme"]; schemeType != nil {
switch schemeType {
case "basic":
if quiet {
autoApplyBasicAuth = "n"
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
} else {
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)
} else {
log.Warn("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.")
}
}
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)")
if quiet {
autoApplyAPIKey = "n"
} else {
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
}
if autoApplyAPIKey == "y" {
if nameVal, ok := scheme["name"].(string); ok {
apiKeyName = nameVal
}
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)
}
case "header":
if mechanism == "bearer" {
log.Infof("A bearer token is accepted. Would you like to provide one? (y/N)")
if quiet {
autoApplyBearer = "n"
} else {
fmt.Scanln(&autoApplyBearer)
autoApplyBearer = strings.ToLower(autoApplyBearer)
}
if autoApplyBearer == "y" {
fmt.Printf("What value would you like to use for the Bearer Token? ")
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.")
}
} 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)
if quiet {
autoApplyAPIKey = "n"
} else {
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
}
if autoApplyAPIKey == "y" {
apiKeyName = nameVal
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
Headers = append(Headers, nameVal+": "+apiKey)
}
}
}
}
}
}
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "header" {
if mechanism == "bearer" {
log.Infof("A bearer token is accepted. Would you like to provide one? (y/N)")
fmt.Scanln(&autoApplyBearer)
autoApplyBearer = strings.ToLower(autoApplyBearer)
if autoApplyBearer == "y" {
fmt.Printf("What value would you like to use for the Bearer Token? ")
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.")
}
} else {
log.Infof("An API key can be provided via the header %s. Would you like to apply one? (y/N)", doc3.Components.SecuritySchemes[mechanism].Value.Name)
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
Headers = append(Headers, doc3.Components.SecuritySchemes[mechanism].Value.Name+": "+apiKey)
}
if bearerFormat, ok := scheme["bearerFormat"].(string); ok {
fmt.Println(" - bearerFormat:", bearerFormat)
}
}
}
}
fmt.Printf("\n")
return apiInQuery, apiKey, apiKeyName
}
+27 -2
View File
@@ -5,6 +5,7 @@ import (
"io"
"os"
"strings"
"time"
"github.com/spf13/cobra"
@@ -14,6 +15,7 @@ import (
var getAccessibleEndpoints bool
var outputFormat string
var responsePreviewLength int
var testString string
var verbose bool
var automateCmd = &cobra.Command{
@@ -23,7 +25,6 @@ var automateCmd = &cobra.Command{
This enables the user to get a quick look at which endpoints require authentication and which ones do not. If a request
responds in an abnormal way, manual testing should be conducted (prepare manual tests using the "prepare" command).`,
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.")
@@ -32,13 +33,31 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
}
}
/* // NEED TO RE-IMPLEMENT RATE LIMIT
if rateLimit <= 0 {
log.Fatal("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)
}
}
_, 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\n")
log.Infof("Gathering API details.\n")
}
if swaggerURL != "" {
@@ -51,6 +70,11 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
bodyBytes, _ = io.ReadAll(specFile)
}
/* // NEED TO RE-IMPLEMENT RATE LIMIT
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
log.Info("Sending requests at a rate of ", rateLimit, " requests per second.")
}
*/
GenerateRequests(bodyBytes, client)
},
}
@@ -58,6 +82,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
func init() {
automateCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "console", "The output format. Only 'console' (default) and 'json' are supported at the moment.")
automateCmd.PersistentFlags().BoolVar(&getAccessibleEndpoints, "get-accessible-endpoints", false, "Only output the accessible endpoints (those that return a 200 status code).")
automateCmd.PersistentFlags().StringVar(&testString, "test-string", "bishopfox", "The string to use when testing endpoints with string values.")
automateCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose mode, which shows a preview of each response.")
automateCmd.PersistentFlags().IntVar(&responsePreviewLength, "response-preview-length", 50, "sets the response preview length when using verbose output.")
+134 -13
View File
@@ -11,17 +11,22 @@ import (
"regexp"
"strings"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
log "github.com/sirupsen/logrus"
)
var endpointOnly bool
var endpointWordlist string
var prefixDirs []string = []string{"", "/swagger", "/swagger/docs", "/swagger/latest", "/swagger/v1", "/swagger/v2", "/swagger/v3", "/swagger/static", "/swagger/ui", "/swagger-ui", "/api-docs", "/api-docs/v1", "/api-docs/v2", "/apidocs", "/api", "/api/v1", "/api/v2", "/api/v3", "/v1", "/v2", "/v3", "/doc", "/docs", "/docs/swagger", "/docs/swagger/v1", "/docs/swagger/v2", "/docs/swagger-ui", "/docs/swagger-ui/v1", "/docs/swagger-ui/v2", "/docs/v1", "/docs/v2", "/docs/v3", "/public", "/redoc"}
var jsonEndpoints []string = []string{"", "/index", "/swagger", "/swagger-ui", "/swagger-resources", "/swagger-config", "/openapi", "/api", "/api-docs", "/apidocs", "/v1", "/v2", "/v3", "/doc", "/docs", "/apispec", "/apispec_1", "/api-merged"}
var javascriptEndpoints []string = []string{"/swagger-ui-init", "/swagger-ui-bundle", "/swagger-ui-standalone-preset", "/swagger-ui", "/swagger-ui.min", "/swagger-ui-es-bundle-core", "/swagger-ui-es-bundle", "/swagger-ui-standalone-preset", "/swagger-ui-layout", "/swagger-ui-plugins"}
var priorityURLs []string = []string{"/swagger.json", "/openapi.json", "/api-docs", "/swagger", "/docs", "/api/swagger.json", "/api/openapi.json", "/api-docs/swagger.json", "/api/schema/", "/webjars/swagger-ui/index.html", "/API/swagger/ui/index", "/swagger/ui/index", "/v2/swagger.json", "/v2/openapi.json", "/v2/api-docs", "/v3/api-docs", "/v3/openapi.json", "/public/api-merged.json", "/analytics/v1/swagger", "/api.json", "/api/4.0/swagger.json", "/api/api-doc/openapi.json", "/api/api-doc/openapi.yaml", "/api/doc.json", "/api/docs.json", "/api/swagger", "/api/swagger/ui/index", "/api/v1/swagger", "/api/v2/api-docs", "/api/v2/openapi.json", "/api/v2/swagger.json", "/api/v3/api-docs", "/api/v3/apispec", "/api/workorder/openapi.json", "/apidocs", "/audiences/v1/swagger", "/audittrail/v1/swagger", "/certification/v1/swagger", "/citrixapi/store/swagger.json", "/conferencetool/v1/swagger", "/course/v1/swagger", "/dcl_swagger.yaml", "/doc/doc.json", "/doc/swagger.json", "/docs/swagger.json", "/docs/v1/swagger.json", "/ecommerce/v1/swagger", "/enrollment/v1/swagger", "/externalids/v1/swagger", "/impact/v1/swagger", "/learn/v1/swagger", "/learningplan/v1/swagger", "/manage/v1/swagger", "/management/info", "/marketplace/v1/swagger", "/messenger/v1/swagger", "/notifications/v1/swagger", "/openapi", "/openapi/spec.json", "/otj/v1/swagger", "/pages/v1/swagger", "/poweruser/v1/swagger", "/proctoring/v1/swagger", "/report/v1/swagger", "/swagger-ui/index.html", "/swagger-ui/openapi.json", "/swagger.yaml", "/swagger/0.1.0/swagger.json", "/swagger/doc.json", "/swagger/latest/swagger.json", "/swagger/swagger.json", "/swagger/test/swagger.json", "/swagger/ui/index.html", "/swagger/v1/openapiv2.json", "/swagger/v1/swagger.json", "/swagger/v2/swagger.json", "/swagger/v4/swagger.json", "/v1/openapi.json", "/v1/swagger", "/v1/swagger.json", "/swagger/docs/v1", "/swagger/docs/v1.json", "/Api/swagger/docs/v1", "/swagger/v1/swagger.json", "/api/api-docs/swagger.json", "/api/docs/", "/api/docs", "/swagger-ui"}
var bruteCmd = &cobra.Command{
Use: "brute",
@@ -29,6 +34,18 @@ var bruteCmd = &cobra.Command{
Long: `The brute command sends requests to the target to find operation definitions based on commonly used file locations.`,
Run: func(cmd *cobra.Command, args []string) {
/* // NEED TO RE-IMPLEMENT RATE LIMIT
if rateLimit <= 0 {
log.Fatal("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)
}
}
client := CheckAndConfigureProxy()
var allURLs []string
@@ -38,10 +55,11 @@ var bruteCmd = &cobra.Command{
}
target := u.Scheme + "://" + u.Host
if endpointWordlist == "" {
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "")...)
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/")...)
allURLs = append(allURLs, makeURLs(target, priorityURLs, "", true)...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "", false)...)
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js", false)...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json", false)...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/", false)...)
} else {
endpointList, err := os.Open(endpointWordlist)
if err != nil {
@@ -60,7 +78,11 @@ var bruteCmd = &cobra.Command{
log.Fatalf("failed to read words from file: %s", err)
}
}
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
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)
} else {
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
}
specFound, definitionFile := findDefinitionFile(allURLs, client)
if specFound {
@@ -86,31 +108,46 @@ var bruteCmd = &cobra.Command{
log.Infof("Wrote file to %s\n", f)
}
} else {
fmt.Println(string(definedOperations))
if endpointOnly {
return
} else {
fmt.Println(string(definedOperations))
}
}
// 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("No definition file found for:\t%s\n", swaggerURL)
log.Errorf("\nNo definition file found for:\t%s\n", swaggerURL)
}
},
}
func makeURLs(target string, endpoints []string, fileExtension string) []string {
func makeURLs(target string, endpoints []string, fileExtension string, skipPrefix bool) []string {
urls := []string{}
for _, dir := range prefixDirs {
if !skipPrefix {
for _, dir := range prefixDirs {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
} else {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
if endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
targetURL := target + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
return urls
}
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
for i, url := range urls {
ct := CheckContentType(client, url)
if strings.Contains(ct, "application/json") {
@@ -129,6 +166,9 @@ 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") {
@@ -143,7 +183,6 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
} else {
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
}
}
return false, nil
}
@@ -151,4 +190,86 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
func init() {
// TODO: Add a flag here (boolean) that defaults to false that will cause the program to execute 'sj automate' on the discovered definition file automatically.
bruteCmd.PersistentFlags().StringVarP(&endpointWordlist, "wordlist", "w", "", "The file containing a list of paths to brute force for discovery.")
bruteCmd.Flags().BoolVarP(&endpointOnly, "endpoint-only", "e", false, "Only return the identified endpoint")
}
func ExtractSpecFromJS(bodyBytes []byte) []byte {
var openApiIndex int
var specClose int
var bodyString, spec string
bodyString = string(bodyBytes)
spec = strings.ReplaceAll(bodyString, "\n", "")
spec = strings.ReplaceAll(spec, "\t", "")
spec = strings.ReplaceAll(spec, " ", "")
if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"swagger":"2.0"`) {
openApiIndex = strings.Index(spec, `"swagger":`) - 1
specClose = strings.LastIndex(spec, "]}") + 2
var doc2 openapi2.T
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc2)
if !strings.Contains(doc2.Swagger, "2") {
specClose = strings.LastIndex(spec, "}") + 1
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.")
}
}
} else if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"openapi":"3`) {
openApiIndex = strings.Index(spec, `"openapi":`) - 1
specClose = strings.LastIndex(spec, "]}") + 2
var doc3 openapi3.T
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc3)
if !strings.Contains(doc3.OpenAPI, "3") {
specClose = strings.LastIndex(spec, "}") + 1
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.")
}
}
} else {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
return bodyBytes
}
func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
var doc openapi2.T
var doc3 openapi3.T
format = strings.ToLower(format)
if format == "js" || strings.HasSuffix(swaggerURL, ".js") {
bodyBytes = ExtractSpecFromJS(bodyBytes)
} else if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") {
_ = yaml.Unmarshal(bodyBytes, &doc)
_ = yaml.Unmarshal(bodyBytes, &doc3)
}
_ = json.Unmarshal(bodyBytes, &doc)
_ = json.Unmarshal(bodyBytes, &doc3)
if strings.HasPrefix(doc3.OpenAPI, "3") {
newDoc := &doc3
return newDoc
} else if strings.HasPrefix(doc.Swagger, "2") {
newDoc, err := openapi2conv.ToV3(&doc)
if err != nil {
fmt.Printf("Error converting v2 document to v3: %s\n", err)
}
return newDoc
} else if os.Args[1] == "brute" {
var noDoc openapi3.T
return &noDoc
} else {
log.Fatal("Error parsing definition file.")
return nil
}
}
+11 -3
View File
@@ -24,6 +24,12 @@ var convertCmd = &cobra.Command{
var bodyBytes []byte
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)
}
}
client := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
@@ -55,14 +61,16 @@ var convertCmd = &cobra.Command{
}
if strings.HasPrefix(doc3.OpenAPI, "3") {
log.Fatal("Definition file is already version 3.")
log.Warnln("Definition file is already version 3.")
WriteConvertedDefinitionFile(bodyBytes)
} else if strings.HasPrefix(doc.Swagger, "2") {
newDoc, err := openapi2conv.ToV3(&doc)
if err != nil {
fmt.Printf("Error converting v2 document to v3: %s\n", err)
}
if format == "json" {
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).")
}
@@ -84,7 +92,7 @@ var convertCmd = &cobra.Command{
WriteConvertedDefinitionFile(converted)
}
}
} else if format == "yaml" || format == "yml" {
case "yaml", "yml":
converted, err := yaml.Marshal(newDoc)
if err != nil {
log.Fatal("Error converting definition file to v3:", err)
+9 -9
View File
@@ -5,7 +5,6 @@ import (
"io"
"os"
"github.com/mpvl/unique"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -16,28 +15,29 @@ var endpointsCmd = &cobra.Command{
Long: `The endpoints command allows you to pull a list of endpoints out of a Swagger definition file.
This list contains the raw endpoints (parameter values will not be appended or modified).`,
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)
}
}
client := CheckAndConfigureProxy()
var bodyBytes []byte
var paths []string
fmt.Printf("\n")
log.Infof("Gathering endpoints.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
paths = GenerateRequests(bodyBytes, client)
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)
}
bodyBytes, _ = io.ReadAll(specFile)
paths = GenerateRequests(bodyBytes, client)
}
unique.Sort(unique.StringSlice{&paths})
for _, v := range paths {
fmt.Println(v)
GenerateRequests(bodyBytes, client)
}
},
}
+56 -44
View File
@@ -19,6 +19,7 @@ type VerboseResult struct {
Preview string `json:"preview"`
Status int `json:"status"`
Target string `json:"target"`
Curl string `json:"curl"`
}
var tempLogger *log.Logger
@@ -33,12 +34,14 @@ 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)
var err error
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")
os.Exit(1)
}
defer file.Close()
tempLogger.SetOutput(file)
}
@@ -53,52 +56,53 @@ func writeLog(sc int, target, method, errorMsg, response string) {
}
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 {
logJSON(target, method)
} else {
logVerboseManual(sc, target, method, errorMsg, response, tempLogger)
}
} else {
switch sc {
case 0:
logVerboseBad(sc, target, method, response, tempLogger)
case 1:
logDangerous(target, method, tempLogger)
case 200:
logVerboseAccessible(sc, target, method, response, tempLogger)
case 301:
logVerboseRedirect(sc, target, method, response, tempLogger)
case 302:
logVerboseRedirect(sc, target, method, response, tempLogger)
case 401:
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
case 403:
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
case 404:
logNotFound(sc, target, method, errorMsg, tempLogger)
case 8899:
logVerboseJSON(specTitle, specDescription)
default:
logVerboseManual(sc, target, method, errorMsg, 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(target, method)
} else {
logManual(sc, target, method, errorMsg, tempLogger)
}
} else {
if sc == 8899 {
logJSON(target, method)
} else {
logAccessible(sc, target, method, tempLogger)
}
switch sc {
case 0:
logBad(sc, target, method, tempLogger)
case 1:
logDangerous(target, method, tempLogger)
case 200:
logAccessible(sc, target, method, tempLogger)
case 301:
logRedirect(sc, target, method, tempLogger)
case 302:
logRedirect(sc, target, method, tempLogger)
case 401:
logUnauth(sc, target, method, errorMsg, tempLogger)
case 403:
logUnauth(sc, target, method, errorMsg, tempLogger)
case 404:
logNotFound(sc, target, method, errorMsg, tempLogger)
case 8899:
logJSON(specTitle, specDescription)
default:
logManual(sc, target, method, errorMsg, tempLogger)
}
}
responsePreviewLength = tempResponsePreviewLength
file.Close()
}
func logAccessible(status int, target, method string, logger *log.Logger) {
@@ -219,8 +223,16 @@ func logVerboseUnauth(status int, target, method, errorMsg, response string, log
func logJSON(title, description string) {
tempLogger.WithFields(log.Fields{
"API Title": title,
"Description": description,
"Results": jsonResults,
"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")
}
+14 -28
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"io"
"os"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -16,6 +16,19 @@ var prepareCmd = &cobra.Command{
Long: `The prepare command prepares a set of commands for manual testing of each endpoint.
This enables you to test specific API functions for common vulnerabilities or misconfigurations.`,
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)
}
}
_, 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")
@@ -36,33 +49,6 @@ This enables you to test specific API functions for common vulnerabilities or mi
}
var prepareFor string
func (s SwaggerRequest) PrintPreparedCommands(method string) {
var preparedCommand string
if strings.ToLower(prepareFor) == "curl" {
preparedCommand = fmt.Sprintf("curl -sk -X %s '%s'", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = fmt.Sprintf("sqlmap --method=%s -u %s", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "ffuf" {
log.Fatal("ffuf is not supported yet :(")
// TODO
} else {
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
}
if s.BodyData != nil {
if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = preparedCommand + fmt.Sprintf(" --data='%s'", s.BodyData)
} else {
preparedCommand = preparedCommand + fmt.Sprintf(" -d '%s'", s.BodyData)
}
}
if len(Headers) != 0 {
preparedCommand = preparedCommand + fmt.Sprintf(" -H '%s'", strings.Join(Headers, "' -H '"))
}
fmt.Println(preparedCommand)
}
func init() {
prepareCmd.PersistentFlags().StringVarP(&prepareFor, "external-tool", "e", "curl", "The external tool to prepare commands for. Generates syntax for 'curl' by default.")
}
+54 -28
View File
@@ -20,6 +20,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 +60,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 {
log.Printf("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) {
@@ -87,38 +92,41 @@ 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 {
log.Fatal("Error: could not create HTTP request - ", err)
}
return nil, "", 0
}
for i := range Headers {
h := strings.Split(Headers[i], ":")
if len(h) == 2 {
if h[0] == "User-Agent" {
UserAgent = h[1]
}
if h[0] == "Content-Type" {
contentType = h[1]
}
if h[0] == "Accept" {
accept = h[1]
}
req.Header.Set(h[0], h[1])
} else {
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])
continue
}
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
if key == "User-Agent" {
UserAgent = value
}
if key == "Content-Type" {
contentType = value
}
if key == "Accept" {
accept = value
}
req.Header.Set(key, value)
}
// User-Agent handling
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Fatalf("Cannot set a User Agent while supplying the 'random-user-agent' flag.")
} else {
rand.New(rand.NewSource(time.Now().UnixNano()))
UserAgent = userAgents[rand.Intn(len(userAgents))]
req.Header.Set("User-Agent", UserAgent)
}
} else if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
rand.New(rand.NewSource(time.Now().UnixNano()))
UserAgent = userAgents[rand.Intn(len(userAgents))]
req.Header.Set("User-Agent", UserAgent)
} else {
req.Header.Set("User-Agent", UserAgent)
}
@@ -156,26 +164,44 @@ 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 {
log.Printf("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 {
log.Fatal("Error: could not create HTTP request - ", err)
}
return ""
}
// User-Agent handling
if randomUserAgent {
rand.New(rand.NewSource(time.Now().UnixNano()))
UserAgent = userAgents[rand.Intn(len(userAgents))]
req.Header.Set("User-Agent", UserAgent)
} else if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
req.Header.Set("User-Agent", UserAgent)
}
resp, err := client.Do(req.WithContext(ctx))
+11 -3
View File
@@ -7,6 +7,9 @@ import (
var apiTarget string
var basePath string
var customDate string
var customEmail string
var customURL string
var format string
var insecure bool
var localFile string
@@ -14,6 +17,7 @@ var outfile string
var proxy string
var quiet bool
var randomUserAgent bool
var rateLimit int
var safeWords []string
var swaggerURL string
var timeout int64
@@ -48,7 +52,7 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
log.Error("Command not specified. See the --help flag for usage.")
}
},
Version: "1.8.2",
Version: "2.3.2",
}
func Execute() {
@@ -61,8 +65,11 @@ func init() {
rootCmd.AddCommand(prepareCmd)
rootCmd.AddCommand(bruteCmd)
rootCmd.AddCommand(convertCmd)
rootCmd.PersistentFlags().StringVarP(&UserAgent, "agent", "a", "Swagger Jacker (github.com/BishopFox/sj)", "Set the User-Agent string.")
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.")
@@ -70,7 +77,8 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&outfile, "outfile", "o", "", "Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.")
rootCmd.PersistentFlags().StringVarP(&proxy, "proxy", "p", "NOPROXY", "Proxy host and port. Example: http://127.0.0.1:8080")
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Do not prompt for user input - uses default values for all requests.")
rootCmd.PersistentFlags().BoolVarP(&randomUserAgent, "randomize-user-agent", "r", false, "Randomizes the user agent string. Default is 'false'.")
rootCmd.PersistentFlags().BoolVar(&randomUserAgent, "randomize-user-agent", false, "Randomizes the user agent string. Default is 'false'.")
// rootCmd.PersistentFlags().IntVarP(&rateLimit, "rate", "r", 15, "Limit the number of requests per second.") // NEED TO RE-IMPLEMENT RATE LIMIT
rootCmd.PersistentFlags().StringArrayVarP(&safeWords, "safe-word", "s", nil, "Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.")
rootCmd.PersistentFlags().StringVarP(&apiTarget, "target", "T", "", "Manually set a target for the requests to be made if separate from the host the documentation resides on.")
rootCmd.PersistentFlags().Int64VarP(&timeout, "timeout", "t", 30, "Set the request timeout period.")
-119
View File
@@ -1,119 +0,0 @@
package cmd
import (
"fmt"
"math/rand"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
type SchemaReference struct {
Required []string
Type string
Properties struct {
Property struct {
Type string
Format string
Example string
Reference string
XML struct {
Name string
Namespace string
Prefix string
Attribute bool
Wrapped bool
}
}
}
}
func HandleSchemaReference(schema *openapi3.SchemaRef) {
}
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
if param != nil {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else if location == "path" {
if schema.Value.Properties[property].Value.Type == "string" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else if location == "query" {
if schema.Value.Properties[property].Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
}
} else if location == "body" {
if schema.Value.Properties[property].Value.Type == "string" {
s.Body[param.Value.Name] = "test"
} else {
s.Body[param.Value.Name] = 1
}
}
}
} else if schema.Value.Enum != nil {
if location == "path" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
} else if location == "query" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Query.Add(param.Value.Name, fmt.Sprint(value))
} else if location == "body" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[param.Value.Name] = value
}
}
}
} else {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else {
if schema.Value.Properties[property].Value.Type == "string" {
s.Body[property] = "test"
} else {
s.Body[property] = 1
}
}
}
} else if schema.Value.Enum != nil {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[name] = value
}
}
}
return s
}
+502 -538
View File
File diff suppressed because it is too large. Load diff
+9 -8
View File
@@ -1,25 +1,26 @@
module github.com/BishopFox/sj
go 1.20
go 1.22.5
toolchain go1.23.4
require (
github.com/getkin/kin-openapi v0.115.0
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de
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
)
require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/invopop/yaml v0.1.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.4 // 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
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+22 -35
View File
@@ -2,38 +2,36 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
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.115.0 h1:c8WHRLVY3G8m9jQTy0/DnIuljgRwTCB5twZytQS4JyU=
github.com/getkin/kin-openapi v0.115.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
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=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
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/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.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.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto=
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY=
github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
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/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=
@@ -42,27 +40,16 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=