mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-08 01:38:15 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f256efca6 | ||
|
|
48c4d83f54 | ||
|
|
365f68696f | ||
|
|
fbf484c3a1 | ||
|
|
bc708d69f8 | ||
|
|
56346e759e | ||
|
|
a69b4dcae1 | ||
|
|
7d9fa54a58 | ||
|
|
09a1183e6f | ||
|
|
90e9c78d9a | ||
|
|
3f99ffb48c | ||
|
|
331bb1a05c | ||
|
|
3c2a3b807d | ||
|
|
7ef8abdb8e | ||
|
|
f1f9e19422 | ||
|
|
0d6776b758 | ||
|
|
8f4e4a5d9f | ||
|
|
55baa79785 | ||
|
|
5bdafb64d8 | ||
|
|
27b59354fc | ||
|
|
bee5088fcf | ||
|
|
1eda960182 | ||
|
|
e805a7d190 | ||
|
|
18b20ad476 | ||
|
|
5115b3cba3 | ||
|
|
942ad84c54 | ||
|
|
878f7dcd39 | ||
|
|
275f426fc2 | ||
|
|
6d027a66ce | ||
|
|
703039dc75 | ||
|
|
e2a94378bf | ||
|
|
c9fa4f1257 | ||
|
|
90f0870bfb | ||
|
|
122d9d0114 | ||
|
|
ac50c4da75 | ||
|
|
6eea0f8a77 | ||
|
|
bdea5822e8 | ||
|
|
7bb228d81c | ||
|
|
3258ca341f | ||
|
|
58d4e91760 | ||
|
|
1f5a946011 | ||
|
|
143fdfda1b | ||
|
|
a33eef47f4 | ||
|
|
4c466112ee | ||
|
|
1195ff7fbf | ||
|
|
bd3265d0a4 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -37,28 +37,84 @@ $ 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 -qi -p http://127.0.0.1:8080
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://127.0.0.1:8080
|
||||
|
||||
INFO[0000] Gathering API details.
|
||||
Gathering API details.
|
||||
Title: Swagger Petstore
|
||||
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/pet/findByTags
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=/v2/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/store/order
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/user/createWithList
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/user/login
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/user/logout
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=/v2/v2/pet/1/uploadImage
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/pet
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=/v2/v2/pet
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/pet/findByStatus
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/store/inventory
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=/v2/v2/store/order/1
|
||||
ERRO Endpoint not found. Method=GET Status=404 Target=/v2/v2/user/bishopfox
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=/v2/v2/user/bishopfox
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/user/createWithArray
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=/v2/v2/user
|
||||
✓ 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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi --replay-proxy http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -185,6 +241,7 @@ Flags:
|
||||
-o, --outfile string Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.
|
||||
-p, --proxy string Proxy host and port. Example: http://127.0.0.1:8080 (default "NOPROXY")
|
||||
-q, --quiet Do not prompt for user input - uses default values for all requests.
|
||||
--replay-proxy string Replay matched requests using this proxy.
|
||||
--randomize-user-agent Randomizes the user agent string. Default is 'false'.
|
||||
-s, --safe-word stringArray Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.
|
||||
-T, --target string Manually set a target for the requests to be made if separate from the host the documentation resides on.
|
||||
|
||||
+31
-30
@@ -4,8 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,16 +22,14 @@ func CheckSecuritySchemes(spec 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.")
|
||||
printInfo("No security schemes defined.\n")
|
||||
} else {
|
||||
if outputFormat != "json" {
|
||||
fmt.Println("Found security schemes:")
|
||||
}
|
||||
printInfo("Found security schemes:\n")
|
||||
var apiKey string
|
||||
var apiKeyName string
|
||||
|
||||
for mechanism, value := range securitySchemes {
|
||||
fmt.Printf(" - %s\n", mechanism)
|
||||
printInfo(" - %s\n", mechanism)
|
||||
scheme, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
@@ -45,30 +41,35 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
if schemeType := scheme["scheme"]; schemeType != nil {
|
||||
switch schemeType {
|
||||
case "basic":
|
||||
fmt.Println("Basic Authentication is accepted. Supply a username and password? (y/N)")
|
||||
fmt.Scanln(&autoApplyBasicAuth)
|
||||
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
|
||||
if autoApplyBasicAuth == "y" {
|
||||
fmt.Printf("Enter a username.")
|
||||
fmt.Scanln(&basicAuthUser)
|
||||
fmt.Printf("Enter a password.")
|
||||
fmt.Scanln(&basicAuthPass)
|
||||
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
|
||||
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
|
||||
log.Infof("Using %s as the Basic Auth value.", basicAuthString)
|
||||
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
|
||||
if quiet {
|
||||
autoApplyBasicAuth = "n"
|
||||
printWarn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
} else {
|
||||
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
printInfo("Basic Authentication is accepted. Supply a username and password? (y/N)\n")
|
||||
fmt.Scanln(&autoApplyBasicAuth)
|
||||
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
|
||||
if autoApplyBasicAuth == "y" {
|
||||
printInfo("Enter a username.")
|
||||
fmt.Scanln(&basicAuthUser)
|
||||
printInfo("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":
|
||||
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
|
||||
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":
|
||||
log.Infof("An API key can be provided via a parameter string. Would you like to apply one? (y/N)")
|
||||
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 {
|
||||
@@ -80,13 +81,13 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
if nameVal, ok := scheme["name"].(string); ok {
|
||||
apiKeyName = nameVal
|
||||
}
|
||||
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
|
||||
printInfo("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)
|
||||
printInfo("Using %s=%s as the API key in all requests.\n", apiKeyName, apiKey)
|
||||
}
|
||||
case "header":
|
||||
if mechanism == "bearer" {
|
||||
log.Infof("A bearer token is accepted. Would you like to provide one? (y/N)")
|
||||
printInfo("A bearer token is accepted. Would you like to provide one? (y/N)\n")
|
||||
if quiet {
|
||||
autoApplyBearer = "n"
|
||||
} else {
|
||||
@@ -94,15 +95,15 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
autoApplyBearer = strings.ToLower(autoApplyBearer)
|
||||
}
|
||||
if autoApplyBearer == "y" {
|
||||
fmt.Printf("What value would you like to use for the Bearer Token? ")
|
||||
printInfo("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.")
|
||||
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 {
|
||||
log.Infof("An API key can be provided via the header %s. Would you like to apply one? (y/N)", nameVal)
|
||||
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 {
|
||||
@@ -111,7 +112,7 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
}
|
||||
if autoApplyAPIKey == "y" {
|
||||
apiKeyName = nameVal
|
||||
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
|
||||
printInfo("What value would you like to use for the API key (%s)?", apiKeyName)
|
||||
fmt.Scanln(&apiKey)
|
||||
Headers = append(Headers, nameVal+": "+apiKey)
|
||||
}
|
||||
@@ -123,7 +124,7 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
|
||||
}
|
||||
|
||||
if bearerFormat, ok := scheme["bearerFormat"].(string); ok {
|
||||
fmt.Println(" - bearerFormat:", bearerFormat)
|
||||
printInfo(" - bearerFormat: %s\n", bearerFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckSecuritySchemesJSONDiagnostics(t *testing.T) {
|
||||
stdout, err := os.CreateTemp(t.TempDir(), "stdout")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stderr, err := os.CreateTemp(t.TempDir(), "stderr")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
previousStdout, previousStderr := os.Stdout, os.Stderr
|
||||
previousFormat, previousQuiet := outputFormat, quiet
|
||||
defer func() {
|
||||
os.Stdout, os.Stderr = previousStdout, previousStderr
|
||||
outputFormat, quiet = previousFormat, previousQuiet
|
||||
stdout.Close()
|
||||
stderr.Close()
|
||||
}()
|
||||
os.Stdout, os.Stderr = stdout, stderr
|
||||
outputFormat, quiet = "json", true
|
||||
|
||||
CheckSecuritySchemes(map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"securitySchemes": map[string]interface{}{
|
||||
"ExampleBearer": map[string]interface{}{
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
stdoutBytes, err := os.ReadFile(stdout.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(stdoutBytes) != 0 {
|
||||
t.Fatalf("authentication diagnostics written to stdout: %q", stdoutBytes)
|
||||
}
|
||||
|
||||
stderrBytes, err := os.ReadFile(stderr.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, expected := range []string{"ExampleBearer", "bearerFormat: JWT"} {
|
||||
if !strings.Contains(string(stderrBytes), expected) {
|
||||
t.Errorf("stderr does not contain %q: %q", expected, stderrBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-10
@@ -1,14 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var getAccessibleEndpoints bool
|
||||
@@ -26,7 +25,7 @@ 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"
|
||||
}
|
||||
@@ -40,17 +39,22 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
|
||||
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 {
|
||||
die("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, replayClient := CheckAndConfigureProxy()
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n")
|
||||
printInfo("\n")
|
||||
printInfo("Gathering API details.\n")
|
||||
}
|
||||
|
||||
if swaggerURL != "" {
|
||||
@@ -58,7 +62,14 @@ 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)
|
||||
@@ -68,7 +79,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
log.Info("Sending requests at a rate of ", rateLimit, " requests per second.")
|
||||
}
|
||||
*/
|
||||
GenerateRequests(bodyBytes, client)
|
||||
GenerateRequests(bodyBytes, client, replayClient)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+174
-55
@@ -16,16 +16,28 @@ import (
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var endpointOnly bool
|
||||
var endpointWordlist string
|
||||
|
||||
// sawWAFChallenge ensures the WAF/challenge warning is emitted only once per run.
|
||||
var sawWAFChallenge bool
|
||||
|
||||
// warnWAFChallengeOnce warns, once, that responses look like a WAF challenge.
|
||||
func warnWAFChallengeOnce() {
|
||||
if sawWAFChallenge {
|
||||
return
|
||||
}
|
||||
sawWAFChallenge = true
|
||||
printWarn("Some responses look like a Cloudflare/WAF browser challenge (e.g. an HTTP 403 \"Just a moment...\" interstitial), so requests are likely being blocked before reaching the app.")
|
||||
printWarn("To scan a challenge-protected target, capture a solved browser session and replay it, e.g.:\n\tsj brute -u %s -A \"<your browser User-Agent>\" -H \"Cookie: cf_clearance=<value>; __cf_bm=<value>\"\n(the cf_clearance cookie is bound to your IP + User-Agent, so run from the same host and use the exact browser User-Agent).", swaggerURL)
|
||||
}
|
||||
|
||||
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 htmlEndpoints []string = []string{"/index", "/swagger", "/swagger-ui", "/api-docs"}
|
||||
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{
|
||||
@@ -36,76 +48,81 @@ var bruteCmd = &cobra.Command{
|
||||
|
||||
/* // 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, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/", false)...)
|
||||
// HTML UI entrypoints first: one hit cascades to its JS initializer
|
||||
// and spec, so UI-referenced specs are found early. Then the priority
|
||||
// list, then the JS and JSON sweeps.
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, htmlEndpoints, ".html", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "", 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 {
|
||||
if endpointOnly {
|
||||
@@ -116,12 +133,12 @@ var bruteCmd = &cobra.Command{
|
||||
}
|
||||
// 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("\nNo 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, skipPrefix bool) []string {
|
||||
func makeURLs(target string, basePath string, endpoints []string, fileExtension string, skipPrefix bool) []string {
|
||||
urls := []string{}
|
||||
if !skipPrefix {
|
||||
for _, dir := range prefixDirs {
|
||||
@@ -129,7 +146,7 @@ func makeURLs(target string, endpoints []string, fileExtension string, skipPrefi
|
||||
if dir == "" && endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + dir + endpoint + fileExtension
|
||||
targetURL := target + basePath + dir + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
|
||||
}
|
||||
@@ -139,46 +156,105 @@ func makeURLs(target string, endpoints []string, fileExtension string, skipPrefi
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + endpoint + fileExtension
|
||||
targetURL := target + basePath + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
// maxFollowDepth bounds how far brute chases discovered spec references.
|
||||
const maxFollowDepth = 3
|
||||
|
||||
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
seen := make(map[string]bool)
|
||||
for i, url := range urls {
|
||||
ct := CheckContentType(client, url)
|
||||
if strings.Contains(ct, "application/json") {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
checkSpec := UnmarshalSpec(bodyBytes)
|
||||
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
|
||||
fmt.Println("")
|
||||
log.Infof("Definition file found: %s\n", url)
|
||||
if found, checkSpec := checkURLForSpec(url, client, seen, 0); found {
|
||||
return true, checkSpec
|
||||
}
|
||||
writeProgress("Request: %d", i+1)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// checkURLForSpec fetches one URL and validates it as an OpenAPI/Swagger
|
||||
// definition. For a Swagger UI page, initializer script, or swagger-config JSON,
|
||||
// it follows the referenced URLs (bounded by maxFollowDepth and a shared visited
|
||||
// set) so specs on non-standard paths are still found.
|
||||
func checkURLForSpec(target string, client http.Client, seen map[string]bool, depth int) (bool, *openapi3.T) {
|
||||
if depth > maxFollowDepth || seen[target] || isExternalDemoSpecURL(target) {
|
||||
return false, nil
|
||||
}
|
||||
seen[target] = true
|
||||
|
||||
// One request so body and Content-Type come from the same response (two
|
||||
// separate requests can desync behind a WAF/CDN).
|
||||
bodyBytes, bodyString, _, ctRaw := MakeRequestFull(client, "GET", target, timeout, nil)
|
||||
if bodyBytes == nil {
|
||||
return false, nil
|
||||
}
|
||||
ct := strings.ToLower(ctRaw)
|
||||
|
||||
// Skip content types that cannot be a spec or reference one.
|
||||
for _, skip := range []string{"image/", "video/", "audio/", "font/", "text/css", "application/zip", "application/pdf"} {
|
||||
if strings.Contains(ct, skip) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if looksLikeChallengeResponse(bodyString) {
|
||||
warnWAFChallengeOnce()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Swagger UI HTML page: follow the references it declares.
|
||||
if strings.Contains(ct, "text/html") || looksLikeHTMLDocument(bodyString) {
|
||||
return followReferences(ExtractSpecURLsFromHTML(bodyString, target), client, seen, depth)
|
||||
}
|
||||
|
||||
// JavaScript initializer: embedded spec, or the spec url it configures.
|
||||
// Library bundles are excluded to avoid their internal url: strings.
|
||||
if strings.Contains(ct, "javascript") || strings.HasSuffix(strings.ToLower(urlPath(target)), ".js") {
|
||||
if !looksLikeSwaggerInitCandidate(target) {
|
||||
return false, nil
|
||||
}
|
||||
if bodyHasEmbeddedSpec(bodyString) {
|
||||
if jsonContent, ok := ExtractJSONFromJSSpec(bodyBytes); ok {
|
||||
checkSpec := UnmarshalSpec(jsonContent)
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
printInfo("Found operation definitions embedded in JavaScript file at %s\n", target)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
var refs []string
|
||||
for _, ref := range extractJSSpecURLs(bodyString) {
|
||||
if !looksLikeSpecReference(ref) {
|
||||
continue
|
||||
}
|
||||
if abs, ok := resolveReferenceURL(target, ref); ok {
|
||||
refs = append(refs, abs)
|
||||
}
|
||||
}
|
||||
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 followReferences(dedupeURLs(refs), client, seen, depth)
|
||||
}
|
||||
|
||||
// Otherwise treat the body as a candidate spec and validate by parsing.
|
||||
checkSpec := UnmarshalSpec(bodyBytes)
|
||||
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
|
||||
printInfo("Definition file found: %s\n", target)
|
||||
return true, checkSpec
|
||||
}
|
||||
|
||||
// Not a spec: may be a swagger-config document that references one.
|
||||
return followReferences(ExtractSpecURLsFromHTML(bodyString, target), client, seen, depth)
|
||||
}
|
||||
|
||||
// followReferences recurses into a set of discovered spec/reference URLs.
|
||||
func followReferences(refs []string, client http.Client, seen map[string]bool, depth int) (bool, *openapi3.T) {
|
||||
for _, ref := range refs {
|
||||
if found, checkSpec := checkURLForSpec(ref, client, seen, depth+1); found {
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
@@ -190,6 +266,49 @@ func init() {
|
||||
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
|
||||
@@ -212,7 +331,7 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
bodyBytes = []byte(spec[openApiIndex:specClose])
|
||||
_ = json.Unmarshal(bodyBytes, &doc2)
|
||||
if !strings.Contains(doc2.Swagger, "2") {
|
||||
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
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`) {
|
||||
@@ -228,11 +347,11 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
bodyBytes = []byte(spec[openApiIndex:specClose])
|
||||
_ = json.Unmarshal(bodyBytes, &doc3)
|
||||
if !strings.Contains(doc3.OpenAPI, "3") {
|
||||
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
printErr("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
|
||||
}
|
||||
|
||||
return bodyBytes
|
||||
@@ -259,14 +378,14 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
|
||||
} 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)
|
||||
printErr("Error converting v2 document to v3: %s", err)
|
||||
}
|
||||
return newDoc
|
||||
} else if os.Args[1] == "brute" {
|
||||
var noDoc openapi3.T
|
||||
return &noDoc
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.")
|
||||
die("Error parsing definition file.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+14
-15
@@ -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("\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,22 +60,22 @@ 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)
|
||||
if err != nil {
|
||||
fmt.Printf("Error converting v2 document to v3: %s\n", err)
|
||||
printErr("Error converting v2 document to v3: %s", err)
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
if strings.HasSuffix(outfile, "yaml") || strings.HasSuffix(outfile, "yml") {
|
||||
log.Warn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
|
||||
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 == "" {
|
||||
@@ -95,7 +94,7 @@ var convertCmd = &cobra.Command{
|
||||
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))
|
||||
@@ -105,7 +104,7 @@ var convertCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.")
|
||||
die("Error parsing definition file.")
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -113,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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Helpers for following spec references out of Swagger UI HTML pages and their
|
||||
// initializer scripts. Extraction is permissive: candidates are captured and
|
||||
// validated by the caller (fetch + parse), so custom document names still work.
|
||||
|
||||
// url / "url" / configUrl / specUrl assignment values (also each entry of a urls: [...] array).
|
||||
var htmlSpecKeyRE = regexp.MustCompile(`(?i)"?(?:config|spec)?url"?\s*:\s*["']([^"']+)["']`)
|
||||
|
||||
// spec-url / href / src attribute values.
|
||||
var htmlSpecAttrRE = regexp.MustCompile(`(?i)(?:spec-url|href|src)\s*=\s*["']([^"']+)["']`)
|
||||
|
||||
// Swashbuckle discoveryPaths: arrayFrom('a|b') first argument.
|
||||
var swashbuckleDiscoveryRE = regexp.MustCompile(`(?i)discoveryPaths\s*:\s*arrayFrom\(\s*['"]([^'"]+)['"]`)
|
||||
|
||||
// url: assignments inside an initializer script; a { or , before url avoids matching unrelated keys.
|
||||
var jsSpecURLAssignRE = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)(?:{\s*|,\s*)url\s*:\s*"([^"]+)"`),
|
||||
regexp.MustCompile(`(?i)(?:{\s*|,\s*)url\s*:\s*'([^']+)'`),
|
||||
regexp.MustCompile(`(?i)(?:{\s*|,\s*)"url"\s*:\s*"([^"]+)"`),
|
||||
}
|
||||
|
||||
// ExtractSpecURLsFromHTML returns the spec/initializer URLs referenced by a
|
||||
// Swagger UI page (or swagger-config JSON), resolved against pageURL and deduped.
|
||||
func ExtractSpecURLsFromHTML(htmlContent, pageURL string) []string {
|
||||
var out []string
|
||||
add := func(ref string) {
|
||||
if abs, ok := resolveReferenceURL(pageURL, ref); ok {
|
||||
out = append(out, abs)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range htmlSpecKeyRE.FindAllStringSubmatch(htmlContent, -1) {
|
||||
if len(m) > 1 && looksLikeSpecReference(m[1]) {
|
||||
add(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Keep spec-like href/src refs, plus initializer scripts (followed for the url they configure).
|
||||
for _, m := range htmlSpecAttrRE.FindAllStringSubmatch(htmlContent, -1) {
|
||||
if len(m) > 1 && (looksLikeSpecReference(m[1]) || looksLikeSwaggerInitCandidate(m[1])) {
|
||||
add(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Swashbuckle discoveryPaths are relative to the site root.
|
||||
if dm := swashbuckleDiscoveryRE.FindStringSubmatch(htmlContent); len(dm) > 1 {
|
||||
if paths := strings.Split(dm[1], "|"); len(paths) > 0 {
|
||||
first := strings.TrimSpace(paths[0])
|
||||
if first != "" {
|
||||
if !strings.HasPrefix(first, "/") && !strings.Contains(first, "://") {
|
||||
first = "/" + first
|
||||
}
|
||||
add(first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeURLs(out)
|
||||
}
|
||||
|
||||
// extractJSSpecURLs pulls url: assignments from an initializer script body.
|
||||
func extractJSSpecURLs(js string) []string {
|
||||
var out []string
|
||||
for _, re := range jsSpecURLAssignRE {
|
||||
for _, m := range re.FindAllStringSubmatch(js, -1) {
|
||||
if len(m) > 1 {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// looksLikeSpecReference accepts references that plausibly point at an
|
||||
// OpenAPI/Swagger document (including custom .json/.yaml names) and rejects UI
|
||||
// assets and bundles. Matches are still validated by fetching and parsing.
|
||||
func looksLikeSpecReference(ref string) bool {
|
||||
s := strings.TrimSpace(ref)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
lo := strings.ToLower(s)
|
||||
if strings.HasPrefix(lo, "javascript:") || strings.HasPrefix(lo, "data:") || strings.HasPrefix(lo, "#") {
|
||||
return false
|
||||
}
|
||||
for _, ext := range []string{".js", ".css", ".map", ".html", ".htm", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf"} {
|
||||
if strings.HasSuffix(lo, ext) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if strings.Contains(lo, "swagger-ui-bundle") || strings.Contains(lo, "swagger-ui-standalone") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lo, "api-docs") || strings.Contains(lo, "api_docs") || strings.Contains(lo, "apidocs") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lo, "openapi") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lo, "swagger.json") || strings.Contains(lo, "swagger.yaml") || strings.Contains(lo, "swagger.yml") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lo, "api-json") || strings.Contains(lo, "api_json") {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(lo, ".json") || strings.HasSuffix(lo, ".yaml") || strings.HasSuffix(lo, ".yml") {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(lo, "-json") || strings.HasSuffix(lo, "_json") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lo, "swagger-resources") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Library bundles are never followed as a config source: they are large and full of internal url: strings.
|
||||
var swaggerUILibraryScripts = []string{
|
||||
"swagger-ui-bundle", "swagger-ui-standalone", "swagger-ui-es-bundle",
|
||||
"swagger-ui-layout", "swagger-ui-plugins", "swagger-ui.min", "swagger-ui.js",
|
||||
"redoc.standalone", "redoc.min", "redoc.js",
|
||||
}
|
||||
|
||||
// looksLikeSwaggerInitCandidate reports whether a .js reference is a plausible
|
||||
// initializer (swagger-initializer.js, swagger-ui-init.js, index.js, ...) rather
|
||||
// than a library bundle.
|
||||
func looksLikeSwaggerInitCandidate(rawURL string) bool {
|
||||
p := strings.ToLower(urlPath(rawURL))
|
||||
if !strings.HasSuffix(p, ".js") {
|
||||
return false
|
||||
}
|
||||
for _, lib := range swaggerUILibraryScripts {
|
||||
if strings.Contains(p, lib) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// urlPath returns the path component of a URL, or the input unchanged if it does
|
||||
// not parse (so relative references still work).
|
||||
func urlPath(raw string) string {
|
||||
if u, err := url.Parse(raw); err == nil && u.Path != "" {
|
||||
return u.Path
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// bodyHasEmbeddedSpec is a cheap sniff for a spec inlined in a JavaScript body.
|
||||
func bodyHasEmbeddedSpec(body string) bool {
|
||||
lo := strings.ToLower(body)
|
||||
return strings.Contains(lo, `"paths"`) && (strings.Contains(lo, `"openapi"`) || strings.Contains(lo, `"swagger"`))
|
||||
}
|
||||
|
||||
// looksLikeHTMLDocument reports whether a response body is an HTML document.
|
||||
func looksLikeHTMLDocument(body string) bool {
|
||||
t := strings.TrimSpace(strings.ToLower(body))
|
||||
return strings.HasPrefix(t, "<!doctype") || strings.HasPrefix(t, "<html")
|
||||
}
|
||||
|
||||
// Substrings found in Cloudflare/WAF browser-challenge interstitial pages.
|
||||
var challengeMarkers = []string{
|
||||
"just a moment...",
|
||||
"_cf_chl_opt",
|
||||
"challenge-platform",
|
||||
"cf-browser-verification",
|
||||
"cf_chl_",
|
||||
"enable javascript and cookies to continue",
|
||||
"checking your browser before accessing",
|
||||
"attention required! | cloudflare",
|
||||
"ddos protection by cloudflare",
|
||||
"/cdn-cgi/challenge-platform",
|
||||
}
|
||||
|
||||
// looksLikeChallengeResponse reports whether a response body is a WAF/anti-bot challenge page.
|
||||
func looksLikeChallengeResponse(body string) bool {
|
||||
lo := strings.ToLower(body)
|
||||
for _, m := range challengeMarkers {
|
||||
if strings.Contains(lo, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isExternalDemoSpecURL guards against following the SwaggerUIBundle default demo specs.
|
||||
func isExternalDemoSpecURL(raw string) bool {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(u.Hostname()) {
|
||||
case "petstore.swagger.io", "generator.swagger.io", "swagger.io", "www.swagger.io":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveReferenceURL resolves a (possibly relative) reference against pageURL.
|
||||
func resolveReferenceURL(pageURL, ref string) (string, bool) {
|
||||
ref = strings.TrimSpace(ref)
|
||||
if ref == "" {
|
||||
return "", false
|
||||
}
|
||||
base, err := url.Parse(pageURL)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
r, err := url.Parse(ref)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
resolved := base.ResolveReference(r)
|
||||
if resolved.Scheme == "" || resolved.Host == "" {
|
||||
return "", false
|
||||
}
|
||||
return resolved.String(), true
|
||||
}
|
||||
|
||||
// dedupeURLs removes duplicate and empty entries, preserving order.
|
||||
func dedupeURLs(in []string) []string {
|
||||
seen := make(map[string]bool, len(in))
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
if s == "" || seen[s] {
|
||||
continue
|
||||
}
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
+16
-10
@@ -1,11 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -17,27 +16,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
|
||||
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering endpoints.\n\n")
|
||||
printInfo("\n")
|
||||
printInfo("Gathering endpoints.\n\n")
|
||||
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
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)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
GenerateRequests(bodyBytes, client, nil)
|
||||
}
|
||||
},
|
||||
}
|
||||
+127
-180
@@ -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,57 @@ 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()
|
||||
|
||||
// progressPending is set while an unterminated progress line is on screen, so
|
||||
// the diagnostic helpers can clear it before printing.
|
||||
var progressPending bool
|
||||
|
||||
// writeProgress renders an overwriting progress line on stderr (the stream the
|
||||
// diagnostic helpers use, so clearing is reliable).
|
||||
func writeProgress(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, "\r\033[2K"+format, args...)
|
||||
progressPending = true
|
||||
}
|
||||
|
||||
// clearProgressLine erases a pending progress line so a diagnostic prints cleanly.
|
||||
func clearProgressLine() {
|
||||
if progressPending {
|
||||
fmt.Fprint(os.Stderr, "\r\033[2K")
|
||||
progressPending = false
|
||||
}
|
||||
}
|
||||
|
||||
func printInfo(format string, args ...interface{}) {
|
||||
clearProgressLine()
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
|
||||
func printWarn(format string, args ...interface{}) {
|
||||
clearProgressLine()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", yellow("[!]"), msg)
|
||||
}
|
||||
|
||||
func printErr(format string, args ...interface{}) {
|
||||
clearProgressLine()
|
||||
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 +83,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(specTitle, specDescription)
|
||||
} 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(specTitle, specDescription)
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg, tempLogger)
|
||||
}
|
||||
} else {
|
||||
if sc == 8899 {
|
||||
logJSON(specTitle, specDescription)
|
||||
} 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
-9
@@ -1,11 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -18,25 +18,37 @@ 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 {
|
||||
die("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
printInfo("\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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+109
-61
@@ -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
|
||||
@@ -53,31 +52,44 @@ var (
|
||||
userChoice string
|
||||
)
|
||||
|
||||
// MakeRequest returns the body bytes, body string, and status code. It wraps
|
||||
// MakeRequestFull for callers that do not need the Content-Type.
|
||||
func MakeRequest(client http.Client, method, target string, timeout int64, reqData io.Reader) ([]byte, string, int) {
|
||||
bodyBytes, bodyString, status, _ := MakeRequestFull(client, method, target, timeout, reqData)
|
||||
return bodyBytes, bodyString, status
|
||||
}
|
||||
|
||||
// MakeRequestFull also returns the response Content-Type. Taking body and
|
||||
// Content-Type from one response avoids a second request per URL.
|
||||
func MakeRequestFull(client http.Client, method, target string, timeout int64, reqData io.Reader) ([]byte, string, int, string) {
|
||||
if quiet {
|
||||
avoidDangerousRequests = "y"
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
return nil, "", 0
|
||||
return nil, "", 0, ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,14 +99,17 @@ 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
|
||||
}
|
||||
|
||||
@@ -138,73 +153,40 @@ 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)
|
||||
return nil, "", 0
|
||||
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
|
||||
return nil, "skipped", 1, ""
|
||||
} else {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
printErr("Error: response not received.\n%v", err)
|
||||
}
|
||||
return nil, "", 0
|
||||
return nil, "", 0, ""
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
bodyString := string(bodyBytes)
|
||||
contentTypeHeader := resp.Header.Get("Content-Type")
|
||||
|
||||
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
|
||||
var ct string
|
||||
bodyBytes, bodyString, requestStatus, ct = MakeRequestFull(client, method, redirect.Scheme+"://"+redirect.Host+redirect.Path, timeout, reqData)
|
||||
return bodyBytes, bodyString, requestStatus, ct
|
||||
}
|
||||
|
||||
requestStatus = resp.StatusCode
|
||||
depth = 0
|
||||
|
||||
return bodyBytes, bodyString, requestStatus
|
||||
return bodyBytes, bodyString, requestStatus, contentTypeHeader
|
||||
}
|
||||
|
||||
func CheckContentType(client http.Client, target string) string {
|
||||
u, _ := url.Parse(target)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// User-Agent handling
|
||||
if randomUserAgent {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
UserAgent = userAgents[rand.Intn(len(userAgents))]
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
} else if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err == context.DeadlineExceeded {
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
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.")
|
||||
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
|
||||
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
|
||||
} else {
|
||||
log.Error("Error: response not received.\n", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return resp.Header.Get("Content-Type")
|
||||
}
|
||||
|
||||
func CheckAndConfigureProxy() (client http.Client) {
|
||||
func CheckAndConfigureProxy() (client http.Client, replayClient *http.Client) {
|
||||
var proxyUrl *url.URL
|
||||
|
||||
transport := &http.Transport{}
|
||||
@@ -224,5 +206,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)
|
||||
}
|
||||
+12
-3
@@ -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: "2.2.0",
|
||||
Version: "2.8.2",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -64,12 +68,17 @@ 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.") // NEED TO RE-IMPLEMENT RATE LIMIT
|
||||
|
||||
+710
-296
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,25 @@
|
||||
module github.com/BishopFox/sj
|
||||
|
||||
go 1.22.5
|
||||
|
||||
toolchain go1.23.4
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/getkin/kin-openapi v0.132.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/getkin/kin-openapi v0.144.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/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/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/oasdiff/yaml v0.1.1 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.14 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
@@ -1,55 +1,56 @@
|
||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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.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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
|
||||
github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
||||
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
|
||||
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
|
||||
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/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/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/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
||||
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
||||
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
||||
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
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/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/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=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -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"
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user