Compare commits

...
48 Commits
Author SHA1 Message Date
Tony West 90e9c78d9a Incremented version string 2026-07-09 11:30:07 -04:00
Tony West 3f99ffb48c Added docker support for releases 2026-07-09 11:29:51 -04:00
Tony West 331bb1a05c Incremented version string 2026-05-13 19:31:55 -04:00
Tony West 3c2a3b807d added fix to auto-extract JS-wrapped specs in automate/endpoints/prepare; fixed POST body leak, Content-Type carryover, and multipart boundary issues 2026-05-13 19:31:22 -04:00
Tony West 7ef8abdb8e Incremented version string 2026-05-13 14:47:17 -04:00
Tony West f1f9e19422 Improved detection for JS specs wrapped in swaggerDoc/spec/openapi 2026-05-13 14:46:47 -04:00
Tony West 0d6776b758 Incremented version string 2026-03-24 16:00:16 -04:00
Tony West 8f4e4a5d9f Fixes #55 2026-03-24 16:00:07 -04:00
Tony West 55baa79785 Incremented version string 2026-03-20 11:37:19 -04:00
Tony West 5bdafb64d8 Merge pull request #54 from bsysop/main
Adds new features from pull request
2026-03-20 11:35:58 -04:00
Guillermo Gregorio 27b59354fc Adds --replay-proxy flag to replay matched requests through a separate proxy 2026-03-19 22:04:42 -03:00
Guillermo Gregorio bee5088fcf Adds --force flag to bypass dangerous keyword checks 2026-03-13 14:56:15 -03:00
Tony West 1eda960182 Fixed bug where custom headers were not sent with the brute command 2026-03-13 00:13:54 -04:00
Tony West e805a7d190 Incremented version string 2026-02-27 22:11:10 -05:00
Tony West 18b20ad476 Merge pull request #52 from aringo-bf/feature/ref-resolution-hardening
Added several features/tests/bug fixes.
2026-02-27 22:10:01 -05:00
Tony West 5115b3cba3 Apply suggestions from code review 2026-02-27 22:06:13 -05:00
aringo-bf 942ad84c54 fix: reconcile merged ref-hardening changes with current logging helpers 2026-02-27 16:06:56 -06:00
aringo-bf 878f7dcd39 fix: normalize server base paths for v3 parsing 2026-02-27 16:05:18 -06:00
aringo-bf 275f426fc2 fix: extract path from absolute server URLs when -T flag is used
High priority fix:
- OpenAPI v3 specs with absolute server URLs (e.g., https://api.example.com/v1)
  now preserve the path component (/v1) when -T flag overrides the host
- Parse absolute server URL to extract path and set as basePath when apiTarget
  is already set by -T flag
- Impact: -T https://staging.example.com with spec servers[0].url =
  https://api.example.com/v1 now produces https://staging.example.com/v1/users
  (was https://staging.example.com/users)

Test coverage:
- Added TestOpenAPIv3AbsoluteServerURLWithTargetFlag to verify path extraction
- Test validates both basePath extraction and apiTarget preservation
- All 10 tests pass (9 PASS, 1 SKIP)
2026-02-27 16:04:42 -06:00
aringo-bf 6d027a66ce fix: preserve basePath with -T flag and allow endpoints with relative URLs
1. -T flag now preserves server path prefixes from spec
2. endpoints command works with relative server URLs
3. Enhanced tests to assert actual runtime behavior
2026-02-27 16:04:02 -06:00
aringo-bf 703039dc75 fix: JSON curl quoting bug and relative server URL handling
High priority fix:
1. Remove trailing double-quote from JSON curl generation
   - Changed -d '%s'" to -d '%s' on line 216
   - Fixes shell syntax errors in prepare output
   - Fixes malformed POST bodies in automate (was stripping " but leaving ')
   - Impact: JSON POST requests now valid shell and valid HTTP bodies

Medium priority fix:
2. Handle relative server URLs in local file mode
   - Detect when spec has relative URL (e.g., /api/v1) and no base available
   - Fail fast with clear error: "Use -T to specify target server"
   - Prevents silent generation of invalid URLs (://...)
   - Impact: Clear user guidance instead of skipped endpoints

Testing:
- Verified JSON curl has proper quoting: -d '{"key":"val"}'
- Verified bash syntax validation passes
- Added TestJSONCurlQuoting and TestRelativeServerURLHandling
- All 8 tests pass (7 PASS, 1 SKIP)
- Added test_relative_server.yaml fixture
2026-02-27 16:03:01 -06:00
aringo-bf e2a94378bf fix: resolve 4 critical reference resolution bugs
1. External parameter/requestBody refs preserve context for nested refs
2. Nested external refs resolve relative to parent file directory
3. Swagger v2 local files use proper URL schemes from spec
4. Added comprehensive Go test suite with 6 test functions
2026-02-27 16:02:55 -06:00
aringo-bf c9fa4f1257 fix: resolve all 5 auditor-reported bugs
Fixed external refs, nested refs, query object params, extra body fields, and default values
2026-02-27 16:02:51 -06:00
aringo-bf 90f0870bfb feat: comprehensive spec reference resolution improvements
Enhanced ResolveRef for Swagger v2 and OpenAPI v3, added external file references, implemented oneOf/anyOf/additionalProperties, refactored parameter handling, added test suite
2026-02-27 16:02:47 -06:00
Tony West 122d9d0114 Fixed sorting to ensure output is consistent across runs 2026-02-26 11:34:23 -05:00
Tony West ac50c4da75 Fixed typo in usage example 2026-02-26 11:14:19 -05:00
Tony West 6eea0f8a77 Improved output and removed logrus dependency 2026-02-26 11:10:04 -05:00
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
26 changed files with 2800 additions and 1365 deletions

No files matched your search

+18
View File
@@ -30,6 +30,24 @@ archives:
- goos: windows
format: zip
dockers_v2:
- images:
- "ghcr.io/bishopfox/sj"
dockerfile: Dockerfile
platforms:
- linux/amd64
- linux/arm64
tags:
- "{{ .Version }}"
- "latest"
labels:
org.opencontainers.image.title: "{{ .ProjectName }}"
org.opencontainers.image.description: "Swagger Jacker"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.source: "https://github.com/BishopFox/sj"
org.opencontainers.image.licenses: "MIT"
changelog:
disable: false
sort: asc
+7
View File
@@ -0,0 +1,7 @@
FROM gcr.io/distroless/static-debian12:nonroot
ARG TARGETOS
ARG TARGETARCH
COPY ${TARGETOS}/${TARGETARCH}/sj /usr/local/bin/sj
ENTRYPOINT ["/usr/local/bin/sj"]
+110 -96
View File
@@ -13,7 +13,7 @@ It does this by parsing the definition file for paths, parameters, and accepted
## Build
To compile from source, ensure you have Go version `>= 1.21` installed and run `go build` from within the repository:
To compile from source, ensure you have Go version `>= 1.22.5` installed and run `go build` from within the repository:
```bash
$ git clone https://github.com/BishopFox/sj.git
@@ -37,139 +37,153 @@ $ export PATH=$PATH:~/go/bin
> Use the `automate` command to send a series of requests to each defined endpoint and analyze the status code of each response.
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -q
INFO[0000] Gathering API details.
INFO[0000] Sending requests at a rate of 15 requests per second.
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
Gathering API details.
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
✓ GET 200 /v2/pet/findByStatus
✓ GET 200 /v2/user/logout
⚠ POST 400 /v2/user/createWithArray
⚠ POST 400 /v2/store/order
✗ GET 404 /v2/store/order/1
⚠ POST 400 /v2/pet
⚠ PUT 415 /v2/pet
⚠ POST 400 /v2/user/createWithList
✗ GET 404 /v2/user/bishopfox
⚠ PUT 415 /v2/user/bishopfox
⚠ POST 400 /v2/user
⚠ POST 415 /v2/pet/1/uploadImage
✓ GET 200 /v2/pet/findByTags
✗ GET 404 /v2/pet/1
⚠ POST 415 /v2/pet/1
✓ GET 200 /v2/store/inventory
✓ GET 200 /v2/user/login
```
WARN[0000] Results for https://petstore.swagger.io/v2:
You can use the `--replay-proxy` flag to replay matched requests through a separate proxy (e.g., Burp Suite). This lets you route all traffic through one proxy (or direct) while only sending interesting results to your interception proxy:
WARN[0007] Results for http://petstore.swagger.io/v2:
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi --replay-proxy http://127.0.0.1:8080
```
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
You can also combine it with `--proxy` to route scanning traffic through a different proxy while replaying matches to Burp:
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://proxy:9090 --replay-proxy http://127.0.0.1:8080
```
You can also request verbose output to see the partial (or full) response:
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080 -v
Gathering API details.
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
✗ GET 404 /v2/user/bishopfox
{"code":1,"type":"error","message":"User not found
⚠ PUT 415 /v2/user/bishopfox
{"code":415,"type":"unknown","message":"com.sun.je
✓ GET 200 /v2/user/logout
{"code":200,"type":"unknown","message":"ok"}
⚠ POST 400 /v2/user/createWithArray
{"code":400,"type":"unknown","message":"bad input"
⚠ POST 400 /v2/user/createWithList
{"code":400,"type":"unknown","message":"bad input"
✗ GET 404 /v2/pet/1
{"code":1,"type":"error","message":"Pet not found"
⚠ POST 415 /v2/pet/1
{"code":415,"type":"unknown"}
✓ GET 200 /v2/store/inventory
{"sold":117,"string":26,"invalidStatus":1,"-1":1,"
⚠ POST 400 /v2/store/order
{"code":400,"type":"unknown","message":"bad input"
✓ GET 200 /v2/user/login
{"code":200,"type":"unknown","message":"logged in
⚠ POST 400 /v2/pet
{"code":400,"type":"unknown","message":"bad input"
⚠ PUT 415 /v2/pet
{"code":415,"type":"unknown","message":"com.sun.je
✓ GET 200 /v2/pet/findByStatus
[]
✓ GET 200 /v2/pet/findByTags
[]
✗ GET 404 /v2/store/order/1
{"code":1,"type":"error","message":"Order not foun
⚠ POST 400 /v2/user
{"code":400,"type":"unknown","message":"bad input"
⚠ POST 415 /v2/pet/1/uploadImage
{"code":415,"type":"unknown"}
```
> Use the `prepare` command to prepare a list of commands for manual testing. Currently supports both `curl` and `sqlmap`. You will likely have to modify these slightly.
```bash
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -q
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
INFO[0000] Gathering API details.
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
curl -sk -X GET 'https://petstore.swagger.io/v2/store/inventory'
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'
$ curl -X POST "https://petstore.swagger.io/v2/pet/{petId}"
$ curl -X GET "https://petstore.swagger.io/v2/pet/{petId}"
$ curl -X GET "https://petstore.swagger.io/v2/store/inventory"
$ curl -X POST "https://petstore.swagger.io/v2/user/createWithList" -d 'body=1'
$ curl -X GET "https://petstore.swagger.io/v2/user/logout"
$ curl -X POST "https://petstore.swagger.io/v2/user/createWithArray" -d 'body=1'
$ curl -X GET "https://petstore.swagger.io/v2/pet/findByStatus"
$ curl -X GET "https://petstore.swagger.io/v2/pet/findByTags"
$ curl -X POST "https://petstore.swagger.io/v2/store/order" -d 'petId=1&quantity=1&shipDate=bishopfox&status=bishopfox&complete=1&id=1&body='
$ curl -X POST "https://petstore.swagger.io/v2/pet/{petId}/uploadImage"
$ curl -X POST "https://petstore.swagger.io/v2/pet" -d 'photoUrls=1&tags=1&status=bishopfox&id=1&category=&name=doggie&body='
$ curl -X PUT "https://petstore.swagger.io/v2/pet" -d 'id=1&category=&name=doggie&photoUrls=1&tags=1&status=bishopfox&body='
$ curl -X GET "https://petstore.swagger.io/v2/user/{username}"
$ curl -X PUT "https://petstore.swagger.io/v2/user/{username}" -d 'email=bishopfox&password=bishopfox&phone=bishopfox&userStatus=1&id=1&username=bishopfox&firstName=bishopfox&lastName=bishopfox&body='
$ curl -X GET "https://petstore.swagger.io/v2/user/login"
$ curl -X POST "https://petstore.swagger.io/v2/user" -d 'phone=bishopfox&userStatus=1&id=1&username=bishopfox&firstName=bishopfox&lastName=bishopfox&email=bishopfox&password=bishopfox&body='
$ curl -X GET "https://petstore.swagger.io/v2/store/order/{orderId}"
```
> Use the `endpoints` command to generate a list of raw endpoints from the provided definition file.
```bash
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
INFO[0000] Gathering endpoints.
/v2/store/inventory
/v2/store/order/{orderId}
/v2/pet
/v2/pet/findByStatus
/v2/pet
/v2/store/order
/v2/user/createWithList
/v2/pet/{petId}/uploadImage
/v2/pet/findByTags
/v2/pet/{petId}
/v2/pet/{petId}/uploadImage
/v2/store/inventory
/v2/store/order
/v2/store/order/{orderId}
/v2/user
/v2/pet/{petId}
/v2/user/{username}
/v2/user/{username}
/v2/user/createWithArray
/v2/user/createWithList
/v2/pet/findByStatus
/v2/user/login
/v2/user/logout
/v2/user/{username}
/v2/user
```
> Use the `brute` command to send a series of requests in an attempt to find a definition file on the target.
```bash
$ sj brute -u https://petstore.swagger.io
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..."}
$ 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 -o openapi.json
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080 -o openapi.json
INFO[0000] Gathering API details.
@@ -227,8 +241,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.
--replay-proxy string Replay matched requests using this proxy.
--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)
+106 -88
View File
@@ -4,10 +4,6 @@ import (
"encoding/base64"
"fmt"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
var (
@@ -21,96 +17,118 @@ var (
bearerToken string
)
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")
}
for mechanism := range doc3.Components.SecuritySchemes {
log.Infof(`Type: %s, Scheme: %s`, doc3.Components.SecuritySchemes[mechanism].Value.Type, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
}
log.Warnln("Note: To avoid these messages, pass the quiet (-q) flag.")
} else if !quiet {
if len(doc3.Components.SecuritySchemes) > 0 {
log.Info("Available authentication mechanisms: ")
}
for mechanism := range doc3.Components.SecuritySchemes {
if doc3.Components.SecuritySchemes[mechanism].Value == nil {
log.Error("Authorization schema structure unsupported. Manual testing is required.")
return false, "", ""
}
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme != "" {
fmt.Printf(" - %s (%s)\n", mechanism, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
} else if outputFormat != "json" {
fmt.Printf(" - %s\n", mechanism)
func CheckSecuritySchemes(spec map[string]interface{}) {
components, ok := spec["components"].(map[string]interface{})
if ok && components != nil {
securitySchemes, ok := components["securitySchemes"].(map[string]interface{})
if !ok || len(securitySchemes) == 0 {
fmt.Println("No security schemes defined.")
} else {
if outputFormat != "json" {
fmt.Println("Found security schemes:")
}
var apiKey string
var apiKeyName string
if doc3.Components.SecuritySchemes[mechanism].Value.Type == "http" {
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme == "basic" {
log.Infof("Basic Authentication is accepted. Supply a username and password? (y/N)")
fmt.Scanln(&autoApplyBasicAuth)
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
if autoApplyBasicAuth == "y" {
fmt.Printf("Enter a username.")
fmt.Scanln(&basicAuthUser)
fmt.Printf("Enter a password.")
fmt.Scanln(&basicAuthPass)
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
log.Infof("Using %s as the Basic Auth value.", basicAuthString)
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
} else {
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
}
} else if strings.ToLower(doc3.Components.SecuritySchemes[mechanism].Value.Scheme) == "bearer" {
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
for mechanism, value := range securitySchemes {
fmt.Printf(" - %s\n", mechanism)
scheme, ok := value.(map[string]interface{})
if !ok {
continue
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "query" {
apiInQuery = true
log.Infof("An API key can be provided via a parameter string. Would you like to apply one? (y/N)")
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
log.Infof("Using %s=%s as the API key in all requests.", apiKeyName, apiKey)
if typ, ok := scheme["type"].(string); ok {
switch typ {
case "http":
if schemeType := scheme["scheme"]; schemeType != nil {
switch schemeType {
case "basic":
if quiet {
autoApplyBasicAuth = "n"
printWarn("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)
printInfo("Using %s as the Basic Auth value.\n", basicAuthString)
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
} else {
printWarn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
}
}
case "bearer":
printWarn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
}
}
case "apiKey":
if inVal, ok := scheme["in"].(string); ok {
switch inVal {
case "query":
printInfo("An API key can be provided via a parameter string. Would you like to apply one? (y/N)\n")
if quiet {
autoApplyAPIKey = "n"
} else {
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)
printInfo("Using %s=%s as the API key in all requests.\n", apiKeyName, apiKey)
}
case "header":
if mechanism == "bearer" {
printInfo("A bearer token is accepted. Would you like to provide one? (y/N)\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 {
printWarn("A bearer token is accepted. Review the spec and craft a header manually using the -H flag.")
}
} else {
if nameVal, ok := scheme["name"].(string); ok {
printInfo("An API key can be provided via the header %s. Would you like to apply one? (y/N)\n", nameVal)
if quiet {
autoApplyAPIKey = "n"
} else {
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
}
if autoApplyAPIKey == "y" {
apiKeyName = nameVal
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
Headers = append(Headers, nameVal+": "+apiKey)
}
}
}
}
}
}
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "header" {
if mechanism == "bearer" {
log.Infof("A bearer token is accepted. Would you like to provide one? (y/N)")
fmt.Scanln(&autoApplyBearer)
autoApplyBearer = strings.ToLower(autoApplyBearer)
if autoApplyBearer == "y" {
fmt.Printf("What value would you like to use for the Bearer Token? ")
fmt.Scanln(&bearerToken)
Headers = append(Headers, "Authorization: Bearer "+bearerToken)
} else {
log.Warn("A bearer token is accepted. Review the spec and craft a header manually using the -H flag.")
}
} else {
log.Infof("An API key can be provided via the header %s. Would you like to apply one? (y/N)", doc3.Components.SecuritySchemes[mechanism].Value.Name)
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
Headers = append(Headers, doc3.Components.SecuritySchemes[mechanism].Value.Name+": "+apiKey)
}
if bearerFormat, ok := scheme["bearerFormat"].(string); ok {
fmt.Println(" - bearerFormat:", bearerFormat)
}
}
}
}
fmt.Printf("\n")
return apiInQuery, apiKey, apiKeyName
}
+27 -8
View File
@@ -4,16 +4,17 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
log "github.com/sirupsen/logrus"
)
var getAccessibleEndpoints bool
var outputFormat string
var responsePreviewLength int
var testString string
var verbose bool
var automateCmd = &cobra.Command{
@@ -25,29 +26,37 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
Run: func(cmd *cobra.Command, args []string) {
if outfile != "" && strings.ToLower(outputFormat) != "" {
if !strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) != "json" {
log.Fatal("Only the JSON output format is supported at the moment.")
die("Only the JSON output format is supported at the moment.")
} else if strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) == "console" {
outputFormat = "json"
}
}
/* // 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)
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
_, err := time.Parse("2006-01-02", customDate)
if err != nil {
fmt.Println("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
os.Exit(1)
}
var bodyBytes []byte
client := CheckAndConfigureProxy()
client, replayClient := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
log.Infof("Gathering API details.\n")
printInfo("Gathering API details.\n")
}
if swaggerURL != "" {
@@ -55,21 +64,31 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
} else {
specFile, err := os.Open(localFile)
if err != nil {
log.Fatal("Error opening file:", err)
die("Error opening file: %v", err)
}
// Set the base directory for resolving external refs
specBaseDir = filepath.Dir(localFile)
if specBaseDir == "." {
if absPath, err := filepath.Abs(localFile); err == nil {
specBaseDir = filepath.Dir(absPath)
}
}
bodyBytes, _ = io.ReadAll(specFile)
}
/* // NEED TO RE-IMPLEMENT RATE LIMIT
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
log.Info("Sending requests at a rate of ", rateLimit, " requests per second.")
}
GenerateRequests(bodyBytes, client)
*/
GenerateRequests(bodyBytes, client, replayClient)
},
}
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.")
+197 -64
View File
@@ -2,7 +2,6 @@ package cmd
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -11,20 +10,21 @@ import (
"path/filepath"
"regexp"
"strings"
"time"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cobra"
"golang.org/x/time/rate"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
var endpointOnly bool
var endpointWordlist string
var prefixDirs []string = []string{"", "/swagger", "/swagger/docs", "/swagger/latest", "/swagger/v1", "/swagger/v2", "/swagger/v3", "/swagger/static", "/swagger/ui", "/swagger-ui", "/api-docs", "/api-docs/v1", "/api-docs/v2", "/apidocs", "/api", "/api/v1", "/api/v2", "/api/v3", "/v1", "/v2", "/v3", "/doc", "/docs", "/docs/swagger", "/docs/swagger/v1", "/docs/swagger/v2", "/docs/swagger-ui", "/docs/swagger-ui/v1", "/docs/swagger-ui/v2", "/docs/v1", "/docs/v2", "/docs/v3", "/public", "/redoc"}
var jsonEndpoints []string = []string{"", "/index", "/swagger", "/swagger-ui", "/swagger-resources", "/swagger-config", "/openapi", "/api", "/api-docs", "/apidocs", "/v1", "/v2", "/v3", "/doc", "/docs", "/apispec", "/apispec_1", "/api-merged"}
var javascriptEndpoints []string = []string{"/swagger-ui-init", "/swagger-ui-bundle", "/swagger-ui-standalone-preset", "/swagger-ui", "/swagger-ui.min", "/swagger-ui-es-bundle-core", "/swagger-ui-es-bundle", "/swagger-ui-standalone-preset", "/swagger-ui-layout", "/swagger-ui-plugins"}
var priorityURLs []string = []string{"/swagger.json", "/openapi.json", "/api-docs", "/swagger", "/docs", "/api/swagger.json", "/api/openapi.json", "/api-docs/swagger.json", "/api/schema/", "/webjars/swagger-ui/index.html", "/API/swagger/ui/index", "/swagger/ui/index", "/v2/swagger.json", "/v2/openapi.json", "/v2/api-docs", "/v3/api-docs", "/v3/openapi.json", "/public/api-merged.json", "/analytics/v1/swagger", "/api.json", "/api/4.0/swagger.json", "/api/api-doc/openapi.json", "/api/api-doc/openapi.yaml", "/api/doc.json", "/api/docs.json", "/api/swagger", "/api/swagger/ui/index", "/api/v1/swagger", "/api/v2/api-docs", "/api/v2/openapi.json", "/api/v2/swagger.json", "/api/v3/api-docs", "/api/v3/apispec", "/api/workorder/openapi.json", "/apidocs", "/audiences/v1/swagger", "/audittrail/v1/swagger", "/certification/v1/swagger", "/citrixapi/store/swagger.json", "/conferencetool/v1/swagger", "/course/v1/swagger", "/dcl_swagger.yaml", "/doc/doc.json", "/doc/swagger.json", "/docs/swagger.json", "/docs/v1/swagger.json", "/ecommerce/v1/swagger", "/enrollment/v1/swagger", "/externalids/v1/swagger", "/impact/v1/swagger", "/learn/v1/swagger", "/learningplan/v1/swagger", "/manage/v1/swagger", "/management/info", "/marketplace/v1/swagger", "/messenger/v1/swagger", "/notifications/v1/swagger", "/openapi", "/openapi/spec.json", "/otj/v1/swagger", "/pages/v1/swagger", "/poweruser/v1/swagger", "/proctoring/v1/swagger", "/report/v1/swagger", "/swagger-ui/index.html", "/swagger-ui/openapi.json", "/swagger.yaml", "/swagger/0.1.0/swagger.json", "/swagger/doc.json", "/swagger/latest/swagger.json", "/swagger/swagger.json", "/swagger/test/swagger.json", "/swagger/ui/index.html", "/swagger/v1/openapiv2.json", "/swagger/v1/swagger.json", "/swagger/v2/swagger.json", "/swagger/v4/swagger.json", "/v1/openapi.json", "/v1/swagger", "/v1/swagger.json", "/swagger/docs/v1", "/swagger/docs/v1.json", "/Api/swagger/docs/v1", "/swagger/v1/swagger.json", "/api/api-docs/swagger.json", "/api/docs/", "/api/docs", "/swagger-ui"}
var bruteCmd = &cobra.Command{
Use: "brute",
@@ -32,141 +32,149 @@ 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")
die("Invalid rate supplied. Must be a positive number")
}
*/
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
var allURLs []string
u, err := url.Parse(swaggerURL)
if err != nil {
log.Warnf("Error parsing URL:%s\n", err)
printWarn("Error parsing URL: %s", err)
}
target := u.Scheme + "://" + u.Host
normalizedBasePath := normalizeBasePath(basePath)
if endpointWordlist == "" {
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "")...)
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/")...)
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, priorityURLs, "", true)...)
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "", false)...)
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, javascriptEndpoints, ".js", false)...)
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, ".json", false)...)
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "/", false)...)
} else {
endpointList, err := os.Open(endpointWordlist)
if err != nil {
log.Fatalf("failed to open file: %s", err)
die("failed to open file: %s", err)
}
defer endpointList.Close()
scanner := bufio.NewScanner(endpointList)
for scanner.Scan() {
endpoint := scanner.Text()
fullURL := target + endpoint
fullURL := target + normalizedBasePath + endpoint
allURLs = append(allURLs, fullURL)
}
if err := scanner.Err(); err != nil {
log.Fatalf("failed to read words from file: %s", err)
die("failed to read words from file: %s", err)
}
}
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
log.Infof("Sending %d requests at a rate of %d requests per second. This could take a while...\n", len(allURLs), rateLimit)
printInfo("Sending %d requests at a rate of %d requests per second. This could take a while...\n", len(allURLs), rateLimit)
} else {
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
printInfo("Sending %d requests. This could take a while...\n", len(allURLs))
}
specFound, definitionFile := findDefinitionFile(allURLs, client)
if specFound {
definedOperations, err := json.Marshal(definitionFile)
if err != nil {
log.Errorf("Error parsing definition file:%s\n", err)
printErr("Error parsing definition file: %s", err)
}
if outfile != "" {
file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Errorf("Error opening file: %s\n", err)
printErr("Error opening file: %s", err)
}
defer file.Close()
_, err = file.Write(definedOperations)
if err != nil {
log.Errorf("Error writing file: %s\n", err)
printErr("Error writing file: %s", err)
} else {
f, _ := filepath.Abs(outfile)
log.Infof("Wrote file to %s\n", f)
printInfo("Wrote file to %s\n", f)
}
} else {
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)
printErr("\nNo definition file found for:\t%s", swaggerURL)
}
},
}
func makeURLs(target string, endpoints []string, fileExtension string) []string {
func makeURLs(target string, basePath string, endpoints []string, fileExtension string, skipPrefix bool) []string {
urls := []string{}
for _, dir := range prefixDirs {
if !skipPrefix {
for _, dir := range prefixDirs {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
continue
}
targetURL := target + basePath + dir + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
} else {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
if endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
targetURL := target + basePath + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
return urls
}
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 {
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)
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("")
printInfo("Definition file found: %s\n", url)
return true, checkSpec
}
}
} else if strings.Contains(ct, "application/javascript") {
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
if bodyBytes != nil {
if jsonContent, ok := ExtractJSONFromJSSpec(bodyBytes); ok {
checkSpec := UnmarshalSpec(jsonContent)
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
printInfo("\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
@@ -175,4 +183,129 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
func init() {
// TODO: Add a flag here (boolean) that defaults to false that will cause the program to execute 'sj automate' on the discovered definition file automatically.
bruteCmd.PersistentFlags().StringVarP(&endpointWordlist, "wordlist", "w", "", "The file containing a list of paths to brute force for discovery.")
bruteCmd.Flags().BoolVarP(&endpointOnly, "endpoint-only", "e", false, "Only return the identified endpoint")
}
// ExtractJSONFromJSSpec scans a Swagger-UI-style JavaScript bundle for an
// OpenAPI/Swagger document assigned to a top-level var/let/const. If the
// captured value is itself a wrapper object (swagger-ui-express / swagger-ui
// bundle config), it unwraps one level via the swaggerDoc/spec/openapi keys.
// Returns the extracted JSON bytes and true on success, or the original
// bytes and false on failure.
func ExtractJSONFromJSSpec(bodyBytes []byte) ([]byte, bool) {
re := regexp.MustCompile(`(?s)(?:let|const|var)\s+(\w+)\s*=\s*({.*?});`)
matches := re.FindAllStringSubmatch(string(bodyBytes), -1)
for _, m := range matches {
if len(m) < 3 {
continue
}
candidate := []byte(m[2])
if looksLikeAPISpec(candidate) {
return candidate, true
}
var wrapper map[string]json.RawMessage
if err := json.Unmarshal(candidate, &wrapper); err != nil {
continue
}
for _, key := range []string{"swaggerDoc", "spec", "openapi"} {
if inner, ok := wrapper[key]; ok && looksLikeAPISpec(inner) {
return inner, true
}
}
}
return bodyBytes, false
}
func looksLikeAPISpec(b []byte) bool {
var probe struct {
OpenAPI string `json:"openapi"`
Swagger string `json:"swagger"`
}
if err := json.Unmarshal(b, &probe); err != nil {
return false
}
return strings.HasPrefix(probe.OpenAPI, "3") ||
strings.HasPrefix(probe.OpenAPI, "2") ||
strings.HasPrefix(probe.Swagger, "2")
}
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") {
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
}
} else if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"openapi":"3`) {
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") {
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
}
} else {
printErr("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 {
die("Error parsing definition file.")
return nil
}
}
+15 -15
View File
@@ -11,7 +11,6 @@ import (
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@@ -26,15 +25,15 @@ var convertCmd = &cobra.Command{
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
log.Infof("Gathering API details.\n\n")
printInfo("Gathering API details.\n\n")
}
if swaggerURL != "" {
@@ -42,7 +41,7 @@ var convertCmd = &cobra.Command{
} else {
specFile, err := os.Open(localFile)
if err != nil {
log.Fatal("Error opening definition file:", err)
die("Error opening definition file: %v", err)
}
bodyBytes, _ = io.ReadAll(specFile)
@@ -61,7 +60,7 @@ var convertCmd = &cobra.Command{
}
if strings.HasPrefix(doc3.OpenAPI, "3") {
log.Warnln("Definition file is already version 3.")
printWarn("Definition file is already version 3.")
WriteConvertedDefinitionFile(bodyBytes)
} else if strings.HasPrefix(doc.Swagger, "2") {
newDoc, err := openapi2conv.ToV3(&doc)
@@ -69,13 +68,14 @@ var convertCmd = &cobra.Command{
fmt.Printf("Error converting v2 document to v3: %s\n", err)
}
if format == "json" {
switch format {
case "json":
if strings.HasSuffix(outfile, "yaml") || strings.HasSuffix(outfile, "yml") {
log.Warn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
printWarn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
}
converted, err := json.Marshal(newDoc)
if err != nil {
log.Fatal("Error converting definition file to v3:", err)
die("Error converting definition file to v3: %v", err)
}
if !strings.HasSuffix(string(converted), "}") {
if outfile == "" {
@@ -91,10 +91,10 @@ var convertCmd = &cobra.Command{
WriteConvertedDefinitionFile(converted)
}
}
} else if format == "yaml" || format == "yml" {
case "yaml", "yml":
converted, err := yaml.Marshal(newDoc)
if err != nil {
log.Fatal("Error converting definition file to v3:", err)
die("Error converting definition file to v3: %v", err)
}
if outfile == "" {
fmt.Println(string(converted))
@@ -104,7 +104,7 @@ var convertCmd = &cobra.Command{
}
} else {
log.Fatal("Error parsing definition file.")
die("Error parsing definition file.")
}
},
}
@@ -112,16 +112,16 @@ var convertCmd = &cobra.Command{
func WriteConvertedDefinitionFile(data []byte) {
file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Errorf("Error opening file: %s\n", err)
printErr("Error opening file: %s", err)
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
log.Errorf("Error writing file: %s\n", err)
printErr("Error writing file: %s", err)
} else {
f, _ := filepath.Abs(outfile)
log.Infof("Wrote file to %s\n", f)
printInfo("Wrote file to %s\n", f)
}
}
+15 -14
View File
@@ -4,9 +4,8 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/mpvl/unique"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -18,32 +17,34 @@ This list contains the raw endpoints (parameter values will not be appended or m
Run: func(cmd *cobra.Command, args []string) {
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
var bodyBytes []byte
var paths []string
fmt.Printf("\n")
log.Infof("Gathering endpoints.\n\n")
printInfo("Gathering endpoints.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
paths = GenerateRequests(bodyBytes, client)
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
GenerateRequests(bodyBytes, client, nil)
} else {
specFile, err := os.Open(localFile)
if err != nil {
log.Fatal("Error opening file:", err)
die("Error opening file: %v", err)
}
// Set the base directory for resolving external refs
specBaseDir = filepath.Dir(localFile)
if specBaseDir == "." {
if absPath, err := filepath.Abs(localFile); err == nil {
specBaseDir = filepath.Dir(absPath)
}
}
bodyBytes, _ = io.ReadAll(specFile)
paths = GenerateRequests(bodyBytes, client)
}
unique.Sort(unique.StringSlice{&paths})
for _, v := range paths {
fmt.Println(v)
GenerateRequests(bodyBytes, client, nil)
}
},
}
+105 -180
View File
@@ -1,11 +1,12 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/fatih/color"
)
type Result struct {
@@ -22,11 +23,35 @@ type VerboseResult struct {
Curl string `json:"curl"`
}
var tempLogger *log.Logger
// Diagnostic helpers — all write to stderr so stdout stays clean for piping.
var green = color.New(color.FgGreen, color.Bold).SprintFunc()
var yellow = color.New(color.FgYellow, color.Bold).SprintFunc()
var red = color.New(color.FgRed, color.Bold).SprintFunc()
var faint = color.New(color.Faint).SprintFunc()
func printInfo(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
}
func printWarn(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s %s\n", yellow("[!]"), msg)
}
func printErr(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s %s\n", red("[✗]"), msg)
}
func die(format string, args ...interface{}) {
printErr(format, args...)
os.Exit(1)
}
// writeLog is the main dispatch function for endpoint results.
func writeLog(sc int, target, method, errorMsg, response string) {
var file *os.File
tempLogger = log.New()
var out io.Writer = os.Stdout
tempResponsePreviewLength := responsePreviewLength
if len(response) < responsePreviewLength {
@@ -36,200 +61,100 @@ func writeLog(sc int, target, method, errorMsg, response string) {
if outfile != "" {
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
fmt.Println("Output file does not exist or cannot be created")
fmt.Fprintln(os.Stderr, "Output file does not exist or cannot be created")
os.Exit(1)
}
tempLogger.SetOutput(file)
}
if outputFormat == "console" {
if strings.Contains(response, "\"") {
tempLogger.SetFormatter(&log.TextFormatter{DisableQuote: true, DisableTimestamp: true})
} else {
tempLogger.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
}
} else {
tempLogger.SetFormatter(&log.JSONFormatter{DisableHTMLEscape: true, DisableTimestamp: true, PrettyPrint: true})
defer file.Close()
out = file
// Disable color when writing to a file.
color.NoColor = true
defer func() { color.NoColor = false }()
}
preview := ""
if verbose {
if sc != 200 {
if sc == 401 || sc == 403 {
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
} else if sc == 301 || sc == 302 {
logVerboseRedirect(sc, target, method, response, tempLogger)
} else if sc == 0 {
logVerboseBad(sc, target, method, response, tempLogger)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg, tempLogger)
} else if sc == 1 {
logDangerous(target, method, tempLogger)
} else if sc == 8899 {
logVerboseJSON(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)
}
}
preview = response[:responsePreviewLength]
}
switch sc {
case 8899:
if verbose {
logVerboseJSON(specTitle, specDescription, out)
} else {
logJSON(specTitle, specDescription, out)
}
default:
logResult(sc, target, method, errorMsg, preview, out)
}
responsePreviewLength = tempResponsePreviewLength
file.Close()
}
func logAccessible(status int, target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Print("Endpoint accessible!")
}
// logResult renders a single endpoint result line.
func logResult(sc int, target, method, errorMsg, preview string, out io.Writer) {
var sym string
var painter func(a ...interface{}) string
func logVerboseAccessible(status int, target, method, response string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Print("Endpoint accessible!")
}
func logDangerous(target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": "skipped",
"Target": target,
"Method": method,
}).Warn("Endpoint skipped due to dangerous keyword (or request cancelled due to timeout).")
}
func logManual(status int, target, method, errorMsg string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Manual testing may be required."
switch sc {
case 200:
sym = "✓"
painter = green
case 301, 302, 0, 1:
sym = "⚠"
painter = yellow
case 401, 403, 404:
sym = "✗"
painter = red
default:
sym = "⚠"
painter = yellow
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Warn(errorMsg)
}
func logVerboseManual(status int, target, method, errorMsg, response string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Manual testing may be required."
statusStr := fmt.Sprintf("%d", sc)
switch sc {
case 0:
statusStr = "N/A"
case 1:
statusStr = "---"
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Warn(errorMsg)
}
func logNotFound(status int, target, method, errorMsg string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Endpoint not found."
line := fmt.Sprintf("%s %-7s %-3s %s\n",
painter(sym),
painter(method),
painter(statusStr),
target,
)
fmt.Fprint(out, line)
if preview != "" {
fmt.Fprintf(out, " %s\n", faint(preview))
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Error(errorMsg)
}
func logRedirect(status int, target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Error("Redirect detected. This likely requires authentication.")
}
func logVerboseRedirect(status int, target, method, response string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[0:responsePreviewLength],
}).Error("Redirect detected. This likely requires authentication.")
}
func logBad(status int, target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": "N/A",
"Target": target,
"Method": method,
}).Warn("Bad request (could not reach the target).")
}
func logVerboseBad(status int, target, method, response string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": "N/A",
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Warn("Bad request (could not reach the target).")
}
func logUnauth(status int, target, method, errorMsg string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Unauthorized."
func logJSON(title, description string, out io.Writer) {
output := struct {
APITitle string `json:"apiTitle"`
Description string `json:"description"`
Results []Result `json:"results"`
}{
APITitle: title,
Description: description,
Results: jsonResultArray,
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Error(errorMsg)
data, _ := json.MarshalIndent(output, "", " ")
fmt.Fprintln(out, string(data))
}
func logVerboseUnauth(status int, target, method, errorMsg, response string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Unauthorized."
func logVerboseJSON(title, description string, out io.Writer) {
output := struct {
APITitle string `json:"apiTitle"`
Description string `json:"description"`
Results []VerboseResult `json:"results"`
}{
APITitle: title,
Description: description,
Results: jsonVerboseResultArray,
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Error(errorMsg)
}
func logJSON(title, description string) {
tempLogger.WithFields(log.Fields{
"apiTitle": title,
"description": description,
"results": jsonResultArray,
}).Println("Done")
}
func logVerboseJSON(title, description string) {
tempLogger.WithFields(log.Fields{
"apiTitle": title,
"description": description,
"results": jsonVerboseResultArray,
}).Println("Done")
data, _ := json.MarshalIndent(output, "", " ")
fmt.Fprintln(out, string(data))
}
+21 -35
View File
@@ -4,9 +4,9 @@ import (
"fmt"
"io"
"os"
"strings"
"path/filepath"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -19,57 +19,43 @@ This enables you to test specific API functions for common vulnerabilities or mi
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
printWarn("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
}
}
client := CheckAndConfigureProxy()
_, err := time.Parse("2006-01-02", customDate)
if err != nil {
fmt.Println("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
os.Exit(1)
}
client, _ := CheckAndConfigureProxy()
fmt.Printf("\n")
log.Infof("Gathering API details.\n\n")
printInfo("Gathering API details.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
GenerateRequests(bodyBytes, client)
GenerateRequests(bodyBytes, client, nil)
} else {
specFile, err := os.Open(localFile)
if err != nil {
log.Fatal("Error opening file:", err)
die("Error opening file: %v", err)
}
// Set the base directory for resolving external refs
specBaseDir = filepath.Dir(localFile)
if specBaseDir == "." {
if absPath, err := filepath.Abs(localFile); err == nil {
specBaseDir = filepath.Dir(absPath)
}
}
specBytes, _ := io.ReadAll(specFile)
GenerateRequests(specBytes, client)
GenerateRequests(specBytes, client, nil)
}
},
}
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.")
}
+128 -37
View File
@@ -11,8 +11,6 @@ import (
"os"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
var (
@@ -20,6 +18,7 @@ var (
avoidDangerousRequests string
contentType string
dangerousStrings []string = []string{"add", "block", "build", "buy", "change", "clear", "create", "delete", "deploy", "destroy", "drop", "edit", "emergency", "erase", "execute", "insert", "modify", "order", "overwrite", "pause", "purchase", "rebuild", "remove", "replace", "reset", "restart", "revoke", "run", "sell", "send", "set", "start", "stop", "update", "upload", "write"}
depth int = 0
Headers []string
requestStatus int
riskSurveyed bool = false
@@ -59,20 +58,24 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
}
// Handling of dangerous keywords
u, _ := url.Parse(target)
u, err := url.Parse(target)
if err != nil || u == nil {
printWarn("Error parsing URL '%s': %v - skipping request.", target, err)
return nil, "", 0
}
endpoint := u.RawPath + "?" + u.RawQuery
for _, v := range dangerousStrings {
if os.Args[1] == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
if os.Args[1] == "automate" && !force && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
userChoice = ""
if avoidDangerousRequests == "y" {
return nil, "", 0
} else {
log.Warnf("Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
printWarn("Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
fmt.Scanln(&userChoice)
if strings.ToLower(userChoice) != "y" {
if !riskSurveyed {
avoidDangerousRequests = "y"
log.Warnf("Do you want to avoid all dangerous requests? (Y/n)")
printWarn("Do you want to avoid all dangerous requests? (Y/n)")
fmt.Scanln(&avoidDangerousRequests)
avoidDangerousRequests = strings.ToLower(avoidDangerousRequests)
riskSurveyed = true
@@ -87,33 +90,33 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
defer cancel()
req, err := http.NewRequest(method, target, reqData)
if err != nil && err != context.Canceled && err != io.EOF {
log.Fatal("Error: could not create HTTP request - ", err)
if err != nil {
if err != context.Canceled && err != io.EOF {
die("Error: could not create HTTP request - %v", err)
}
return nil, "", 0
}
for i := range Headers {
delimIndex := strings.Index(Headers[i], ":")
if delimIndex == -1 {
log.Warnf("Header provided (%s) cannot be used. Headers must be in 'Key: Value' format (this may be caused by a header declared within the definition file).\n", Headers[i])
printWarn("Header provided (%s) cannot be used. Headers must be in 'Key: Value' format (this may be caused by a header declared within the definition file).", Headers[i])
continue
}
key := Headers[i][:delimIndex]
value := Headers[i][delimIndex+1:]
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
if len(Headers) == 2 {
if key == "User-Agent" {
UserAgent = value
}
if key == "Content-Type" {
contentType = value
}
if key == "Accept" {
accept = value
}
} else {
req.Header.Set(key, value)
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
@@ -141,17 +144,17 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
resp, err := client.Do(req.WithContext(ctx))
if err == context.DeadlineExceeded {
log.Printf("Error: %s - skipping request.", err)
printWarn("Error: %s - skipping request.", err)
return nil, "", 0
} else if err != nil && err != context.Canceled && err != io.EOF {
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
log.Fatal("Try supplying the --insecure flag.")
die("Try supplying the --insecure flag.")
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
die("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
} else if strings.Contains(fmt.Sprint(err), "user canceled") {
return nil, "skipped", 1
} else {
log.Error("Error: response not received.\n", err)
printErr("Error: response not received.\n%v", err)
}
return nil, "", 0
}
@@ -159,26 +162,48 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") {
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") && depth < 10 {
depth += 1
redirect, _ := resp.Location()
bodyBytes, bodyString, requestStatus = MakeRequest(client, method, redirect.Scheme+"://"+redirect.Host+redirect.Path, timeout, reqData)
return bodyBytes, bodyString, requestStatus
}
requestStatus = resp.StatusCode
depth = 0
return bodyBytes, bodyString, requestStatus
}
func CheckContentType(client http.Client, target string) string {
u, _ := url.Parse(target)
u, err := url.Parse(target)
if err != nil || u == nil {
printWarn("Error parsing URL '%s': %v - skipping request.", target, err)
return ""
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
req, err := http.NewRequest("GET", target, nil)
if err != nil && err != context.Canceled && err != io.EOF {
log.Fatal("Error: could not create HTTP request - ", err)
if err != nil {
if err != context.Canceled && err != io.EOF {
die("Error: could not create HTTP request - %v", err)
}
return ""
}
for i := range Headers {
delimIndex := strings.Index(Headers[i], ":")
if delimIndex == -1 {
continue
}
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
if key == "User-Agent" {
UserAgent = value
}
req.Header.Set(key, value)
}
// User-Agent handling
@@ -192,22 +217,22 @@ func CheckContentType(client http.Client, target string) string {
resp, err := client.Do(req.WithContext(ctx))
if err == context.DeadlineExceeded {
log.Printf("Error: %s - skipping request.", err)
printWarn("Error: %s - skipping request.", err)
return ""
} else if err != nil && err != context.Canceled && err != io.EOF {
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
log.Fatal("Try supplying the --insecure flag.")
die("Try supplying the --insecure flag.")
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
die("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
} else {
log.Error("Error: response not received.\n", err)
printErr("Error: response not received.\n%v", err)
}
return ""
}
return resp.Header.Get("Content-Type")
}
func CheckAndConfigureProxy() (client http.Client) {
func CheckAndConfigureProxy() (client http.Client, replayClient *http.Client) {
var proxyUrl *url.URL
transport := &http.Transport{}
@@ -227,5 +252,71 @@ func CheckAndConfigureProxy() (client http.Client) {
return http.ErrUseLastResponse
},
}
return client
if replayProxy != "" {
replayTransport := &http.Transport{}
if insecure {
replayTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
replayProxyUrl, err := url.Parse(replayProxy)
if err != nil {
die("Error parsing replay proxy URL: %v", err)
}
replayTransport.Proxy = http.ProxyURL(replayProxyUrl)
replayClient = &http.Client{
Transport: replayTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
return client, replayClient
}
func ReplayRequest(replayClient *http.Client, method, target string, timeout int64, reqData io.Reader) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
req, err := http.NewRequest(method, target, reqData)
if err != nil {
printWarn("Replay: error creating request for %s - %v", target, err)
return
}
for i := range Headers {
delimIndex := strings.Index(Headers[i], ":")
if delimIndex == -1 {
continue
}
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
req.Header.Set(key, value)
}
req.Header.Set("User-Agent", UserAgent)
if accept == "" {
req.Header.Set("Accept", "application/json, text/html, */*")
} else {
req.Header.Set("Accept", accept)
}
if method == "POST" {
if contentType == "" {
req.Header.Set("Content-Type", "application/json")
} else {
req.Header.Set("Content-Type", contentType)
}
}
resp, err := replayClient.Do(req.WithContext(ctx))
if err != nil {
printWarn("Replay: error sending request to %s - %v", target, err)
return
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
fmt.Fprintf(os.Stderr, "[Replay] %s %s -> %d\n", method, target, resp.StatusCode)
}
+13 -4
View File
@@ -1,17 +1,21 @@
package cmd
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var apiTarget string
var basePath string
var customDate string
var customEmail string
var customURL string
var force bool
var format string
var insecure bool
var localFile string
var outfile string
var proxy string
var replayProxy string
var quiet bool
var randomUserAgent bool
var rateLimit int
@@ -46,10 +50,10 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
log.Error("Command not specified. See the --help flag for usage.")
printErr("Command not specified. See the --help flag for usage.")
}
},
Version: "1.10.3",
Version: "2.7.0",
}
func Execute() {
@@ -64,15 +68,20 @@ func init() {
rootCmd.AddCommand(convertCmd)
rootCmd.PersistentFlags().StringVarP(&UserAgent, "agent", "A", "Swagger Jacker (github.com/BishopFox/sj)", "Set the User-Agent string.")
rootCmd.PersistentFlags().StringVarP(&basePath, "base-path", "b", "", "Set the API base path if not defined in the definition file (i.e. /V2/).")
rootCmd.PersistentFlags().StringVarP(&customURL, "custom-url", "c", "https://bishopfox.com", "Set a custom URL to test discovered URL parameters.")
rootCmd.PersistentFlags().StringVarP(&customDate, "custom-date", "d", "1990-01-01", "A custom date to test discovered date parameters.")
rootCmd.PersistentFlags().StringVar(&customEmail, "custom-email", "[email protected]", "A custom email address to test discovered email parameters.")
rootCmd.PersistentFlags().BoolVar(&force, "force", false, "Send requests without prompting, even if dangerous keywords are detected.")
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().StringVar(&replayProxy, "replay-proxy", "", "Replay matched requests using this proxy.")
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Do not prompt for user input - uses default values for all requests.")
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().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.")
-175
View File
@@ -1,175 +0,0 @@
package cmd
import (
"fmt"
"math/rand"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
type SchemaReference struct {
Required []string
Type string
Properties struct {
Property struct {
Type string
Format string
Example string
Reference string
XML struct {
Name string
Namespace string
Prefix string
Attribute bool
Wrapped bool
}
}
}
}
func HandleSchemaReference(schema *openapi3.SchemaRef) {
}
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
if param != nil {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else if location == "path" {
if schema.Value.Properties[property].Value.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
}
+833 -593
View File
File diff suppressed because it is too large. Load diff
+723
View File
@@ -0,0 +1,723 @@
package cmd
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
)
func TestSwaggerV2SchemeHandling(t *testing.T) {
oldSwaggerURL := swaggerURL
oldAPITarget := apiTarget
oldSpecBaseDir := specBaseDir
defer func() {
swaggerURL = oldSwaggerURL
apiTarget = oldAPITarget
specBaseDir = oldSpecBaseDir
}()
specPath := filepath.Join("..", "tests", "test_spec_v2.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
swaggerURL = ""
apiTarget = ""
if schemes, ok := spec["schemes"].([]interface{}); ok && len(schemes) > 0 {
if scheme, ok := schemes[0].(string); ok {
if scheme != "https" && scheme != "http" {
t.Errorf("Expected http or https scheme, got: %s", scheme)
}
}
}
if host, ok := spec["host"].(string); !ok || host == "" {
t.Error("Expected host field in Swagger v2 spec")
}
}
func TestExternalReferenceResolution(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldCache := externalRefCache
defer func() {
specBaseDir = oldSpecBaseDir
externalRefCache = oldCache
}()
externalRefCache = make(map[string]map[string]interface{})
specPath := filepath.Join("..", "tests", "test_spec_external.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
ref := "external_schemas.yaml#/components/schemas/Customer"
resolved := ResolveExternalRef(ref, specBaseDir)
if resolved == nil {
t.Fatal("Failed to resolve external reference")
}
if resolvedType, ok := resolved["type"].(string); !ok || resolvedType != "object" {
t.Error("Expected Customer schema to be an object")
}
if props, ok := resolved["properties"].(map[string]interface{}); ok {
if _, hasCustomerId := props["customerId"]; !hasCustomerId {
t.Error("Expected Customer schema to have 'customerId' property")
}
if _, hasContact := props["contactInfo"]; !hasContact {
t.Error("Expected Customer schema to have 'contactInfo' property")
}
} else {
t.Error("Expected Customer schema to have properties")
}
}
func TestNestedExternalReferences(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldCache := externalRefCache
defer func() {
specBaseDir = oldSpecBaseDir
externalRefCache = oldCache
}()
externalRefCache = make(map[string]map[string]interface{})
schemaPath := filepath.Join("..", "tests", "external_schemas.yaml")
absPath, _ := filepath.Abs(schemaPath)
data, err := os.ReadFile(schemaPath)
if err != nil {
t.Skipf("Test schema file not found: %v", err)
return
}
externalSpec := SafelyUnmarshalSpec(data)
if externalSpec == nil {
t.Fatal("Failed to unmarshal external schema")
}
externalRefCache[absPath] = externalSpec
customerSchema, ok := externalSpec["components"].(map[string]interface{})
if !ok {
t.Fatal("Expected components in external schema")
}
schemas, ok := customerSchema["schemas"].(map[string]interface{})
if !ok {
t.Fatal("Expected schemas in components")
}
customer, ok := schemas["Customer"].(map[string]interface{})
if !ok {
t.Fatal("Expected Customer definition")
}
visited := make(map[string]bool)
expanded := ExpandSchema(externalSpec, customer, visited, externalSpec)
if expanded == nil {
t.Fatal("Failed to expand Customer schema")
}
if contactInfo, ok := expanded.Properties["contactInfo"]; ok {
if len(contactInfo.Properties) == 0 {
t.Error("Expected contactInfo to have expanded properties (email, phone)")
}
} else {
t.Error("Expected Customer to have contactInfo property")
}
}
func TestQueryObjectParameterHandling(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldCache := externalRefCache
defer func() {
specBaseDir = oldSpecBaseDir
externalRefCache = oldCache
}()
externalRefCache = make(map[string]map[string]interface{})
specPath := filepath.Join("..", "tests", "test_query_object.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
paths, ok := spec["paths"].(map[string]interface{})
if !ok {
t.Fatal("Expected paths in spec")
}
searchPath, ok := paths["/search"].(map[string]interface{})
if !ok {
t.Fatal("Expected /search path")
}
getOp, ok := searchPath["get"].(map[string]interface{})
if !ok {
t.Fatal("Expected GET operation")
}
params, ok := getOp["parameters"].([]interface{})
if !ok || len(params) == 0 {
t.Fatal("Expected parameters in GET /search")
}
param0, ok := params[0].(map[string]interface{})
if !ok {
t.Fatal("Expected first parameter to be a map")
}
if in, ok := param0["in"].(string); !ok || in != "query" {
t.Error("Expected parameter to be in query")
}
if schema, ok := param0["schema"].(map[string]interface{}); !ok {
t.Error("Expected parameter to have schema")
} else {
if schemaType, ok := schema["type"].(string); !ok || schemaType != "object" {
t.Error("Expected parameter schema to be object type")
}
}
}
func TestRequestBodyContextPreservation(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldCache := externalRefCache
defer func() {
specBaseDir = oldSpecBaseDir
externalRefCache = oldCache
}()
externalRefCache = make(map[string]map[string]interface{})
specPath := filepath.Join("..", "tests", "test_spec_v3.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
paths, ok := spec["paths"].(map[string]interface{})
if !ok {
t.Fatal("Expected paths in spec")
}
foundTest := false
for _, pathItem := range paths {
if pathMap, ok := pathItem.(map[string]interface{}); ok {
for method, op := range pathMap {
if strings.ToLower(method) == "post" || strings.ToLower(method) == "put" {
if opMap, ok := op.(map[string]interface{}); ok {
if reqBody, ok := opMap["requestBody"].(map[string]interface{}); ok {
if ref, hasRef := reqBody["$ref"].(string); hasRef {
resolved, contextSpec := ResolveRefWithContext(spec, ref)
if resolved == nil {
t.Errorf("Failed to resolve requestBody ref: %s", ref)
}
if contextSpec == nil {
t.Error("Expected context spec to be returned")
}
foundTest = true
}
}
}
}
}
}
}
if !foundTest {
t.Skip("No requestBody refs found in spec")
}
}
func TestDefaultValueHandling(t *testing.T) {
oldSpecBaseDir := specBaseDir
defer func() {
specBaseDir = oldSpecBaseDir
}()
specPath := filepath.Join("..", "tests", "test_spec_v2.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
paths, ok := spec["paths"].(map[string]interface{})
if !ok {
t.Fatal("Expected paths in spec")
}
usersPath, ok := paths["/users"].(map[string]interface{})
if !ok {
t.Skip("No /users path in spec")
}
getOp, ok := usersPath["get"].(map[string]interface{})
if !ok {
t.Skip("No GET operation on /users")
}
params, ok := getOp["parameters"].([]interface{})
if !ok {
t.Skip("No parameters on GET /users")
}
foundDefault := false
for _, p := range params {
if pMap, ok := p.(map[string]interface{}); ok {
if defaultVal := pMap["default"]; defaultVal != nil {
foundDefault = true
if name, ok := pMap["name"].(string); ok {
if name == "limit" {
if defaultInt, ok := defaultVal.(int); ok && defaultInt != 10 {
t.Errorf("Expected default value of 10 for limit, got %d", defaultInt)
}
}
}
}
}
}
if !foundDefault {
t.Skip("No parameters with default values found")
}
}
func TestJSONCurlQuoting(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldSwaggerURL := swaggerURL
oldAPITarget := apiTarget
oldBasePath := basePath
defer func() {
specBaseDir = oldSpecBaseDir
swaggerURL = oldSwaggerURL
apiTarget = oldAPITarget
basePath = oldBasePath
}()
specPath := filepath.Join("..", "tests", "test_spec_v3.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
// Set up for local file mode
swaggerURL = ""
apiTarget = ""
basePath = ""
// Simulate GenerateRequests parsing
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
if srv, ok := servers[0].(map[string]interface{}); ok {
if serverURL, ok := srv["url"].(string); ok {
if strings.Contains(serverURL, "://") {
apiTarget = serverURL
}
}
}
}
// Find a POST endpoint with JSON request body
paths, ok := spec["paths"].(map[string]interface{})
if !ok {
t.Fatal("Expected paths in spec")
}
foundJSONPost := false
var testCurl string
for pathName, pathItem := range paths {
if pathMap, ok := pathItem.(map[string]interface{}); ok {
if postOp, ok := pathMap["post"].(map[string]interface{}); ok {
if reqBody, ok := postOp["requestBody"].(map[string]interface{}); ok {
if content, ok := reqBody["content"].(map[string]interface{}); ok {
if jsonContent, ok := content["application/json"].(map[string]interface{}); ok {
if schema, ok := jsonContent["schema"].(map[string]interface{}); ok {
// Generate a minimal curl command to verify quoting
expanded := ExpandSchema(spec, schema, map[string]bool{}, spec)
example := GenerateExample(expanded)
bodyBytes, err := json.Marshal(example)
if err == nil {
// This simulates the curl generation logic
testCurl = fmt.Sprintf("curl -X POST \"%s%s%s\" -H \"Content-Type: application/json\" -d '%s'",
apiTarget, basePath, pathName, bodyBytes)
foundJSONPost = true
break
}
}
}
}
}
}
}
if foundJSONPost {
break
}
}
if !foundJSONPost {
t.Skip("No POST endpoint with JSON body found")
}
// Verify the curl command has proper quoting
// Should end with -d '...' NOT -d '...'"
if !strings.Contains(testCurl, "-d '") {
t.Error("Expected -d ' in curl command")
}
// Check that it doesn't have the trailing quote bug: -d '%s'"
if strings.Contains(testCurl, "'\"") {
t.Error("Found trailing quote bug: curl has '\" which indicates malformed quoting")
}
// Verify it ends with a single quote after the JSON data
if !strings.HasSuffix(testCurl, "'}") && !strings.HasSuffix(testCurl, "']") && !strings.HasSuffix(testCurl, "'") {
t.Errorf("Curl command should end with properly closed JSON in single quotes, got: %s", testCurl[len(testCurl)-20:])
}
}
func TestRelativeServerURLHandling(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldSwaggerURL := swaggerURL
oldAPITarget := apiTarget
oldBasePath := basePath
defer func() {
specBaseDir = oldSpecBaseDir
swaggerURL = oldSwaggerURL
apiTarget = oldAPITarget
basePath = oldBasePath
}()
specPath := filepath.Join("..", "tests", "test_relative_server.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
// Test 1: Verify that when -T is used with a spec that has a relative URL,
// the basePath from the spec is preserved
swaggerURL = ""
apiTarget = "https://example.com" // Simulating -T flag
basePath = ""
// Parse server info from spec (simulating what GenerateRequests does)
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
if srv, ok := servers[0].(map[string]interface{}); ok {
if serverURL, ok := srv["url"].(string); ok {
if !strings.Contains(serverURL, "://") && serverURL != "/" {
// Relative URL - should become basePath
basePath = serverURL
}
}
}
}
// Verify basePath was extracted even though -T was used
if basePath != "/api/v1" {
t.Errorf("Expected basePath to be '/api/v1' from spec even with -T flag, got: '%s'", basePath)
}
// Verify apiTarget was not overwritten by spec (since -T was used)
if apiTarget != "https://example.com" {
t.Errorf("Expected apiTarget to remain 'https://example.com' from -T flag, got: '%s'", apiTarget)
}
// Test 2: Verify that endpoints command can work even without apiTarget
// Reset for this test
apiTarget = ""
basePath = ""
swaggerURL = ""
// When apiTarget is empty and we have relative URL, it should still extract basePath
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
if srv, ok := servers[0].(map[string]interface{}); ok {
if serverURL, ok := srv["url"].(string); ok {
if !strings.Contains(serverURL, "://") && serverURL != "/" {
basePath = normalizeBasePath(serverURL)
// For endpoints command, we don't need apiTarget
}
}
}
}
if basePath != "/api/v1" {
t.Errorf("Expected basePath to be extracted for endpoints command, got: '%s'", basePath)
}
}
func TestTargetFlagPreservesSpecBasePath(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldSwaggerURL := swaggerURL
oldAPITarget := apiTarget
oldBasePath := basePath
defer func() {
specBaseDir = oldSpecBaseDir
swaggerURL = oldSwaggerURL
apiTarget = oldAPITarget
basePath = oldBasePath
}()
// Test with Swagger v2 spec
specPath := filepath.Join("..", "tests", "test_spec_v2.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
// Simulate -T flag being used
swaggerURL = ""
apiTarget = "https://staging.example.com" // User provided target
basePath = ""
// Parse basePath from spec (should happen even with -T)
if v, ok := spec["swagger"].(string); ok && strings.HasPrefix(v, "2") {
if bp, ok := spec["basePath"].(string); ok && bp != "/" && bp != "" {
basePath = bp
}
}
// Verify basePath was extracted
if basePath != "/v1" {
t.Errorf("Expected basePath '/v1' to be preserved from spec even with -T flag, got: '%s'", basePath)
}
// Verify full constructed path would be correct
fullPath := apiTarget + basePath + "/users"
expectedPath := "https://staging.example.com/v1/users"
if fullPath != expectedPath {
t.Errorf("Expected full path '%s', got '%s'", expectedPath, fullPath)
}
}
func TestOpenAPIv3AbsoluteServerURLWithTargetFlag(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldSwaggerURL := swaggerURL
oldAPITarget := apiTarget
oldBasePath := basePath
defer func() {
specBaseDir = oldSpecBaseDir
swaggerURL = oldSwaggerURL
apiTarget = oldAPITarget
basePath = oldBasePath
}()
// Test with OpenAPI v3 spec that has absolute server URL with path
specPath := filepath.Join("..", "tests", "test_spec_v3.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
// Simulate -T flag being used (user wants to override host)
swaggerURL = ""
apiTarget = "https://staging.example.com" // User provided target
basePath = ""
// Parse server info from spec (simulating what GenerateRequests does)
if v, ok := spec["openapi"].(string); ok && strings.HasPrefix(v, "3") {
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
if srv, ok := servers[0].(map[string]interface{}); ok {
if serverURL, ok := srv["url"].(string); ok {
if strings.Contains(serverURL, "://") {
// This is an absolute URL
// When -T is set, we should still extract the path
if apiTarget != "" {
// Parse the server URL to extract path
if parsedURL, err := url.Parse(serverURL); err == nil && parsedURL.Path != "" && parsedURL.Path != "/" {
basePath = normalizeBasePath(parsedURL.Path)
}
}
}
}
}
}
}
// Verify basePath was extracted from the absolute server URL
if basePath != "/v1" {
t.Errorf("Expected basePath '/v1' to be extracted from absolute server URL even with -T flag, got: '%s'", basePath)
}
// Verify apiTarget was not overwritten (should still be from -T flag)
if apiTarget != "https://staging.example.com" {
t.Errorf("Expected apiTarget to remain 'https://staging.example.com' from -T flag, got: '%s'", apiTarget)
}
// Verify full constructed path would be correct
fullPath := apiTarget + basePath + "/users"
expectedPath := "https://staging.example.com/v1/users"
if fullPath != expectedPath {
t.Errorf("Expected full path '%s', got '%s'", expectedPath, fullPath)
}
}
func TestOpenAPIv3AbsoluteServerURLWithoutTargetFlag(t *testing.T) {
oldSpecBaseDir := specBaseDir
oldSwaggerURL := swaggerURL
oldAPITarget := apiTarget
oldBasePath := basePath
defer func() {
specBaseDir = oldSpecBaseDir
swaggerURL = oldSwaggerURL
apiTarget = oldAPITarget
basePath = oldBasePath
}()
specPath := filepath.Join("..", "tests", "test_spec_v3.yaml")
absPath, _ := filepath.Abs(specPath)
specBaseDir = filepath.Dir(absPath)
data, err := os.ReadFile(specPath)
if err != nil {
t.Skipf("Test spec not found: %v", err)
return
}
spec := SafelyUnmarshalSpec(data)
if spec == nil {
t.Fatal("Failed to unmarshal spec")
}
swaggerURL = ""
apiTarget = ""
basePath = ""
if v, ok := spec["openapi"].(string); ok && strings.HasPrefix(v, "3") {
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
if srv, ok := servers[0].(map[string]interface{}); ok {
if serverURL, ok := srv["url"].(string); ok && strings.Contains(serverURL, "://") {
if parsedURL, err := url.Parse(serverURL); err == nil {
basePath = normalizeBasePath(parsedURL.Path)
if apiTarget == "" {
apiTarget = parsedURL.Scheme + "://" + parsedURL.Host
}
}
}
}
}
}
if apiTarget != "https://api.example.com" {
t.Errorf("Expected apiTarget 'https://api.example.com', got '%s'", apiTarget)
}
if basePath != "/v1" {
t.Errorf("Expected basePath '/v1', got '%s'", basePath)
}
if endpointPath := basePath + "/users"; endpointPath != "/v1/users" {
t.Errorf("Expected endpoints path '/v1/users', got '%s'", endpointPath)
}
}
func TestNormalizeBasePath(t *testing.T) {
cases := []struct {
in string
want string
}{
{in: "", want: ""},
{in: "/", want: ""},
{in: " /v1/ ", want: "/v1"},
{in: "/v1/", want: "/v1"},
{in: "/v1", want: "/v1"},
{in: "v1/", want: "/v1"},
{in: "v1", want: "/v1"},
}
for _, tc := range cases {
got := normalizeBasePath(tc.in)
if got != tc.want {
t.Errorf("normalizeBasePath(%q): expected %q, got %q", tc.in, tc.want, got)
}
}
}
+13 -11
View File
@@ -1,26 +1,28 @@
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/sirupsen/logrus v1.9.0
github.com/fatih/color v1.18.0
github.com/getkin/kin-openapi v0.132.0
github.com/spf13/cobra v1.6.1
golang.org/x/time v0.8.0
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/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/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
golang.org/x/sys v0.25.0 // indirect
)
+33 -45
View File
@@ -1,70 +1,58 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/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=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/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=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-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/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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+46
View File
@@ -0,0 +1,46 @@
openapi: 3.0.0
info:
title: Shared Schemas
version: 1.0.0
components:
schemas:
Customer:
type: object
required:
- customerId
- companyName
properties:
customerId:
type: string
example: "CUST-12345"
companyName:
type: string
example: "Acme Corporation"
contactInfo:
$ref: '#/components/schemas/ContactInfo'
billingAddress:
$ref: '#/components/schemas/Address'
ContactInfo:
type: object
properties:
phone:
type: string
example: "+1-555-0123"
email:
type: string
example: "[email protected]"
Address:
type: object
properties:
street:
type: string
example: "123 Main St"
city:
type: string
example: "Springfield"
state:
type: string
example: "IL"
postalCode:
type: string
example: "62701"
+13
View File
@@ -0,0 +1,13 @@
openapi: 3.0.0
info:
title: Schema A
version: 1.0.0
components:
schemas:
SchemaA:
type: object
properties:
id:
type: string
nested:
$ref: 'schema_b.yaml#/components/schemas/SchemaB'
+12
View File
@@ -0,0 +1,12 @@
openapi: 3.0.0
info:
title: Schema B
version: 1.0.0
components:
schemas:
SchemaB:
type: object
properties:
value:
type: string
example: "nested-value"
+17
View File
@@ -0,0 +1,17 @@
openapi: 3.0.0
info:
title: Main spec for nested external refs test
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: 'subdir/schema_a.yaml#/components/schemas/SchemaA'
responses:
'200':
description: OK
+23
View File
@@ -0,0 +1,23 @@
openapi: 3.0.0
info:
title: Query Object Test
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/search:
get:
summary: Search with object filter
parameters:
- name: filter
in: query
schema:
type: object
properties:
name:
type: string
age:
type: integer
responses:
'200':
description: Success
+12
View File
@@ -0,0 +1,12 @@
openapi: 3.0.0
info:
title: Relative Server URL Test
version: 1.0.0
servers:
- url: /api/v1
paths:
/test:
get:
responses:
'200':
description: OK
+30
View File
@@ -0,0 +1,30 @@
openapi: 3.0.0
info:
title: Main API Spec
version: 1.0.0
description: Test spec with external file references
servers:
- url: https://api.example.com/v2
paths:
/customers:
get:
summary: Get customers
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: './external_schemas.yaml#/components/schemas/Customer'
post:
summary: Create customer
requestBody:
content:
application/json:
schema:
$ref: './external_schemas.yaml#/components/schemas/Customer'
responses:
'201':
description: Created
+124
View File
@@ -0,0 +1,124 @@
swagger: "2.0"
info:
title: Swagger v2 Test API
version: 1.0.0
description: Test spec for validating Swagger 2.0 reference resolution
host: api.example.com
basePath: /v1
schemes:
- https
paths:
/users:
get:
summary: Get all users
parameters:
- $ref: '#/parameters/LimitParam'
- name: offset
in: query
type: integer
responses:
'200':
description: Success
schema:
type: array
items:
$ref: '#/definitions/User'
post:
summary: Create user
parameters:
- name: user
in: body
schema:
$ref: '#/definitions/User'
responses:
'201':
description: Created
/products/{id}:
get:
summary: Get product
parameters:
- name: id
in: path
type: string
required: true
responses:
'200':
description: Success
schema:
$ref: '#/definitions/Product'
/orders:
post:
summary: Create order
parameters:
- name: order
in: body
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/definitions/OrderItem'
total:
type: number
responses:
'201':
description: Created
parameters:
LimitParam:
name: limit
in: query
type: integer
default: 10
definitions:
User:
type: object
required:
- name
- email
properties:
id:
type: integer
example: 42
name:
type: string
example: "Jane Smith"
email:
type: string
example: "[email protected]"
address:
$ref: '#/definitions/Address'
Address:
type: object
properties:
street:
type: string
city:
type: string
zipCode:
type: string
example: "12345"
Product:
type: object
properties:
id:
type: string
name:
type: string
price:
type: number
example: 29.99
category:
type: string
enum:
- electronics
- clothing
- books
OrderItem:
type: object
properties:
productId:
type: string
quantity:
type: integer
example: 2
+159
View File
@@ -0,0 +1,159 @@
openapi: 3.0.0
info:
title: Test API with References
version: 1.0.0
description: Test spec for validating reference resolution improvements
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: Get all users
parameters:
- $ref: '#/components/parameters/LimitParam'
- name: offset
in: query
schema:
type: integer
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create user
requestBody:
$ref: '#/components/requestBodies/UserBody'
responses:
'201':
description: Created
/products/{id}:
get:
summary: Get product by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
/pets:
post:
summary: Create a pet
requestBody:
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
responses:
'201':
description: Created
/config:
post:
summary: Update config
requestBody:
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Config'
responses:
'200':
description: Updated
components:
parameters:
LimitParam:
name: limit
in: query
schema:
type: integer
example: 10
requestBodies:
UserBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
schemas:
User:
type: object
required:
- name
- email
properties:
id:
type: integer
example: 42
name:
type: string
example: "John Doe"
email:
type: string
example: "[email protected]"
address:
$ref: '#/components/schemas/Address'
preferences:
type: object
additionalProperties:
type: string
Address:
type: object
properties:
street:
type: string
city:
type: string
country:
type: string
enum:
- USA
- Canada
- UK
Product:
type: object
properties:
id:
type: string
name:
type: string
price:
type: number
tags:
type: array
items:
type: string
Cat:
type: object
properties:
type:
type: string
enum: [cat]
meow:
type: boolean
Dog:
type: object
properties:
type:
type: string
enum: [dog]
bark:
type: boolean
Config:
type: object
properties:
theme:
type: string
example: "dark"
notifications:
type: boolean