mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-07 17:08:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdea5822e8 | ||
|
|
7bb228d81c | ||
|
|
3258ca341f | ||
|
|
58d4e91760 | ||
|
|
1f5a946011 | ||
|
|
143fdfda1b | ||
|
|
a33eef47f4 | ||
|
|
4c466112ee | ||
|
|
1195ff7fbf | ||
|
|
bd3265d0a4 | ||
|
|
d9fac2e9fe | ||
|
|
10fe0376c0 | ||
|
|
baf4ef6c51 | ||
|
|
8e2e5fbebc | ||
|
|
a77f9f46bb | ||
|
|
5554e2bd30 | ||
|
|
09c460e768 | ||
|
|
ba45c6da26 | ||
|
|
7417f9f917 | ||
|
|
70a3d0b8c2 | ||
|
|
77b11f40e6 | ||
|
|
fd602b9ca8 | ||
|
|
83d1b57aef | ||
|
|
6f903a7f78 | ||
|
|
0d59e6c34f | ||
|
|
ceb99e4ddd | ||
|
|
8310c8995d | ||
|
|
017eb9af50 | ||
|
|
025f3f18cf | ||
|
|
585100037a | ||
|
|
f4b2634f42 | ||
|
|
2e9da2e810 | ||
|
|
0fd8b6e6ec | ||
|
|
8acc15aad1 | ||
|
|
25d12a52e3 | ||
|
|
a9f907e2f1 | ||
|
|
3b3fed2c99 | ||
|
|
1571b14873 | ||
|
|
94e65fd96f | ||
|
|
5274b1b0ab | ||
|
|
49d6867ebf | ||
|
|
65d424c6a7 | ||
|
|
d7b34598d2 | ||
|
|
4526ddbf23 | ||
|
|
31e286235b | ||
|
|
c84e679ac4 | ||
|
|
41ba000755 | ||
|
|
cc99aef9ae | ||
|
|
b98e8c7d25 | ||
|
|
2879da6f87 | ||
|
|
530143b4ee | ||
|
|
69de3abbf1 | ||
|
|
9fd1b78081 | ||
|
|
c5a46d35bd | ||
|
|
2877384d54 | ||
|
|
cb3e0bb16e | ||
|
|
291edfe60c | ||
|
|
69b316e7bc | ||
|
|
615f0c7a26 | ||
|
|
5bbf0453a5 | ||
|
|
a39028b463 | ||
|
|
e22c11c118 | ||
|
|
528a771c87 | ||
|
|
c85512dfe0 | ||
|
|
6a01afb367 | ||
|
|
7ac01dc2a1 | ||
|
|
9b48aab4fb | ||
|
|
8ce3ca7aa5 | ||
|
|
62be170a97 | ||
|
|
a290716c76 | ||
|
|
927b2adf53 | ||
|
|
cecaccc07a | ||
|
|
54413d32e0 | ||
|
|
c399ca1ff8 | ||
|
|
45dd989ef3 | ||
|
|
1474d660e4 | ||
|
|
fe597147e6 | ||
|
|
15cc756a03 | ||
|
|
2b83f36c9d | ||
|
|
8261a1c154 | ||
|
|
17743c8c8a | ||
|
|
03ac3f1a3d | ||
|
|
3f02c53d94 | ||
|
|
246d6f1241 | ||
|
|
eef14078b4 | ||
|
|
20c02e4fa6 | ||
|
|
1fc2b657da | ||
|
|
3d0e81ffd4 | ||
|
|
44141deba9 | ||
|
|
91c8ffd56f | ||
|
|
75da0224d3 | ||
|
|
540d6064dc | ||
|
|
686022c827 | ||
|
|
df162f7c35 | ||
|
|
4d7814c74e | ||
|
|
4bde10ad1b | ||
|
|
4d05f51fbe |
No files matched your search
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
A code block containing the command executed with output:
|
||||
|
||||
```bash
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
|
||||
[SOME OUTPUT AND/OR ERROR HERE]
|
||||
|
||||
#Note: To maintain privacy, please omit any IP address, URL, organizational information contained within the definition file, etc.
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
*Note: To maintain privacy, please omit any IP address, URL, organizational information contained within the definition file, etc.*
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@@ -9,6 +11,11 @@ builds:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X main.build={{.Version}}
|
||||
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
@@ -24,8 +31,18 @@ archives:
|
||||
format: zip
|
||||
|
||||
changelog:
|
||||
disable: false
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
groups:
|
||||
- title: Features
|
||||
regexp: "^(?i).*add.*$"
|
||||
order: 0
|
||||
- title: "Bug fixes"
|
||||
regexp: "^(?i).*fix.*$"
|
||||
order: 1
|
||||
- title: Others
|
||||
order: 999
|
||||
@@ -1,16 +1,19 @@
|
||||
# sj (Swagger Jacker)
|
||||
|
||||

|
||||
|
||||
sj is a command line tool designed to assist with auditing of exposed Swagger/OpenAPI definition files by checking the associated API endpoints for weak authentication. It also provides command templates for manual vulnerability testing.
|
||||
|
||||
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of three commands:
|
||||
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of five sub-commands:
|
||||
- `automate` - Crafts a series of requests and analyzes the status code of the response.
|
||||
- `prepare` - Generates a list of commands to use for manual testing.
|
||||
- `endpoints` - Generates a list of raw API routes. *Path values will not be replaced with test data*.
|
||||
- `brute` - Sends a series of requests to a target to find operation definitions based on commonly used file paths.
|
||||
- `convert` - Converts a definition file from v2 to v3.
|
||||
|
||||
## 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
|
||||
@@ -24,32 +27,170 @@ To install the latest version of the tool, run:
|
||||
|
||||
```bash
|
||||
$ go install github.com/BishopFox/sj@latest
|
||||
|
||||
# Note: you may also need to place the path to your Go binaries within your PATH environment variable:
|
||||
$ export PATH=$PATH:~/go/bin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> 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
|
||||
|
||||
> Use the `prepare` command to prepare a list of curl commands for manual testing. You will likely have to modify these slightly.
|
||||
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 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 -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.
|
||||
$ 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 -qi -p http://127.0.0.1:8080
|
||||
|
||||
INFO[0000] Gathering endpoints.
|
||||
|
||||
/v2/store/inventory
|
||||
/v2/store/order/{orderId}
|
||||
/v2/pet
|
||||
/v2/pet
|
||||
/v2/store/order
|
||||
/v2/user/createWithList
|
||||
/v2/pet/{petId}/uploadImage
|
||||
/v2/pet/findByTags
|
||||
/v2/pet/{petId}
|
||||
/v2/pet/{petId}
|
||||
/v2/user/{username}
|
||||
/v2/user/{username}
|
||||
/v2/user/createWithArray
|
||||
/v2/pet/findByStatus
|
||||
/v2/user/login
|
||||
/v2/user/logout
|
||||
/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://<TARGET>
|
||||
INFO[0000] Sending 2045 requests. This could take a while...
|
||||
INFO[0111] Found operation definitions embedded in JavaScript file at https://<TARGET>/example.js
|
||||
$ 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[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 -qi -p http://127.0.0.1:8080 -o openapi.json
|
||||
|
||||
INFO[0000] Gathering API details.
|
||||
|
||||
INFO[0000] Wrote file to /current/directory/openapi.json
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
A full list of commands can be found by using the `--help` flag:
|
||||
|
||||

|
||||
```bash
|
||||
$ sj --help
|
||||
The process of reviewing and testing exposed API definition files is often tedious and requires a large investment of time for a thorough review.
|
||||
|
||||
sj (swaggerjacker) is a CLI tool that can be used to perform an initial check of API endpoints identified through exposed Swagger/OpenAPI definition files.
|
||||
Once you determine what endpoints require authentication and which do not, you can use the "prepare" command to generate command templates for further (manual) testing.
|
||||
|
||||
Example usage:
|
||||
|
||||
Perform a quick check of endpoints which require authentication:
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Generate a list of commands to use for manual testing:
|
||||
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Generate a list of raw API routes for use with custom scripts:
|
||||
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Perform a brute-force attack against the target to identify hidden definition files:
|
||||
$ sj brute -u https://petstore.swagger.io
|
||||
|
||||
Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file:
|
||||
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
|
||||
|
||||
Usage:
|
||||
sj [flags]
|
||||
sj [command]
|
||||
|
||||
Available Commands:
|
||||
automate Sends a series of automated requests to the discovered endpoints.
|
||||
brute Sends a series of automated requests to discover hidden API operation definitions.
|
||||
convert Converts a Swagger definition file to an OpenAPI v3 definition file.
|
||||
endpoints Prints a list of endpoints from the target.
|
||||
help Help about any command
|
||||
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)")
|
||||
-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.
|
||||
-h, --help help for sj
|
||||
-i, --insecure Ignores server certificate validation.
|
||||
-l, --local-file string Loads the documentation from a local file.
|
||||
-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.
|
||||
--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)
|
||||
-u, --url string Loads the documentation file from a URL
|
||||
-v, --version version for sj
|
||||
|
||||
Use "sj [command] --help" for more information about a command.
|
||||
```
|
||||
+110
-58
@@ -5,80 +5,132 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
autoApplyAPIKey string = "n"
|
||||
autoApplyBasicAuth string = "n"
|
||||
autoApplyBearer string = "n"
|
||||
basicAuthUser string
|
||||
basicAuthPass string
|
||||
basicAuth []byte
|
||||
basicAuthString string
|
||||
bearerToken string
|
||||
)
|
||||
|
||||
func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName string) {
|
||||
|
||||
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)
|
||||
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 {
|
||||
fmt.Printf(" - %s\n", mechanism)
|
||||
}
|
||||
if outputFormat != "json" {
|
||||
fmt.Println("Found security schemes:")
|
||||
}
|
||||
var apiKey string
|
||||
var apiKeyName string
|
||||
|
||||
if doc3.Components.SecuritySchemes[mechanism].Value.Type == "http" && !quiet {
|
||||
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.")
|
||||
for mechanism, value := range securitySchemes {
|
||||
fmt.Printf(" - %s\n", mechanism)
|
||||
scheme, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bearerFormat, ok := scheme["bearerFormat"].(string); ok {
|
||||
fmt.Println(" - bearerFormat:", bearerFormat)
|
||||
}
|
||||
} 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.")
|
||||
}
|
||||
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "query" && !quiet {
|
||||
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)
|
||||
}
|
||||
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "header" && !quiet {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
return apiInQuery, apiKey, apiKeyName
|
||||
}
|
||||
+37
-4
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -13,6 +14,10 @@ import (
|
||||
|
||||
var getAccessibleEndpoints bool
|
||||
var outputFormat string
|
||||
var responsePreviewLength int
|
||||
var testString string
|
||||
var verbose bool
|
||||
|
||||
var automateCmd = &cobra.Command{
|
||||
Use: "automate",
|
||||
Short: "Sends a series of automated requests to the discovered endpoints.",
|
||||
@@ -20,24 +25,43 @@ 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.")
|
||||
} else if strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) == "console" {
|
||||
outputFormat = "json"
|
||||
}
|
||||
}
|
||||
|
||||
/* // 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 != "" {
|
||||
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil, "automate")
|
||||
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
@@ -46,11 +70,20 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
}
|
||||
GenerateRequests(bodyBytes, client, "automate")
|
||||
/* // 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)
|
||||
},
|
||||
}
|
||||
|
||||
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.")
|
||||
|
||||
}
|
||||
+205
-126
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -10,132 +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 (
|
||||
prefixDirs = []string{
|
||||
"/docs",
|
||||
"",
|
||||
"/swagger",
|
||||
"/swagger/docs",
|
||||
"/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/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",
|
||||
}
|
||||
jsonEndpoints = []string{
|
||||
"",
|
||||
"/index",
|
||||
"/swagger",
|
||||
"/swagger-ui",
|
||||
"/swagger-resources",
|
||||
"/swagger-config",
|
||||
"/openapi",
|
||||
"/api",
|
||||
"/api-docs",
|
||||
"/apidocs",
|
||||
"/v1",
|
||||
"/v2",
|
||||
"/v3",
|
||||
"/doc",
|
||||
"/docs",
|
||||
"/apispec",
|
||||
"/apispec_1",
|
||||
"/api-merged",
|
||||
}
|
||||
javascriptEndpoints = []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 endpointOnly bool
|
||||
var endpointWordlist string
|
||||
|
||||
func makeURLs(target string, endpoints []string, fileExtension string) []string {
|
||||
urls := []string{}
|
||||
for _, dir := range prefixDirs {
|
||||
for _, endpoint := range endpoints {
|
||||
if dir == "" && endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + dir + 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") {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil, "brute")
|
||||
if bodyBytes != nil {
|
||||
checkSpec := UnmarshalSpec(bodyBytes)
|
||||
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
|
||||
log.Infof("\nDefinition file found: %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil, "brute")
|
||||
if bodyBytes != nil {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
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)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if i+1 == len(urls) {
|
||||
fmt.Printf("\033[2K\r%s%d\n", "Request: ", i+1)
|
||||
} else {
|
||||
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
|
||||
}
|
||||
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
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",
|
||||
@@ -143,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
|
||||
@@ -151,12 +54,35 @@ var bruteCmd = &cobra.Command{
|
||||
log.Warnf("Error parsing URL:%s\n", err)
|
||||
}
|
||||
target := u.Scheme + "://" + u.Host
|
||||
if endpointWordlist == "" {
|
||||
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 {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
}
|
||||
defer endpointList.Close()
|
||||
|
||||
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, "/")...)
|
||||
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
|
||||
scanner := bufio.NewScanner(endpointList)
|
||||
for scanner.Scan() {
|
||||
endpoint := scanner.Text()
|
||||
fullURL := target + endpoint
|
||||
allURLs = append(allURLs, fullURL)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("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)
|
||||
} else {
|
||||
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
|
||||
}
|
||||
|
||||
specFound, definitionFile := findDefinitionFile(allURLs, client)
|
||||
if specFound {
|
||||
@@ -182,15 +108,168 @@ 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, skipPrefix bool) []string {
|
||||
urls := []string{}
|
||||
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 endpoint == "" {
|
||||
continue
|
||||
}
|
||||
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") {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
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)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
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)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if i == len(urls) {
|
||||
fmt.Printf("\033[2K\r%s%d\n", "Request: ", i+1)
|
||||
} else {
|
||||
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var convertCmd = &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Converts a Swagger definition file to an OpenAPI v3 definition file.",
|
||||
Long: `The convert command converts a provided definition file from the Swagger specification (v2) to the OpenAPI specification (v3) and stores it into an output file.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
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" {
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
}
|
||||
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening definition file:", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
}
|
||||
|
||||
var doc openapi2.T
|
||||
var doc3 openapi3.T
|
||||
|
||||
format = strings.ToLower(format)
|
||||
if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") {
|
||||
_ = yaml.Unmarshal(bodyBytes, &doc)
|
||||
_ = yaml.Unmarshal(bodyBytes, &doc3)
|
||||
} else {
|
||||
_ = json.Unmarshal(bodyBytes, &doc)
|
||||
_ = json.Unmarshal(bodyBytes, &doc3)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(doc3.OpenAPI, "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)
|
||||
}
|
||||
|
||||
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).")
|
||||
}
|
||||
converted, err := json.Marshal(newDoc)
|
||||
if err != nil {
|
||||
log.Fatal("Error converting definition file to v3:", err)
|
||||
}
|
||||
if !strings.HasSuffix(string(converted), "}") {
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted))
|
||||
} else {
|
||||
WriteConvertedDefinitionFile(converted)
|
||||
}
|
||||
} else {
|
||||
endOfJSON := strings.LastIndex(string(converted), "}") + 1
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted)[:endOfJSON])
|
||||
} else {
|
||||
WriteConvertedDefinitionFile(converted)
|
||||
}
|
||||
}
|
||||
case "yaml", "yml":
|
||||
converted, err := yaml.Marshal(newDoc)
|
||||
if err != nil {
|
||||
log.Fatal("Error converting definition file to v3:", err)
|
||||
}
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted))
|
||||
} else {
|
||||
WriteConvertedDefinitionFile(converted)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
log.Errorf("Error writing file: %s\n", err)
|
||||
} else {
|
||||
f, _ := filepath.Abs(outfile)
|
||||
log.Infof("Wrote file to %s\n", f)
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -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, "endpoints")
|
||||
paths = GenerateRequests(bodyBytes, client, "endpoints")
|
||||
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, "endpoints")
|
||||
}
|
||||
unique.Sort(unique.StringSlice{&paths})
|
||||
for _, v := range paths {
|
||||
fmt.Println(v)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
}
|
||||
},
|
||||
}
|
||||
+187
-29
@@ -1,80 +1,238 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func writeLog(sc int, target, method string, errorMsg string) {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logUnauth(sc, target, method, errorMsg)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method)
|
||||
} else if sc == 0 {
|
||||
logSkipped(sc, target, method)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg)
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg)
|
||||
}
|
||||
} else {
|
||||
logAccessible(sc, target, method)
|
||||
}
|
||||
type Result struct {
|
||||
Method string `json:"method"`
|
||||
Status int `json:"status"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
type VerboseResult struct {
|
||||
Method string `json:"method"`
|
||||
Preview string `json:"preview"`
|
||||
Status int `json:"status"`
|
||||
Target string `json:"target"`
|
||||
Curl string `json:"curl"`
|
||||
}
|
||||
|
||||
var tempLogger *log.Logger
|
||||
|
||||
func writeLog(sc int, target, method, errorMsg, response string) {
|
||||
var file *os.File
|
||||
tempLogger = log.New()
|
||||
tempResponsePreviewLength := responsePreviewLength
|
||||
|
||||
if len(response) < responsePreviewLength {
|
||||
responsePreviewLength = len(response)
|
||||
}
|
||||
|
||||
if outfile != "" {
|
||||
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)
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
if verbose {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
|
||||
func logManual(status int, target, method, errorMsg 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."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logNotFound(status int, target, method, errorMsg string) {
|
||||
func logVerboseManual(status int, target, method, errorMsg, response string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Manual testing may be required."
|
||||
}
|
||||
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."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logRedirect(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
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 logSkipped(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
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("Request skipped (dangerous keyword found).")
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logUnauth(status int, target, method, errorMsg string) {
|
||||
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."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logVerboseUnauth(status int, target, method, errorMsg, response string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
}
|
||||
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")
|
||||
}
|
||||
+23
-4
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -12,16 +13,29 @@ import (
|
||||
var prepareCmd = &cobra.Command{
|
||||
Use: "prepare",
|
||||
Short: "Prepares a set of commands for manual testing of each endpoint.",
|
||||
Long: `The prepare command prepares a set of curl commands for manual testing of each endpoint.
|
||||
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")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil, "prepare")
|
||||
GenerateRequests(bodyBytes, client, "prepare")
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
@@ -29,7 +43,12 @@ This enables you to test specific API functions for common vulnerabilities or mi
|
||||
}
|
||||
|
||||
specBytes, _ := io.ReadAll(specFile)
|
||||
GenerateRequests(specBytes, client, "prepare")
|
||||
GenerateRequests(specBytes, client)
|
||||
}
|
||||
},
|
||||
}
|
||||
var prepareFor string
|
||||
|
||||
func init() {
|
||||
prepareCmd.PersistentFlags().StringVarP(&prepareFor, "external-tool", "e", "curl", "The external tool to prepare commands for. Generates syntax for 'curl' by default.")
|
||||
}
|
||||
+108
-38
@@ -2,11 +2,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,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
|
||||
@@ -51,25 +54,30 @@ var (
|
||||
userChoice string
|
||||
)
|
||||
|
||||
func MakeRequest(client http.Client, method, target string, timeout int64, reqData io.Reader, command string) ([]byte, string, int) {
|
||||
func MakeRequest(client http.Client, method, target string, timeout int64, reqData io.Reader) ([]byte, string, int) {
|
||||
if quiet {
|
||||
avoidDangerousRequests = "y"
|
||||
}
|
||||
|
||||
u, _ := url.Parse(target)
|
||||
// Handling of dangerous keywords
|
||||
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 command == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
|
||||
if os.Args[1] == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
|
||||
userChoice = ""
|
||||
if avoidDangerousRequests == "y" {
|
||||
return nil, "", 0
|
||||
} else {
|
||||
fmt.Printf("[!] Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
|
||||
log.Warnf("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"
|
||||
fmt.Printf("[!] Do you want to avoid all dangerous requests? (Y/n)")
|
||||
log.Warnf("Do you want to avoid all dangerous requests? (Y/n)")
|
||||
fmt.Scanln(&avoidDangerousRequests)
|
||||
avoidDangerousRequests = strings.ToLower(avoidDangerousRequests)
|
||||
riskSurveyed = true
|
||||
@@ -84,37 +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 {
|
||||
log.Fatal("Custom header provided cannot be used.")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -137,27 +149,59 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
return nil, "", 0
|
||||
} else if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
if strings.Contains(fmt.Sprint(err), "tls") && !strings.Contains(fmt.Sprint(err), "user canceled") {
|
||||
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.")
|
||||
} 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)
|
||||
} else if strings.Contains(fmt.Sprint(err), "user canceled") {
|
||||
return nil, "skipped", 1
|
||||
} else {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
}
|
||||
return nil, "", 0
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
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, url string) string {
|
||||
func CheckContentType(client http.Client, target string) string {
|
||||
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", url, nil)
|
||||
if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
req, err := http.NewRequest("GET", target, nil)
|
||||
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))
|
||||
@@ -165,11 +209,37 @@ func CheckContentType(client http.Client, url string) string {
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
return ""
|
||||
} else if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
if strings.Contains(fmt.Sprint(err), "tls") && !strings.Contains(fmt.Sprint(err), "user canceled") {
|
||||
fmt.Println("Try supplying the --insecure flag.")
|
||||
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.")
|
||||
} 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)
|
||||
} else {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return resp.Header.Get("Content-Type")
|
||||
}
|
||||
|
||||
func CheckAndConfigureProxy() (client http.Client) {
|
||||
var proxyUrl *url.URL
|
||||
|
||||
transport := &http.Transport{}
|
||||
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
if proxy != "NOPROXY" {
|
||||
proxyUrl, _ = url.Parse(proxy)
|
||||
transport.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
client = http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
return client
|
||||
}
|
||||
+19
-8
@@ -5,9 +5,11 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var accessibleEndpointFound bool
|
||||
var apiTarget string
|
||||
var basePath string
|
||||
var customDate string
|
||||
var customEmail string
|
||||
var customURL string
|
||||
var format string
|
||||
var insecure bool
|
||||
var localFile string
|
||||
@@ -15,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
|
||||
@@ -32,21 +35,24 @@ Example usage:
|
||||
Perform a quick check of endpoints which require authentication:
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Generate a list of curl commands to use for manual testing:
|
||||
Generate a list of commands to use for manual testing:
|
||||
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Generate a list of raw API routes for use with custom scripts:
|
||||
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Perform a brute-force attack against the target to identify hidden definition files:
|
||||
$ sj brute -u https://petstore.swagger.io`,
|
||||
$ sj brute -u https://petstore.swagger.io
|
||||
|
||||
Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file:
|
||||
$ 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.")
|
||||
}
|
||||
},
|
||||
Version: "1.3.4", // Also change in .goreleaser.yaml !
|
||||
Version: "2.3.2",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -58,16 +64,21 @@ func init() {
|
||||
rootCmd.AddCommand(endpointsCmd)
|
||||
rootCmd.AddCommand(prepareCmd)
|
||||
rootCmd.AddCommand(bruteCmd)
|
||||
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 spec (i.e. /V2/)")
|
||||
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the documentation (json/yaml/yml/js).")
|
||||
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.")
|
||||
rootCmd.PersistentFlags().StringVarP(&localFile, "local-file", "l", "", "Loads the documentation from a local file.")
|
||||
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.")
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func BuildObjectsFromSchemaDefinitions(doc openapi3.T) {
|
||||
if doc.Components.Schemas != nil {
|
||||
log.Infof("Building test objects from schema definitions...\n\n")
|
||||
}
|
||||
|
||||
if doc.Components.Schemas != nil {
|
||||
for definition := range doc.Components.Schemas {
|
||||
log.Infof("%s accepts %d parameters:\n", definition, len(doc.Components.Schemas[definition].Value.Properties))
|
||||
for property := range doc.Components.Schemas[definition].Value.Properties {
|
||||
schemaDef := doc.Components.Schemas[definition]
|
||||
definitionProperty := schemaDef.Value.Properties[property]
|
||||
if definitionProperty.Value.Type == "string" {
|
||||
log.Infof("%s=%s\n", property, "test")
|
||||
} else if definitionProperty.Value.Type == "integer" {
|
||||
log.Infof("%s=%d\n", property, 1)
|
||||
} else if definitionProperty.Ref != "" {
|
||||
fmt.Println("[!] [!] [!] " + definitionProperty.Ref) // Item $ref within a property
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+498
-343
@@ -1,342 +1,527 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi2"
|
||||
"github.com/getkin/kin-openapi/openapi2conv"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var accessibleEndpoints []string
|
||||
var specJSON string
|
||||
var jsonResultsStringArray []string
|
||||
var jsonResultArray []Result
|
||||
var jsonVerboseResultArray []VerboseResult
|
||||
var specTitle string
|
||||
var specDescription string
|
||||
|
||||
func CheckAndConfigureProxy() (client http.Client) {
|
||||
var proxyUrl *url.URL
|
||||
|
||||
transport := &http.Transport{}
|
||||
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
if proxy != "NOPROXY" {
|
||||
proxyUrl, _ = url.Parse(proxy)
|
||||
transport.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
client = http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
return client
|
||||
type SchemaNode struct {
|
||||
Type string
|
||||
Properties map[string]*SchemaNode
|
||||
Items *SchemaNode
|
||||
Required map[string]bool
|
||||
Enum []interface{}
|
||||
Example interface{}
|
||||
Ref string
|
||||
}
|
||||
|
||||
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.")
|
||||
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.")
|
||||
}
|
||||
|
||||
return bodyBytes
|
||||
}
|
||||
var errorDescriptions = make(map[any]string)
|
||||
for pathName, pathItem := range paths {
|
||||
if ops, ok := pathItem.(map[string]interface{}); ok {
|
||||
for method, op := range ops {
|
||||
switch strings.ToLower(method) {
|
||||
// SKIPS THE "DELETE" AND "PATCH" METHODS FOR SAFETY
|
||||
case "delete":
|
||||
continue
|
||||
case "patch":
|
||||
continue
|
||||
default:
|
||||
if opMap, ok := op.(map[string]interface{}); ok {
|
||||
if responses, ok := opMap["responses"].(map[string]interface{}); ok {
|
||||
for status, respItem := range responses {
|
||||
if respMap, ok := respItem.(map[string]interface{}); ok {
|
||||
if desc, ok := respMap["description"].(string); ok {
|
||||
errorDescriptions[status] = desc
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client, command string) []string {
|
||||
var apiKey string
|
||||
var apiKeyName string
|
||||
var apiInQuery bool = false
|
||||
var resultsJSON []string
|
||||
var pathMap = make(map[string]bool)
|
||||
var paths []string
|
||||
|
||||
fullUrl, _ := url.Parse(swaggerURL)
|
||||
newDoc := UnmarshalSpec(bodyBytes)
|
||||
basePathResult := GetBasePath(newDoc.Servers, TrimHostScheme(apiTarget, fullUrl.Host))
|
||||
|
||||
// BuildObjectsFromSchemaDefinitions(*newDoc) TODO
|
||||
|
||||
if command != "endpoints" {
|
||||
// Prints Title/Description values if they exist
|
||||
PrintSpecInfo(*newDoc.Info)
|
||||
apiInQuery, apiKey, apiKeyName = CheckSecDefs(*newDoc)
|
||||
}
|
||||
|
||||
if command == "automate" && outfile != "" {
|
||||
WriteJSONFile(`"results":[`, false)
|
||||
}
|
||||
|
||||
for path, pathItem := range newDoc.Paths {
|
||||
operations := map[string]*openapi3.Operation{
|
||||
"CONNECT": pathItem.Connect,
|
||||
"GET": pathItem.Get,
|
||||
"HEAD": pathItem.Head,
|
||||
"OPTIONS": pathItem.Options,
|
||||
"PATCH": pathItem.Patch,
|
||||
"POST": pathItem.Post,
|
||||
"PUT": pathItem.Put,
|
||||
"TRACE": pathItem.Trace,
|
||||
}
|
||||
|
||||
for method, op := range operations {
|
||||
// Do all the things here :D
|
||||
if op != nil {
|
||||
var newPath string
|
||||
newPath = path
|
||||
|
||||
u := url.URL{
|
||||
Scheme: SetScheme(swaggerURL),
|
||||
Host: TrimHostScheme(apiTarget, fullUrl.Host),
|
||||
Path: basePathResult,
|
||||
}
|
||||
if u.Path == "/" {
|
||||
u.Path = ""
|
||||
}
|
||||
query := url.Values{}
|
||||
|
||||
var bodyData []byte
|
||||
body := make(map[string]any)
|
||||
|
||||
for _, param := range op.Parameters {
|
||||
if param.Ref != "" || param.Value == nil {
|
||||
continue
|
||||
} else if param.Value.In == "path" {
|
||||
newPath = strings.ReplaceAll(newPath, "{"+param.Value.Name+"}", "test")
|
||||
} else if param.Value.In == "query" {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
query.Add(param.Value.Name, "test") // TODO: Implement actual definition/schema
|
||||
} else {
|
||||
if param.Value.Schema.Value.Type == "string" {
|
||||
query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
query.Add(param.Value.Name, "1")
|
||||
}
|
||||
}
|
||||
} else if param.Value.In == "header" && param.Value.Required {
|
||||
Headers = append(Headers, fmt.Sprintf("%s: %s", param.Value.Name, "1"))
|
||||
} else if param.Value.In == "body" {
|
||||
if param.Value.Schema.Value.Type == "string" {
|
||||
body[param.Value.Name] = "test"
|
||||
} else {
|
||||
body[param.Value.Name] = 1
|
||||
|
||||
targetURL := fmt.Sprintf("%s%s%s", apiTarget, basePath, pathName)
|
||||
curl := fmt.Sprintf("curl -X %s \"%s\"", strings.ToUpper(method), targetURL)
|
||||
var bodyData string
|
||||
|
||||
// Extracts the expected parameters from the parameters object
|
||||
if params, ok := opMap["parameters"].([]interface{}); ok {
|
||||
for _, p := range params {
|
||||
var pType string
|
||||
var pValue string
|
||||
if pMap, ok := p.(map[string]interface{}); ok {
|
||||
if name, ok := pMap["name"].(string); ok {
|
||||
in := pMap["in"].(string)
|
||||
if schema, ok := pMap["schema"].(map[string]interface{}); ok {
|
||||
if schema["type"] != nil {
|
||||
pType = schema["type"].(string)
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if pType == "string" && pMap["name"] != "version" {
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
} else if schema["$ref"] != nil {
|
||||
ref := schema["$ref"].(string)
|
||||
if defs, ok := spec["definitions"].(map[string]interface{}); ok {
|
||||
schemaName := strings.TrimPrefix(ref, "#/definitions/")
|
||||
if s, ok := defs[schemaName].(map[string]interface{}); ok {
|
||||
if properties, ok := s["properties"].(map[string]interface{}); ok {
|
||||
for propertyItem := range properties {
|
||||
var pValue string
|
||||
if propertyName, ok := properties[propertyItem].(map[string]interface{}); ok {
|
||||
if exampleValue, ok := propertyName["example"]; ok {
|
||||
switch ex := exampleValue.(type) {
|
||||
case string:
|
||||
pValue = exampleValue.(string)
|
||||
case []interface{}:
|
||||
if len(ex) > 0 {
|
||||
if v, ok := ex[0].(string); ok {
|
||||
pValue = v
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
}
|
||||
default:
|
||||
pValue = "1"
|
||||
}
|
||||
} else if propertyType, ok := propertyName["type"].(string); ok {
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if propertyType == "string" && propertyName["name"] != "version" {
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if strings.Contains(curl, "-d '") {
|
||||
bodyData += fmt.Sprintf("&%s=%s", propertyItem, pValue)
|
||||
curl = strings.TrimSuffix(curl, "'")
|
||||
curl += fmt.Sprintf("&%s=%s'", propertyItem, pValue)
|
||||
} else {
|
||||
bodyData += fmt.Sprintf("%s=%s", propertyItem, pValue)
|
||||
curl += fmt.Sprintf(" -d '%s=%s'", propertyItem, pValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if pType, ok := pMap["type"].(string); ok {
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if pType == "string" && pMap["name"] != "version" {
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
}
|
||||
|
||||
switch in {
|
||||
case "query":
|
||||
if strings.Contains(curl, "?") || strings.Contains(targetURL, "?") {
|
||||
targetURL += fmt.Sprintf("&%s=%s", name, pValue)
|
||||
} else {
|
||||
targetURL += fmt.Sprintf("?%s=%s", name, pValue)
|
||||
}
|
||||
case "path":
|
||||
targetURL = strings.Replace(targetURL, "{"+name+"}", pValue, 1)
|
||||
case "header":
|
||||
curl += fmt.Sprintf(" -H \"%s: %s\"", name, pValue)
|
||||
case "body":
|
||||
if strings.Contains(curl, "-d '") {
|
||||
bodyData += fmt.Sprintf("&%s=%s", name, pValue)
|
||||
curl = strings.TrimSuffix(curl, "'")
|
||||
curl += fmt.Sprintf("&%s=%s'", name, pValue)
|
||||
} else {
|
||||
bodyData += fmt.Sprintf("%s=%s", name, pValue)
|
||||
curl += fmt.Sprintf(" -d '%s=%s'", name, pValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bodyData, _ = json.Marshal(body)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if op.RequestBody != nil {
|
||||
body["test"] = "test"
|
||||
bodyData, _ = json.Marshal(body)
|
||||
}
|
||||
// Extracts the expected parameters from the requestBody object
|
||||
if reqBody, ok := opMap["requestBody"].(map[string]interface{}); ok {
|
||||
if contentTypes, ok := reqBody["content"].(map[string]interface{}); ok {
|
||||
for cType := range contentTypes {
|
||||
if contentType == "" {
|
||||
EnforceSingleContentType(cType)
|
||||
} else {
|
||||
EnforceSingleContentType(contentType)
|
||||
}
|
||||
|
||||
if apiInQuery && apiKey != "" {
|
||||
query.Add(apiKeyName, apiKey)
|
||||
}
|
||||
if ct, ok := contentTypes[cType].(map[string]interface{}); ok {
|
||||
if schema, ok := ct["schema"].(map[string]interface{}); ok {
|
||||
expanded := ExpandSchema(spec, schema, map[string]bool{})
|
||||
example := GenerateExample(expanded)
|
||||
|
||||
var errorDescriptions = make(map[any]string)
|
||||
for status := range op.Responses {
|
||||
if op.Responses[status].Ref == "" {
|
||||
if op.Responses[status].Value == nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logURL, parseErr := url.Parse(targetURL)
|
||||
if parseErr != nil || logURL == nil {
|
||||
log.Printf("Error parsing URL '%s': %v - skipping endpoint.", targetURL, parseErr)
|
||||
continue
|
||||
} else {
|
||||
errorDescriptions[status] = *op.Responses[status].Value.Description
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
u.Path = u.Path + newPath
|
||||
u.RawQuery = query.Encode()
|
||||
if command == "automate" {
|
||||
_, _, sc := MakeRequest(client, method, u.String(), timeout, bytes.NewReader([]byte(bodyData)), command)
|
||||
if sc == 200 {
|
||||
accessibleEndpointFound = true
|
||||
accessibleEndpoints = append(accessibleEndpoints, u.String())
|
||||
}
|
||||
if outfile != "" {
|
||||
if getAccessibleEndpoints {
|
||||
if sc == 200 {
|
||||
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
WriteJSONFile(result, false)
|
||||
switch os.Args[1] {
|
||||
case "automate":
|
||||
var postBodyData string
|
||||
if strings.ToLower(method) == "post" && strings.Contains(curl, "-d") {
|
||||
dataIndex := strings.Index(curl, "'")
|
||||
postBodyData = curl[dataIndex+1 : len(curl)-1]
|
||||
}
|
||||
} else {
|
||||
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
WriteJSONFile(result, false)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
} else if outfile == "" {
|
||||
if strings.ToLower(outputFormat) != "console" {
|
||||
|
||||
_, resp, sc := MakeRequest(client, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
|
||||
|
||||
tempResponsePreviewLength := responsePreviewLength
|
||||
if len(resp) <= responsePreviewLength {
|
||||
tempResponsePreviewLength = len(resp)
|
||||
}
|
||||
|
||||
var result []byte
|
||||
|
||||
if verbose {
|
||||
result, _ = json.Marshal(VerboseResult{Method: method, Preview: resp[:tempResponsePreviewLength], Status: sc, Target: logURL.Path, Curl: curl})
|
||||
} else {
|
||||
result, _ = json.Marshal(Result{Method: method, Status: sc, Target: logURL.Path})
|
||||
}
|
||||
|
||||
if getAccessibleEndpoints {
|
||||
if sc == 200 {
|
||||
resultsJSON = append(resultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)]))
|
||||
accessibleEndpoints = append(accessibleEndpoints, logURL.Path)
|
||||
if jsonResultsStringArray == nil {
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, string(result))
|
||||
} else {
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, ","+string(result))
|
||||
}
|
||||
if outputFormat == "console" {
|
||||
writeLog(sc, logURL.Path, strings.ToUpper(method), errorDescriptions[sc], resp[:tempResponsePreviewLength])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resultsJSON = append(resultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)]))
|
||||
if jsonResultsStringArray == nil {
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, string(result))
|
||||
} else {
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, ","+string(result))
|
||||
}
|
||||
if outputFormat == "console" {
|
||||
writeLog(sc, logURL.Path, strings.ToUpper(method), errorDescriptions[sc], resp[:tempResponsePreviewLength])
|
||||
}
|
||||
}
|
||||
} else if !getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
|
||||
writeLog(sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
}
|
||||
}
|
||||
} else if command == "prepare" {
|
||||
if bodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s'\n", method, u.String())
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -H '%s'\n", method, u.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s'\n", method, u.String(), bodyData)
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s' -H '%s'\n", method, u.String(), bodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if command == "endpoints" {
|
||||
|
||||
for k := range newDoc.Paths {
|
||||
if !pathMap[k] {
|
||||
paths = append(paths, k)
|
||||
pathMap[k] = true
|
||||
case "endpoints":
|
||||
fmt.Println(basePath + pathName)
|
||||
case "prepare":
|
||||
var preparedCommand string = curl
|
||||
if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
preparedCommand = strings.Replace(preparedCommand, "curl", "sqlmap", 1)
|
||||
preparedCommand = strings.Replace(preparedCommand, "-X "+strings.ToUpper(method), "--method="+strings.ToUpper(method)+" -u", 1)
|
||||
if bodyData != "" {
|
||||
preparedCommand = strings.Replace(preparedCommand, "-d '"+bodyData+"'", "--data='"+bodyData+"'", 1)
|
||||
}
|
||||
preparedCommand = "$ " + preparedCommand
|
||||
} else if prepareFor == "curl" {
|
||||
preparedCommand = "$ " + curl
|
||||
}
|
||||
fmt.Println(preparedCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if command == "automate" {
|
||||
if outfile == "" && getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
|
||||
var isDuplicateEndpoint bool
|
||||
var printedEndpoints []string
|
||||
if accessibleEndpoints != nil {
|
||||
log.Infof("Accessible endpoints:\n")
|
||||
for i := range accessibleEndpoints {
|
||||
for j := range printedEndpoints {
|
||||
if accessibleEndpoints[i] == printedEndpoints[j] {
|
||||
isDuplicateEndpoint = true
|
||||
}
|
||||
}
|
||||
if !isDuplicateEndpoint {
|
||||
fmt.Printf(" %s\n", accessibleEndpoints[i])
|
||||
printedEndpoints = append(printedEndpoints, accessibleEndpoints[i])
|
||||
}
|
||||
isDuplicateEndpoint = false
|
||||
if os.Args[1] == "automate" && outputFormat == "json" {
|
||||
slices.Sort(jsonResultsStringArray)
|
||||
for r := range jsonResultsStringArray {
|
||||
var result Result
|
||||
var verboseResult VerboseResult
|
||||
if verbose {
|
||||
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &verboseResult)
|
||||
if err != nil {
|
||||
log.Fatal("Error marshalling JSON:", err)
|
||||
}
|
||||
}
|
||||
} else if outfile == "" && strings.ToLower(outputFormat) == "json" {
|
||||
results := strings.Join(resultsJSON, ",")
|
||||
fmt.Printf(`%s"results":[%s]}%s`, specJSON, results, "\n")
|
||||
} else if outfile != "" {
|
||||
if accessibleEndpoints == nil {
|
||||
WriteJSONFile("]}\n", false)
|
||||
jsonVerboseResultArray = append(jsonVerboseResultArray, verboseResult)
|
||||
} else {
|
||||
WriteJSONFile(",", true)
|
||||
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &result)
|
||||
if err != nil {
|
||||
log.Fatal("Error marshalling JSON:", err)
|
||||
}
|
||||
jsonResultArray = append(jsonResultArray, result)
|
||||
}
|
||||
log.Infof("Results written to %s", outfile)
|
||||
}
|
||||
writeLog(8899, "", "", "", "")
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func GetBasePath(servers openapi3.Servers, host string) (bp string) {
|
||||
if basePath == "" {
|
||||
if servers != nil {
|
||||
s1, _ := url.Parse(servers[0].URL)
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
newContentType = strings.TrimSpace(newContentType)
|
||||
|
||||
if len(servers) >= 2 {
|
||||
s2, _ := url.Parse(servers[1].URL)
|
||||
if s1.Host != s2.Host || len(servers) > 2 {
|
||||
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 {
|
||||
fmt.Printf("Server %d: %s\n", i+1, servers[i].URL)
|
||||
// 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 == "" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if servers[0].URL == "/" {
|
||||
basePath = "/"
|
||||
} else {
|
||||
basePath = servers[0].URL
|
||||
if strings.Contains(basePath, host) || strings.Contains(basePath, "http") {
|
||||
basePath = strings.ReplaceAll(basePath, host, "")
|
||||
basePath = strings.ReplaceAll(basePath, "http://", "")
|
||||
basePath = strings.ReplaceAll(basePath, "https://", "")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
return basePath
|
||||
|
||||
// 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 PrintSpecInfo(i openapi3.Info) {
|
||||
specJSON = fmt.Sprintf(`{"title":"%s","description":"%s",`, i.Title, i.Description)
|
||||
if outfile != "" {
|
||||
WriteJSONFile(specJSON, false)
|
||||
} else if strings.ToLower(outputFormat) == "console" {
|
||||
if i.Title != "" {
|
||||
fmt.Println("Title:", i.Title)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if i.Description != "" {
|
||||
fmt.Printf("Description: %s\n\n", i.Description)
|
||||
description, ok := info["description"].(string)
|
||||
if ok && description != "" {
|
||||
if outputFormat == "json" {
|
||||
specDescription = description
|
||||
} else {
|
||||
fmt.Printf("Description: %s\n", description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +537,20 @@ 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.
|
||||
*/
|
||||
func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
|
||||
if apiTarget != "" {
|
||||
if strings.HasPrefix(apiTarget, "http://") {
|
||||
@@ -367,73 +566,29 @@ func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
|
||||
return host
|
||||
}
|
||||
|
||||
func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
|
||||
var doc openapi2.T
|
||||
var doc3 openapi3.T
|
||||
func XmlFromObject(obj map[string]interface{}) string {
|
||||
var b strings.Builder
|
||||
|
||||
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 {
|
||||
newDoc, err := openapi2conv.ToV3(&doc)
|
||||
if err != nil {
|
||||
fmt.Printf("Error converting v2 document to v3: %s\n", err)
|
||||
}
|
||||
return newDoc
|
||||
}
|
||||
}
|
||||
|
||||
func WriteJSONFile(result string, end bool) {
|
||||
if end {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR, 0644)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("File does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w := bufio.NewWriter(file)
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
log.Error("Failed to get file info:", err)
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
if fileSize != 0 {
|
||||
lastChar := fileSize - 1
|
||||
|
||||
_, err = file.Seek(lastChar, 0)
|
||||
if err != nil {
|
||||
log.Error("Error seeking to last character", err)
|
||||
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 + ">")
|
||||
}
|
||||
fmt.Fprintf(w, "]}\n")
|
||||
|
||||
default:
|
||||
b.WriteString(fmt.Sprintf("<%s>%v</%s>", k, val, k))
|
||||
}
|
||||
w.Flush()
|
||||
} else {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("File does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w := bufio.NewWriter(file)
|
||||
|
||||
fmt.Fprintf(w, "%s", result)
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 498 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 480 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 386 KiB |
Reference in new issue
Block a user