Compare commits

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

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

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

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

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

Bumps version to 2.3.1
2026-02-25 21:57:34 -06:00
Tony West 4c466112ee Added custom date, email, and URL flags 2026-02-03 17:10:52 -05:00
Tony West 1195ff7fbf Incremented version string 2026-02-03 13:09:21 -05:00
Tony West bd3265d0a4 Improved handling of schema references 2026-02-03 13:08:53 -05:00
Tony West d9fac2e9fe Updated version string 2026-01-28 14:30:49 -05:00
Tony West 10fe0376c0 Added the ability to customize the value used for testing identified string parameters 2026-01-28 14:30:25 -05:00
Tony West baf4ef6c51 Added prioritized list of endpoints for the brute command and fixed base path handling for v2 2026-01-15 18:37:16 -05:00
Tony West 8e2e5fbebc Fixed improper handling of base path 2026-01-15 10:54:22 -05:00
Tony West a77f9f46bb Merge pull request #49 from altperfect/main
Prevent header-processing logic from dropping custom headers in 'automate' mode
2026-01-15 10:37:28 -05:00
altperfect 5554e2bd30 fix: prevent header-processing logic from dropping custom headers 2026-01-15 19:08:39 +05:00
Tony West 09c460e768 Fixed issue that occurred when running against subdomains that contain '-d' in the subdomain name 2025-11-25 16:36:39 -05:00
Tony West ba45c6da26 Removed duplicate base path from output, minor changes to value type handling 2025-11-21 17:17:50 -05:00
Tony West 7417f9f917 Fixed bug where the base path was sometimes applied to the URL multiple times 2025-09-19 17:24:33 -04:00
Tony West 70a3d0b8c2 Updated the documentation 2025-09-16 21:40:16 -04:00
Tony West 77b11f40e6 Made some improvements for greater flexibility when encountering improperly defined API specifications 2025-09-16 21:40:04 -04:00
Tony West fd602b9ca8 Fixed a bug that prevented use of header values containing a colon 2025-04-15 13:24:23 -04:00
Tony West 83d1b57aef Merge pull request #41 from altperfect/fix/enforce-single-content-type
Fix EnforceSingleContentType function to not remove all custom headers
2025-03-18 16:06:26 -04:00
Tony West 6f903a7f78 Merge pull request #42 from altperfect/feat/add-curl-command-to-verbose-output
Add curl command in verbose output for 'automate' mode
2025-03-18 16:03:33 -04:00
Tony West 0d59e6c34f Additional fix to output format check 2025-03-18 15:57:32 -04:00
Tony West ceb99e4ddd Fixed insufficient output format check when displaying rate limit information 2025-03-18 15:54:55 -04:00
altperfect 8310c8995d feat: add curl command in verbose output for 'automate' mode 2025-03-13 04:52:43 +05:00
altperfect 017eb9af50 chore: bump version 2025-03-13 04:42:44 +05:00
altperfect 025f3f18cf fix: refactor EnforceSingleContentType to not remove all custom headers except Content-Type 2025-03-13 04:24:03 +05:00
Tony West 585100037a Updated documentation 2024-11-15 16:30:15 -05:00
Tony West f4b2634f42 Fixed improper handling of parameter values in the URL path 2024-11-15 16:28:29 -05:00
Tony West 2e9da2e810 Merge pull request #38 from six2dez/patch-1
Added failsafe check for files with no security definitions
2024-11-15 15:48:15 -05:00
Tony West 0fd8b6e6ec Updated documentation 2024-11-15 15:37:39 -05:00
Tony West 8acc15aad1 Added rate limit functionality to the automate and brute commands 2024-11-15 15:37:24 -05:00
six2dez 25d12a52e3 Added failsafe check for Swaggers with no security definitions
When a swagger file does not provide a security definition the tool crashes, now it gives a warning and continues
2024-11-08 10:20:21 +01:00
Tony West a9f907e2f1 Modified convert command to enable saved output even when definition file is already v3 2024-10-16 14:27:48 -04:00
Tony West 3b3fed2c99 Merge branch 'main' of https://github.com/BishopFox/sj 2024-10-09 18:07:14 -04:00
Tony West 1571b14873 Updated version string 2024-10-09 18:06:38 -04:00
Tony West 94e65fd96f Fixed missing check for 'formData' in the 'in' parameter object value 2024-10-09 18:06:26 -04:00
Tony West 5274b1b0ab Fixed null pointer bug 2024-10-09 13:06:50 -04:00
Tony West 49d6867ebf Now requires Go >= 1.21 2024-10-04 18:44:51 -04:00
Tony West 65d424c6a7 Added support for 'oneOf', 'anyOf', and 'allOf' schema keywords. 2024-10-03 22:47:27 -04:00
Tony West d7b34598d2 Fixed bug in verbose JSON output 2024-10-03 14:52:47 -04:00
Tony West 4526ddbf23 Fixed improper JSON output structure 2024-10-01 13:37:23 -04:00
Tony West 31e286235b Merge pull request #27 from mr-pmillz/main
Resolved nil pointer dereference
2024-09-20 15:27:05 -04:00
Tony West c84e679ac4 Merge branch 'main' into main 2024-09-20 15:26:18 -04:00
Tony West 41ba000755 Updated the version string. 2024-09-20 15:24:30 -04:00
Tony West cc99aef9ae Adds automatic sorting of results when output to the console 2024-09-20 15:13:14 -04:00
mr-pmillz b98e8c7d25 resolved nil pointer dereference 2 2024-09-16 18:23:45 -04:00
mr-pmillz 2879da6f87 resolved nil pointer dereference 2024-09-16 18:06:07 -04:00
Tony West 530143b4ee Fixed bug that caused improperly formatted JSON output 2024-09-13 18:29:02 -04:00
Tony West 69de3abbf1 Merge pull request #26 from idanya/fix-param-condition-check
Fix parameter handling bug in AddParametersToRequest() function
2024-09-13 11:49:58 -04:00
Tony West 9fd1b78081 Modified version number to reflect bug fix 2024-09-13 11:46:03 -04:00
Idan Yael c5a46d35bd Refactor parameter handling in SwaggerRequest.AddParametersToRequest() 2024-09-11 23:23:57 +03:00
Tony West 2877384d54 Updated issue templates 2024-08-26 22:03:33 -04:00
Tony West cb3e0bb16e Updated issue templates 2024-08-26 22:02:39 -04:00
Tony West 291edfe60c Added the 'convert' subcommand 2024-08-26 14:08:07 -04:00
Tony West 69b316e7bc Fixed bug in the '--get-accessible-endpoints' flag 2024-08-23 19:07:41 -04:00
Tony West 615f0c7a26 Fixed bug where output file would close pre-emptively 2024-08-23 13:27:38 -04:00
Tony West 5bbf0453a5 Fixed missing preview response for JSON output when the verbose flag is applied 2024-08-21 19:16:15 -04:00
Tony West a39028b463 Fixed bug in handling variations of schema references 2024-08-19 15:58:51 -04:00
Tony West e22c11c118 Added verbose output option for the automate command 2024-08-14 23:03:13 -04:00
Tony West 528a771c87 Fixed improper handling of defined XML content 2024-08-12 18:21:17 -04:00
Tony West c85512dfe0 Improved handling of nested schema references 2024-08-07 22:20:15 -04:00
Tony West 6a01afb367 Fixed bug that occurred when referencing non-existent schema values 2024-08-02 10:57:41 -04:00
Tony West 7ac01dc2a1 Fixed issue with fatal errors when encountering nested references 2024-08-01 18:29:49 -04:00
Tony West 9b48aab4fb Added warning when localhost (and equivalent) values are defined. 2024-07-30 18:47:59 -04:00
Tony West 8ce3ca7aa5 Fixed handling of x509 errors 2024-07-30 16:08:26 -04:00
Tony West 62be170a97 Update issue templates 2024-07-30 10:06:38 -04:00
Tony West a290716c76 Update README.md 2024-07-29 17:36:04 -04:00
Tony West 927b2adf53 Refactored schema reference handling code 2024-07-29 17:11:02 -04:00
Tony West cecaccc07a Added support for basic parsing of schema references 2024-07-29 13:28:24 -04:00
Tony West 54413d32e0 Updated README 2024-07-29 13:27:11 -04:00
Tony West c399ca1ff8 Fixed bug where parameter values did not populate correctly 2024-07-23 18:33:42 -04:00
Tony West 45dd989ef3 Fixed breaking change in 1.5.1 2024-07-23 12:11:54 -04:00
Tony West 1474d660e4 Fixed improper handling of empty server object 2024-07-19 14:07:23 -04:00
Tony West fe597147e6 Fixed improper parsing of the host object 2024-07-19 01:03:19 -04:00
Tony West 15cc756a03 Fixed output when target is set manually 2024-07-18 22:56:27 -04:00
Tony West 2b83f36c9d Fixed handling of TCP 'no such host' errors 2024-07-18 22:44:00 -04:00
Tony West 8261a1c154 Fixed bug in response handling when a definition file initiates a redirect 2024-07-18 16:43:43 -04:00
Tony West 17743c8c8a Merge pull request #8 from 0x303/feature_1-brute_custom_wordlists
Feature: brute custom wordlists
2024-07-16 17:52:30 -04:00
Tony West 03ac3f1a3d Updated version string, code cleanup 2024-07-16 17:51:21 -04:00
Hank 3f02c53d94 add a param to pass in a wordlist to the brute subcommand 2024-07-16 14:41:03 -04:00
Hank 246d6f1241 add updated path to the prefix paths 2024-07-16 13:07:51 -04:00
Tony West eef14078b4 Merge pull request #7 from affix/fix_basic_auth
Fix for issue #6 - Handle Bearer tokens correctly
2024-07-15 10:50:38 -04:00
Tony West 20c02e4fa6 Updated version string 2024-07-15 10:49:24 -04:00
Keiran Smith 1fc2b657da Fix for issue #6 - Handle Bearer tokens correctly 2024-07-13 22:14:24 +01:00
Tony West 3d0e81ffd4 Fixes bug due to improper handling of definition files with no servers defined 2024-07-12 13:42:36 -04:00
Tony West 44141deba9 Fixed issue where automate command could only test one defined server. 2024-07-08 23:14:55 -04:00
Tony West 91c8ffd56f Bug fixes for improper handling of the server object to populate base paths. Also added additional paths to the brute command. 2024-07-03 14:16:59 -04:00
Tony West 75da0224d3 Fixed improper base path handling in the prepare command 2024-06-17 14:40:20 -04:00
Tony West 540d6064dc Fixed missing base path in prepare command 2024-06-17 14:30:50 -04:00
Tony West 686022c827 Added sqlmap support to the 'prepare' command. 2024-06-17 14:03:51 -04:00
Tony West df162f7c35 Fixes improper content check in brute command 2024-06-11 15:18:52 -04:00
Tony West 4d7814c74e Updated release config 2024-06-10 22:58:26 -04:00
Tony West 4bde10ad1b Updated release config 2024-06-10 22:53:44 -04:00
Tony West 4d05f51fbe fixes multiple output and error handling bugs 2024-06-10 22:46:36 -04:00
22 changed files with 1589 additions and 703 deletions

No files matched your search

+42
View File
@@ -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.
+24
View File
@@ -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.
+17
View File
@@ -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
+151 -10
View File
@@ -1,16 +1,19 @@
# sj (Swagger Jacker)
![](img/sj-logo.png)
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.
![Automate Command](img/sj-automate.gif)
```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
```
![Prepare Command](img/sj-prepare.gif)
> 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.
![Endpoints Command](img/sj-endpoints.gif)
```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:
![Help Command](img/sj-help.gif)
```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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -5,7 +5,6 @@ import (
"io"
"os"
"github.com/mpvl/unique"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -16,28 +15,29 @@ var endpointsCmd = &cobra.Command{
Long: `The endpoints command allows you to pull a list of endpoints out of a Swagger definition file.
This list contains the raw endpoints (parameter values will not be appended or modified).`,
Run: func(cmd *cobra.Command, args []string) {
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
client := CheckAndConfigureProxy()
var bodyBytes []byte
var paths []string
fmt.Printf("\n")
log.Infof("Gathering endpoints.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil, "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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.")
-31
View File
@@ -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
View File
@@ -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()
}
+9 -8
View File
@@ -1,25 +1,26 @@
module github.com/BishopFox/sj
go 1.20
go 1.22.5
toolchain go1.23.4
require (
github.com/getkin/kin-openapi v0.115.0
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de
github.com/getkin/kin-openapi v0.132.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.4 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+22 -35
View File
@@ -2,38 +2,36 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.115.0 h1:c8WHRLVY3G8m9jQTy0/DnIuljgRwTCB5twZytQS4JyU=
github.com/getkin/kin-openapi v0.115.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto=
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY=
github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -42,27 +40,16 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB