Compare commits

...
45 Commits
Author SHA1 Message Date
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
17 changed files with 1058 additions and 442 deletions

No files matched your search

+15 -11
View File
@@ -8,31 +8,35 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
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]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
@@ -8,13 +8,17 @@ 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.
+5
View File
@@ -11,6 +11,11 @@ builds:
- linux
- windows
- darwin
flags:
- -trimpath
ldflags:
- -s -w -X main.build={{.Version}}
archives:
- format: tar.gz
+98 -49
View File
@@ -4,15 +4,16 @@
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 four 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.21` installed and run `go build` from within the repository:
```bash
$ git clone https://github.com/BishopFox/sj.git
@@ -38,32 +39,50 @@ $ export PATH=$PATH:~/go/bin
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -q
INFO[0000] Gathering API details.
INFO[0000] Gathering API details.
INFO[0000] Sending requests at a rate of 15 requests per second.
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
INFO[0000] Available authentication mechanisms:
- api_key
- petstore_auth
WARN[0000] Results for https://petstore.swagger.io/v2:
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByTags?tags=1"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/store/order"
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithList"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/user"
ERRO[0000] User not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/user/test"
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/user/test"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/login?password=test&username=test"
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithArray"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/logout"
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test"
ERRO[0000] Pet not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/pet/test"
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test/uploadImage"
ERRO[0000] Order not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/store/order/test"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/store/inventory"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/pet"
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/pet"
INFO[0001] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByStatus?status=1"
WARN[0007] Results for http://petstore.swagger.io/v2:
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/pet
WARN Manual testing may be required. Method=PUT Status=415 Target=http://petstore.swagger.io/v2/pet
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/pet/1
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/pet/1
WARN Manual testing may be required. Method=POST Status=400 Target=http://petstore.swagger.io/v2/pet/1/uploadImage
INFO Endpoint accessible! Method=GET Status=200 Target="http://petstore.swagger.io/v2/pet/findByStatus?status=1"
INFO Endpoint accessible! Method=GET Status=200 Target="http://petstore.swagger.io/v2/pet/findByTags?tags=1"
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/store/inventory
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/store/order
ERRO Order not found Method=GET Status=404 Target=http://petstore.swagger.io/v2/store/order/1
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/user
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/user/createWithArray
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/user/createWithList
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/user/login?password=test&username=test
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/user/logout
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/user/test
WARN Manual testing may be required. Method=PUT Status=415 Target=http://petstore.swagger.io/v2/user/test
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/pet
WARN Manual testing may be required. Method=PUT Status=415 Target=https://petstore.swagger.io/v2/pet
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/pet/1
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/pet/1
WARN Manual testing may be required. Method=POST Status=400 Target=https://petstore.swagger.io/v2/pet/1/uploadImage
INFO Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByStatus?status=1"
INFO Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByTags?tags=1"
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/store/inventory
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/store/order
ERRO Order not found Method=GET Status=404 Target=https://petstore.swagger.io/v2/store/order/1
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/user
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/user/createWithArray
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/user/createWithList
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/user/login?password=test&username=test
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/user/logout
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/user/test
WARN Manual testing may be required. Method=PUT Status=415 Target=https://petstore.swagger.io/v2/user/test
```
> 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.
@@ -72,31 +91,46 @@ INFO[0001] Endpoint accessible! Method=GET Status=200 T
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -q
INFO[0000] Gathering API details.
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
INFO[0000] Available authentication mechanisms:
- api_key
- petstore_auth
curl -sk -X PUT 'https://petstore.swagger.io/v2/user/test' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/test'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/test'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/store/order' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
curl -sk -X PUT 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/order/test'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByTags?tags=1'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithList' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/user' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/logout'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/login?password=test&username=test'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test/uploadImage' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByStatus?status=1'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/inventory'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithArray' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet' -d '<root><name>test</name><id></id><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags><status>test</status></root>' -H 'Content-Type: application/xml'
curl -sk -X PUT 'https://petstore.swagger.io/v2/pet' -d '<root><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags><status>test</status><id></id><name>test</name></root>' -H 'Content-Type: application/xml'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/logout' -H 'Content-Type: application/xml'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/login?password=test&username=test' -H 'Content-Type: application/xml'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByTags?tags=1' -H 'Content-Type: application/xml'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithList' -H 'Content-Type: application/json'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByStatus?status=1' -H 'Content-Type: application/json'
curl -sk -X POST 'https://petstore.swagger.io/v2/user' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
curl -sk -X POST 'https://petstore.swagger.io/v2/store/order' -d '{"complete":false,"id":1,"petId":1,"quantity":1,"shipDate":"test","status":"test"}' -H 'Content-Type: application/json'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/order/1' -H 'Content-Type: application/json'
curl -sk -X PUT 'https://petstore.swagger.io/v2/user/test' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/test' -H 'Content-Type: application/json'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/1' -d 'name=test&status=test' -H 'Content-Type: application/x-www-form-urlencoded'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/1' -H 'Content-Type: application/x-www-form-urlencoded'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithArray' -H 'Content-Type: application/json'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/1/uploadImage' -d 'test=test' -H 'Content-Type: multipart/form-data'
curl -sk -X POST 'http://petstore.swagger.io/v2/store/order' -d '{"complete":false,"id":1,"petId":1,"quantity":1,"shipDate":"test","status":"test"}' -H 'Content-Type: application/json'
curl -sk -X GET 'http://petstore.swagger.io/v2/store/order/1' -H 'Content-Type: application/json'
curl -sk -X GET 'http://petstore.swagger.io/v2/user/test' -H 'Content-Type: application/json'
curl -sk -X PUT 'http://petstore.swagger.io/v2/user/test' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
curl -sk -X POST 'http://petstore.swagger.io/v2/pet/1' -d 'name=test&status=test' -H 'Content-Type: application/x-www-form-urlencoded'
curl -sk -X GET 'http://petstore.swagger.io/v2/pet/1' -H 'Content-Type: application/x-www-form-urlencoded'
curl -sk -X POST 'http://petstore.swagger.io/v2/user/createWithArray' -H 'Content-Type: application/json'
curl -sk -X POST 'http://petstore.swagger.io/v2/pet/1/uploadImage' -d 'test=test' -H 'Content-Type: multipart/form-data'
curl -sk -X GET 'http://petstore.swagger.io/v2/pet/findByStatus?status=1' -H 'Content-Type: multipart/form-data'
curl -sk -X POST 'http://petstore.swagger.io/v2/user' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
curl -sk -X PUT 'http://petstore.swagger.io/v2/pet' -d '<root><id>1</id><name>test</name><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags><status>test</status></root>' -H 'Content-Type: application/xml'
curl -sk -X POST 'http://petstore.swagger.io/v2/pet' -d '<root><status>test</status><id></id><name>test</name><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags></root>' -H 'Content-Type: application/xml'
curl -sk -X GET 'http://petstore.swagger.io/v2/store/inventory' -H 'Content-Type: application/xml'
curl -sk -X GET 'http://petstore.swagger.io/v2/user/login?password=test&username=test' -H 'Content-Type: application/xml'
curl -sk -X GET 'http://petstore.swagger.io/v2/user/logout' -H 'Content-Type: application/xml'
curl -sk -X POST 'http://petstore.swagger.io/v2/user/createWithList' -H 'Content-Type: application/json'
curl -sk -X GET 'http://petstore.swagger.io/v2/pet/findByTags?tags=1' -H 'Content-Type: application/json'
```
> Use the `endpoints` command to generate a list of raw endpoints from the provided definition file.
@@ -125,13 +159,23 @@ INFO[0000] Gathering endpoints.
> Use the `brute` command to send a series of requests in an attempt to find a definition file on the target.
```bash
$ sj brute -u https://petstore.swagger.io/v2/swagger.json
INFO[0000] Sending 2045 requests. This could take a while...
Request: 343
INFO[0015] Definition file found: https://petstore.swagger.io/v2/swagger
$ sj brute -u https://petstore.swagger.io
INFO[0000] Sending 2173 requests at a rate of 15 requests per second. This could take a while...
Request: 1294
INFO[0090] Definition file found: https://petstore.swagger.io/v2/swagger.json
{"...SNIP..."}
```
> Use the `convert` command to convert a definition file from version 2 to version 3.
```bash
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
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:
@@ -157,6 +201,9 @@ $ 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]
@@ -164,12 +211,13 @@ Usage:
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)")
-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.
@@ -179,7 +227,8 @@ Flags:
-o, --outfile string Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.
-p, --proxy string Proxy host and port. Example: http://127.0.0.1:8080 (default "NOPROXY")
-q, --quiet Do not prompt for user input - uses default values for all requests.
-r, --randomize-user-agent Randomizes the user agent string. Default is 'false'.
--randomize-user-agent Randomizes the user agent string. Default is 'false'.
-r, --rate int Limit the number of requests per second. (default 15)
-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)
+7
View File
@@ -23,6 +23,13 @@ var (
func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName string) {
if doc3.Components == nil || doc3.Components.SecuritySchemes == nil || len(doc3.Components.SecuritySchemes) == 0 {
if !quiet {
log.Warn("No security schemes defined in the OpenAPI specification.")
}
return false, "", ""
}
if outputFormat == "json" && !quiet {
if len(doc3.Components.SecuritySchemes) != 0 {
log.Warnf("The following authentication mechanisms are supported. If necessary, supply these manually when using the JSON output format:\n")
+22 -2
View File
@@ -13,6 +13,9 @@ import (
var getAccessibleEndpoints bool
var outputFormat string
var responsePreviewLength int
var verbose bool
var automateCmd = &cobra.Command{
Use: "automate",
Short: "Sends a series of automated requests to the discovered endpoints.",
@@ -20,10 +23,21 @@ 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"
}
}
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)
}
}
@@ -33,7 +47,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
log.Infof("Gathering API details.\n\n")
log.Infof("Gathering API details.\n")
}
if swaggerURL != "" {
@@ -46,6 +60,9 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
bodyBytes, _ = io.ReadAll(specFile)
}
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
log.Info("Sending requests at a rate of ", rateLimit, " requests per second.")
}
GenerateRequests(bodyBytes, client)
},
}
@@ -53,4 +70,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
func init() {
automateCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "console", "The output format. Only 'console' (default) and 'json' are supported at the moment.")
automateCmd.PersistentFlags().BoolVar(&getAccessibleEndpoints, "get-accessible-endpoints", false, "Only output the accessible endpoints (those that return a 200 status code).")
automateCmd.PersistentFlags().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.")
}
+53 -29
View File
@@ -2,6 +2,7 @@ package cmd
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -10,9 +11,11 @@ import (
"path/filepath"
"regexp"
"strings"
"time"
"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cobra"
"golang.org/x/time/rate"
log "github.com/sirupsen/logrus"
)
@@ -29,6 +32,16 @@ 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) {
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
@@ -60,7 +73,11 @@ var bruteCmd = &cobra.Command{
log.Fatalf("failed to read words from file: %s", err)
}
}
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
log.Infof("Sending %d requests at a rate of %d requests per second. This could take a while...\n", len(allURLs), rateLimit)
} else {
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
}
specFound, definitionFile := findDefinitionFile(allURLs, client)
if specFound {
@@ -111,39 +128,46 @@ func makeURLs(target string, endpoints []string, fileExtension string) []string
}
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
var rateLimiter = rate.NewLimiter(rate.Every(time.Second/(time.Duration(rateLimit))), 1)
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 {
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 rateLimit > 0 {
if err := rateLimiter.Wait(context.Background()); err != nil {
fmt.Println("Rate limit error...")
} else {
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 {
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)
}
}
}
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
}
+127
View File
@@ -0,0 +1,127 @@
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)
}
if format == "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)
}
}
} else if format == "yaml" || format == "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)
}
}
+6
View File
@@ -16,6 +16,12 @@ 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
+178 -33
View File
@@ -1,90 +1,235 @@
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 {
logBad(sc, target, method)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg)
} else if sc == 1 {
logDangerous(target, method)
} 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 != "" {
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)
}
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 {
if sc != 200 {
if sc == 401 || sc == 403 {
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
} else if sc == 301 || sc == 302 {
logVerboseRedirect(sc, target, method, response, tempLogger)
} else if sc == 0 {
logVerboseBad(sc, target, method, response, tempLogger)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg, tempLogger)
} else if sc == 1 {
logDangerous(target, method, tempLogger)
} else if sc == 8899 {
logVerboseJSON(target, method)
} else {
logVerboseManual(sc, target, method, errorMsg, response, tempLogger)
}
} else {
logVerboseAccessible(sc, target, method, response, tempLogger)
}
} else {
if sc != 200 {
if sc == 401 || sc == 403 {
logUnauth(sc, target, method, errorMsg, tempLogger)
} else if sc == 301 || sc == 302 {
logRedirect(sc, target, method, tempLogger)
} else if sc == 0 {
logBad(sc, target, method, tempLogger)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg, tempLogger)
} else if sc == 1 {
logDangerous(target, method, tempLogger)
} else if sc == 8899 {
logJSON(target, method)
} else {
logManual(sc, target, method, errorMsg, tempLogger)
}
} else {
if sc == 8899 {
logJSON(target, method)
} else {
logAccessible(sc, target, method, tempLogger)
}
}
}
responsePreviewLength = tempResponsePreviewLength
file.Close()
}
func logAccessible(status int, target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Print("Endpoint accessible!")
}
func logDangerous(target, method string) {
log.WithFields(log.Fields{
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.")
}).Warn("Endpoint skipped due to dangerous keyword (or request cancelled due to timeout).")
}
func logManual(status int, target, method, errorMsg string) {
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 logBad(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("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")
}
+35
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -15,6 +16,13 @@ var prepareCmd = &cobra.Command{
Long: `The prepare command prepares a set of commands for manual testing of each endpoint.
This enables you to test specific API functions for common vulnerabilities or misconfigurations.`,
Run: func(cmd *cobra.Command, args []string) {
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
client := CheckAndConfigureProxy()
fmt.Printf("\n")
@@ -35,6 +43,33 @@ This enables you to test specific API functions for common vulnerabilities or mi
}
var prepareFor string
func (s SwaggerRequest) PrintPreparedCommands(method string) {
var preparedCommand string
if strings.ToLower(prepareFor) == "curl" {
preparedCommand = fmt.Sprintf("curl -sk -X %s '%s'", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = fmt.Sprintf("sqlmap --method=%s -u %s", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "ffuf" {
log.Fatal("ffuf is not supported yet :(")
// TODO
} else {
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
}
if s.BodyData != nil {
if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = preparedCommand + fmt.Sprintf(" --data='%s'", s.BodyData)
} else {
preparedCommand = preparedCommand + fmt.Sprintf(" -d '%s'", s.BodyData)
}
}
if len(Headers) != 0 {
preparedCommand = preparedCommand + fmt.Sprintf(" -H '%s'", strings.Join(Headers, "' -H '"))
}
fmt.Println(preparedCommand)
}
func init() {
prepareCmd.PersistentFlags().StringVarP(&prepareFor, "external-tool", "e", "curl", "The external tool to prepare commands for. Generates syntax for 'curl' by default.")
}
+37 -8
View File
@@ -2,6 +2,7 @@ package cmd
import (
"context"
"crypto/tls"
"fmt"
"io"
"math/rand"
@@ -104,19 +105,15 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
}
req.Header.Set(h[0], h[1])
} else {
log.Fatal("Custom header provided cannot be used.")
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])
}
}
// 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)
}
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)
}
@@ -177,6 +174,15 @@ func CheckContentType(client http.Client, target string) string {
log.Fatal("Error: could not create HTTP request - ", err)
}
// 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))
if err == context.DeadlineExceeded {
log.Printf("Error: %s - skipping request.", err)
@@ -193,3 +199,26 @@ func CheckContentType(client http.Client, target string) string {
}
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
}
+10 -5
View File
@@ -5,7 +5,6 @@ import (
"github.com/spf13/cobra"
)
var accessibleEndpointFound bool
var apiTarget string
var basePath string
var format string
@@ -15,6 +14,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
@@ -39,14 +39,17 @@ 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.6.2",
Version: "1.10.2",
}
func Execute() {
@@ -58,7 +61,8 @@ 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.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(&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.")
@@ -67,7 +71,8 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&outfile, "outfile", "o", "", "Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.")
rootCmd.PersistentFlags().StringVarP(&proxy, "proxy", "p", "NOPROXY", "Proxy host and port. Example: http://127.0.0.1:8080")
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Do not prompt for user input - uses default values for all requests.")
rootCmd.PersistentFlags().BoolVarP(&randomUserAgent, "randomize-user-agent", "r", false, "Randomizes the user agent string. Default is 'false'.")
rootCmd.PersistentFlags().BoolVar(&randomUserAgent, "randomize-user-agent", false, "Randomizes the user agent string. Default is 'false'.")
rootCmd.PersistentFlags().IntVarP(&rateLimit, "rate", "r", 15, "Limit the number of requests per second.")
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.")
+175
View File
@@ -0,0 +1,175 @@
package cmd
import (
"fmt"
"math/rand"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
type SchemaReference struct {
Required []string
Type string
Properties struct {
Property struct {
Type string
Format string
Example string
Reference string
XML struct {
Name string
Namespace string
Prefix string
Attribute bool
Wrapped bool
}
}
}
}
func HandleSchemaReference(schema *openapi3.SchemaRef) {
}
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
if param != nil {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else if location == "path" {
if schema.Value.Properties[property].Value.Example != "" && schema.Value.Properties[property].Value.Example != nil {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", schema.Value.Properties[property].Value.Example.(string))
} else if schema.Value.Properties[property].Value.Type == "string" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else if location == "query" {
if schema.Value.Properties[property].Value.Example != "" && schema.Value.Properties[property].Value.Example != nil {
s.Query.Add(param.Value.Name, (schema.Value.Properties[property].Value.Example).(string))
} else if schema.Value.Properties[property].Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
}
} else if location == "body" {
if schema.Value.Properties[property].Value.Example != "" && schema.Value.Properties[property].Value.Example != nil {
s.Body[param.Value.Name] = schema.Value.Properties[property].Value.Example
} else if schema.Value.Properties[property].Value.Type == "string" {
s.Body[param.Value.Name] = "test"
} else {
s.Body[param.Value.Name] = 1
}
}
}
} else if schema.Value.Enum != nil {
if location == "path" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
} else if location == "query" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Query.Add(param.Value.Name, fmt.Sprint(value))
} else if location == "body" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[param.Value.Name] = value
}
} else if schema.Value.AllOf != nil {
for i := range schema.Value.AllOf {
if schema.Value.AllOf[i].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AllOf[i].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
break
}
}
}
} else if schema.Value.OneOf != nil && schema.Value.OneOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.OneOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
} else if schema.Value.AnyOf != nil && schema.Value.AnyOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AnyOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
}
}
} else {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else {
if schema.Value.Properties[property].Value.Example != "" {
s.Body[property] = schema.Value.Properties[property].Value.Example
} else if schema.Value.Properties[property].Value.Type == "string" {
s.Body[property] = "test"
} else {
s.Body[property] = 1
}
}
}
} else if schema.Value.Enum != nil {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[name] = value
} else {
if schema.Value.AllOf != nil {
for i := range schema.Value.AllOf {
if schema.Value.AllOf[i].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AllOf[i].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
break
}
}
}
} else if schema.Value.OneOf != nil && schema.Value.OneOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.OneOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
} else if schema.Value.AnyOf != nil && schema.Value.AnyOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AnyOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
}
}
}
}
return s
}
+283 -305
View File
@@ -1,15 +1,17 @@
package cmd
import (
"bufio"
"bytes"
"crypto/tls"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"math/rand"
"net/http"
"net/url"
"os"
"slices"
"sort"
"strconv"
"strings"
"time"
@@ -17,6 +19,7 @@ import (
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
"gopkg.in/yaml.v3"
)
@@ -37,7 +40,10 @@ type SwaggerRequest struct {
}
var accessibleEndpoints []string
var specJSON string
var consoleResults map[int][]any
var jsonResultsStringArray []string
var jsonResultArray []Result
var jsonVerboseResultArray []VerboseResult
func GenerateRequests(bodyBytes []byte, client http.Client) []string {
@@ -52,10 +58,6 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
PrintSpecInfo(*s.Def.Info)
}
if os.Args[1] == "automate" && outfile != "" {
WriteJSONFile(`"results":[`, false)
}
if s.Def.Paths == nil {
log.Fatalf("Could not find any defined operations. Review the file manually.")
}
@@ -70,7 +72,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
fmt.Println()
}
}
if len(s.Def.Servers) > 1 && apiTarget == "" {
if apiTarget == "" {
for _, server := range s.Def.Servers {
if outputFormat == "console" && os.Args[1] == "automate" {
log.Warnf("Results for %s:\n", server.URL)
@@ -79,6 +81,11 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
if server.URL == "/" {
s.Path = ""
}
if strings.Contains(server.URL, "localhost") || strings.Contains(server.URL, "127.0.0.1") || strings.Contains(server.URL, "::1") {
log.Warn("The server(s) documented in the definition file contain(s) a local host value and may result in errors. Supply a target manually using the '-T' flag.")
}
u, _ := url.Parse(server.URL)
s.URL = *u
s = s.IterateOverPaths(client)
@@ -94,11 +101,38 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
} else {
if apiTarget != "" {
u, _ = url.Parse(apiTarget)
} else {
if swaggerURL == "" {
u, _ = url.Parse(s.Def.Servers[0].URL)
if strings.Contains(s.Def.Servers[0].URL, "localhost") || strings.Contains(s.Def.Servers[0].URL, "127.0.0.1") || strings.Contains(s.Def.Servers[0].URL, "::1") {
log.Warn("The server documented in the definition file contains a local host value and may result in errors. Supply a target manually using the '-T' flag.")
}
}
}
s.URL = *u
s = s.IterateOverPaths(client)
}
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)
}
jsonVerboseResultArray = append(jsonVerboseResultArray, verboseResult)
} else {
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &result)
if err != nil {
log.Fatal("Error marshalling JSON:", err)
}
jsonResultArray = append(jsonResultArray, result)
}
}
if os.Args[1] == "automate" {
if outfile == "" && getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
var isDuplicateEndpoint bool
@@ -118,16 +152,38 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
isDuplicateEndpoint = false
}
}
} else if outfile == "" && strings.ToLower(outputFormat) == "json" {
results := strings.Join(s.ResultsJSON, ",")
fmt.Printf(`%s"results":[%s]}%s`, specJSON, results, "\n")
} else if outfile != "" {
if accessibleEndpoints == nil {
WriteJSONFile("]}\n", false)
} else {
WriteJSONFile(",", true)
}
if strings.ToLower(outputFormat) == "json" {
writeLog(8899, s.Def.Info.Title, s.Def.Info.Description, "", "") // This is a temporary workaround to get the logging to work as intended.. needs to be fixed eventually.
} else if strings.ToLower(outputFormat) == "console" && !getAccessibleEndpoints {
var r []string
for i := range consoleResults {
tempString := fmt.Sprintf("%s,%s,%d,%s,%s", consoleResults[i][1], consoleResults[i][2], consoleResults[i][0], consoleResults[i][3], consoleResults[i][4])
r = append(r, tempString)
}
sort.Strings(r)
for i := range r {
result := r[i]
i1 := strings.Index(r[i], ",")
endpoint := result[:i1]
result = result[i1+1:]
i2 := strings.Index(result, ",")
method := result[:i2]
result = result[i2+1:]
i3 := strings.Index(result, ",")
statusCode, _ := strconv.Atoi(result[:i3])
result = result[i3+1:]
i4 := strings.Index(result, ",")
errorMessage := result[:i4]
result = result[i4+1:]
responsePreview := result
writeLog(statusCode, endpoint, method, errorMessage, responsePreview)
}
log.Infof("Results written to %s", outfile)
}
}
return s.Paths
@@ -146,12 +202,20 @@ func (s SwaggerRequest) IterateOverPaths(client http.Client) SwaggerRequest {
"TRACE": pathItem.Trace,
}
var rateLimiter = rate.NewLimiter(rate.Every(time.Second/(time.Duration(rateLimit))), 1)
for method, op := range operations {
// Do all the things here :D
s.BodyData = nil
if op != nil {
s.URL.Path = s.Path + path
s = s.BuildDefinedRequests(client, method, pathItem, op)
if rateLimit > 0 {
if err := rateLimiter.Wait(context.Background()); err != nil {
fmt.Println("Rate limit error...")
} else {
if op != nil {
s.URL.Path = s.Path + path
s = s.BuildDefinedRequests(client, method, pathItem, op)
}
}
}
}
}
@@ -186,74 +250,61 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
s.URL.RawQuery = s.Query.Encode()
if os.Args[1] == "automate" {
_, _, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
tempResponsePreviewLength := responsePreviewLength
_, resp, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
if len(resp) < responsePreviewLength {
tempResponsePreviewLength = len(resp)
}
if sc == 200 {
accessibleEndpointFound = true
accessibleEndpoints = append(accessibleEndpoints, s.URL.String())
}
if outfile != "" {
if strings.ToLower(outputFormat) != "console" {
curlCommand := fmt.Sprintf("curl -X %s -sk %s", method, s.URL.String())
if s.BodyData != nil {
curlCommand = fmt.Sprintf("%s -d '%s'", curlCommand, string(s.BodyData))
}
if len(Headers) != 0 {
curlCommand = fmt.Sprintf("%s -H '%s'", curlCommand, strings.Join(Headers, "' -H '"))
}
var result []byte
if getAccessibleEndpoints {
if sc == 200 {
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
WriteJSONFile(result, false)
if verbose {
result, _ = json.Marshal(VerboseResult{Method: method, Preview: resp[:tempResponsePreviewLength], Status: sc, Target: s.URL.String(), Curl: curlCommand})
} else {
result, _ = json.Marshal(Result{Method: method, Status: sc, Target: s.URL.String()})
}
if jsonResultsStringArray == nil {
jsonResultsStringArray = append(jsonResultsStringArray, string(result))
} else {
jsonResultsStringArray = append(jsonResultsStringArray, ","+string(result))
}
}
} else {
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
WriteJSONFile(result, false)
}
time.Sleep(1 * time.Second)
} else if outfile == "" {
if strings.ToLower(outputFormat) != "console" {
if getAccessibleEndpoints {
if sc == 200 {
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)]))
}
if verbose {
result, _ = json.Marshal(VerboseResult{Method: method, Preview: resp[:tempResponsePreviewLength], Status: sc, Target: s.URL.String(), Curl: curlCommand})
} else {
if sc == 1 {
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"Skipped due to dangerous keyword in request"}`, sc, s.URL.String(), method))
} else {
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)]))
}
result, _ = json.Marshal(Result{Method: method, Status: sc, Target: s.URL.String()})
}
} else if !getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
jsonResultsStringArray = append(jsonResultsStringArray, string(result))
}
} else {
if !getAccessibleEndpoints {
if consoleResults == nil {
consoleResults = make(map[int][]any)
}
n := len(consoleResults)
if n != 0 {
n = n + 1
}
var r []any
r = append(r, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
consoleResults[n] = r
}
}
} else if os.Args[1] == "prepare" {
if strings.ToLower(prepareFor) == "curl" {
if s.BodyData == nil {
if len(Headers) == 0 {
fmt.Printf("curl -sk -X %s '%s'\n", method, s.URL.String())
} else {
fmt.Printf("curl -sk -X %s '%s' -H '%s'\n", method, s.URL.String(), strings.Join(Headers, "' -H '"))
}
} else {
if len(Headers) == 0 {
fmt.Printf("curl -sk -X %s '%s' -d '%s'\n", method, s.URL.String(), s.BodyData)
} else {
fmt.Printf("curl -sk -X %s '%s' -d '%s' -H '%s'\n", method, s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
}
}
} else if strings.ToLower(prepareFor) == "sqlmap" {
if s.BodyData == nil {
if len(Headers) == 0 {
fmt.Printf("sqlmap -u %s\n", s.URL.String())
} else {
fmt.Printf("sqlmap -u %s -H '%s'\n", s.URL.String(), strings.Join(Headers, "' -H '"))
}
} else {
if len(Headers) == 0 {
fmt.Printf("sqlmap -u %s --data='%s'\n", s.URL.String(), s.BodyData)
} else {
fmt.Printf("sqlmap -u %s --data='%s' -H '%s'\n", s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
}
}
} else if strings.ToLower(prepareFor) == "ffuf" {
// TODO
} else {
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
}
s.PrintPreparedCommands(method)
} else if os.Args[1] == "endpoints" {
for k := range s.Def.Paths {
@@ -269,39 +320,52 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
// This whole function needs to be refactored/cleaned up a bit
func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRequest {
for _, param := range op.Parameters {
if param.Value == nil && param.Value.Schema.Ref == "" {
if param.Value == nil {
continue
} else if param.Value.In == "path" {
if param.Value.Schema.Ref != "" {
s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil)
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
if param.Value.Schema != nil {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil, 0)
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else if param.Value.In == "query" {
if param.Value.Schema.Ref != "" {
s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil)
} else {
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
if param.Value.Schema != nil {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil, 0)
} else {
s.Query.Add(param.Value.Name, "1")
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
}
}
} else {
s.Query.Add(param.Value.Name, "test")
}
} else if param.Value.In == "header" && param.Value.Required {
} else if param.Value.In == "header" && param.Value.Required && strings.ToLower(param.Value.Name) != "content-type" {
Headers = append(Headers, fmt.Sprintf("%s: %s", param.Value.Name, "1"))
} else if param.Value.In == "body" {
} else if param.Value.In == "body" || param.Value.In == "formData" {
if param.Value.Schema.Ref != "" {
s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil)
s = s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil, 0)
}
if param.Value.Schema.Value.Type == "string" {
s.Body[param.Value.Name] = "test"
@@ -319,82 +383,120 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
}
if op.RequestBody != nil {
if op.RequestBody.Value.Content != nil {
for i := range op.RequestBody.Value.Content {
if contentType == "" && Headers != nil && strings.Contains(strings.ToLower(strings.Join(Headers, ",")), "content-type") {
headerString := strings.Join(Headers, ",")
Headers = nil
ctIndex := strings.Index(strings.ToLower(headerString), "content-type:") + 14
headerString = headerString[ctIndex:]
if strings.Contains(headerString, ",") {
headerString = strings.TrimPrefix(headerString, ",")
ctEndIndex := strings.Index(headerString[ctIndex:], ",") + 1
headerString = headerString[:ctEndIndex]
} else if !strings.Contains(strings.ToLower(headerString), "content-type") && (strings.Contains(strings.ToLower(headerString), "application/x-www-form-urlencoded") || strings.Contains(strings.ToLower(headerString), "application/json")) {
headerString = ""
}
if headerString != "" {
Headers = append(Headers, strings.Split(headerString, ",")...)
if op.RequestBody.Value != nil {
if op.RequestBody.Value.Content != nil {
for i := range op.RequestBody.Value.Content {
if contentType == "" {
EnforceSingleContentType(i)
} else {
EnforceSingleContentType(contentType)
}
if op.RequestBody.Value.Content.Get(i).Schema != nil {
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody, 0)
if strings.Contains(i, "json") {
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "x-www-form-urlencoded") {
var formData []string
for j := range s.Body {
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "xml") {
type Element struct {
XMLName xml.Name
Content any `xml:",chardata"`
}
}
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody)
if strings.Contains(i, "json") {
s.BodyData, _ = json.Marshal(s.Body)
}
} else if i == "application/x-www-form-urlencoded" {
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
Headers = append(Headers, "Content-Type: application/x-www-form-urlencoded")
}
var formData []string
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if valueType == "string" {
s.Body[j] = "test"
} else if valueType == "boolean" {
s.Body[j] = false
} else if valueType == "integer" || valueType == "number" {
s.Body[j] = 1
type Root struct {
XMLName xml.Name `xml:"root"`
Elements []Element `xml:",any"`
}
var elements []Element
for key, value := range s.Body {
elements = append(elements, Element{
XMLName: xml.Name{Local: key},
Content: value,
})
}
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
s.BodyData = xmlData
} else {
s.Body[j] = "unknown_type_populate_manually"
log.Warnf("Content type not supported. Test this path manually: %s (Content type: %s)\n", s.URL.Path, i)
}
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
}
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "json") {
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
Headers = append(Headers, "Content-Type: application/json")
}
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if valueType == "string" {
s.Body[j] = "test"
} else if valueType == "boolean" {
s.Body[j] = false
} else if valueType == "integer" || valueType == "number" {
s.Body[j] = 1
} else {
s.Body[j] = "unknown_type_populate_manually"
} else {
var formData []string
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref != "" {
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref, op.RequestBody, 0)
} else {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
if valueType == "string" {
s.Body[j] = "test"
} else if valueType == "boolean" {
s.Body[j] = false
} else if valueType == "integer" || valueType == "number" {
s.Body[j] = 1
} else {
s.Body[j] = "unknown_type_populate_manually"
}
if i == "application/x-www-form-urlencoded" {
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
}
if i == "application/x-www-form-urlencoded" {
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "json") || i == "*/*" {
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "xml") {
//
type Element struct {
XMLName xml.Name
Content any `xml:",chardata"`
}
type Root struct {
XMLName xml.Name `xml:"root"`
Elements []Element `xml:",any"`
}
var elements []Element
for key, value := range s.Body {
elements = append(elements, Element{
XMLName: xml.Name{Local: key},
Content: value,
})
}
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
s.BodyData = xmlData
} else {
s.Body["test"] = "test"
s.BodyData = []byte("test=test")
}
}
}
}
}
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "multipart/form-data") {
//TODO
s.Body["MULTIPART_FORM_DATA"] = "TEST_MANUALLY"
s.BodyData = []byte("MULTIPART_FORM_DATA=TEST_MANUALLY")
} else if strings.Contains(i, "xml") {
//TODO
s.Body["XML"] = "TEST_MANUALLY"
s.BodyData = []byte("XML=TEST_MANUALLY")
} else {
fmt.Println(i)
s.Body["test"] = "test"
s.BodyData = []byte("test=test")
}
}
}
@@ -406,78 +508,6 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
return s
}
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef) {
if param != nil {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Value.Type == "string" {
if location == "path" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else if location == "query" {
if schema.Value.Properties[property].Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
}
} else if location == "body" {
if schema.Value.Properties[property].Value.Type == "string" {
s.Body[param.Value.Name] = "test"
} else {
s.Body[param.Value.Name] = 1
}
}
}
} else if schema.Value.Enum != nil {
if location == "path" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
} else if location == "query" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Query.Add(param.Value.Name, fmt.Sprint(value))
} else if location == "body" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[param.Value.Name] = value
}
}
}
} else {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
s.SetParametersFromSchema(nil, "body", schema.Value.Properties[property].Ref, req)
} else {
if schema.Value.Properties[property].Value.Type == "string" {
s.Body[property] = "test"
} else {
s.Body[property] = 1
}
}
}
} else if schema.Value.Enum != nil {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[schema.Value.Title] = value
}
}
}
}
func (s SwaggerRequest) GetBasePath() string {
if basePath == "" {
if s.Def.Servers != nil {
@@ -512,17 +542,17 @@ func (s SwaggerRequest) GetBasePath() string {
}
}
bp, _ := s.Def.Servers.BasePath()
if bp != "" {
basePath = bp
}
}
basePath = strings.TrimSuffix(basePath, "/")
return basePath
}
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 strings.ToLower(outputFormat) == "console" {
if i.Title != "" {
fmt.Println("Title:", i.Title)
}
@@ -599,51 +629,6 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
}
}
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)
}
fmt.Fprintf(w, "]}\n")
}
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()
}
}
func ExtractSpecFromJS(bodyBytes []byte) []byte {
var openApiIndex int
var specClose int
@@ -692,25 +677,18 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
return bodyBytes
}
func CheckAndConfigureProxy() (client http.Client) {
var proxyUrl *url.URL
func EnforceSingleContentType(newContentType string) {
newContentType = strings.TrimSpace(newContentType)
transport := &http.Transport{}
// Remove old 'Content-Type' header
slices.DeleteFunc(Headers, func(h string) bool {
return strings.HasPrefix(strings.ToLower(h), "content-type:")
})
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
Headers = append(Headers, "Content-Type: "+newContentType)
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
// Remove empty elements to avoid repetitions of "-H ''"
Headers = slices.DeleteFunc(Headers, func(h string) bool {
return strings.TrimSpace(h) == ""
})
}
+1
View File
@@ -7,6 +7,7 @@ require (
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
golang.org/x/time v0.8.0
gopkg.in/yaml.v3 v3.0.1
)
+2
View File
@@ -56,6 +56,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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=