mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-07 17:08:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e9c78d9a | ||
|
|
3f99ffb48c | ||
|
|
331bb1a05c | ||
|
|
3c2a3b807d | ||
|
|
7ef8abdb8e | ||
|
|
f1f9e19422 | ||
|
|
0d6776b758 | ||
|
|
8f4e4a5d9f | ||
|
|
55baa79785 | ||
|
|
5bdafb64d8 | ||
|
|
27b59354fc | ||
|
|
bee5088fcf |
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"]
|
||||
@@ -61,6 +61,18 @@ Description: This is a sample server Petstore server. You can find out more abo
|
||||
✓ 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
|
||||
@@ -229,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.
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, replayClient := CheckAndConfigureProxy()
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
@@ -81,7 +81,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)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+57
-19
@@ -44,7 +44,7 @@ var bruteCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
var allURLs []string
|
||||
u, err := url.Parse(swaggerURL)
|
||||
@@ -52,12 +52,13 @@ var bruteCmd = &cobra.Command{
|
||||
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)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, ".json", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "/", false)...)
|
||||
} else {
|
||||
endpointList, err := os.Open(endpointWordlist)
|
||||
if err != nil {
|
||||
@@ -68,7 +69,7 @@ var bruteCmd = &cobra.Command{
|
||||
scanner := bufio.NewScanner(endpointList)
|
||||
for scanner.Scan() {
|
||||
endpoint := scanner.Text()
|
||||
fullURL := target + endpoint
|
||||
fullURL := target + normalizedBasePath + endpoint
|
||||
allURLs = append(allURLs, fullURL)
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ var bruteCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -127,7 +128,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)
|
||||
|
||||
}
|
||||
@@ -137,7 +138,7 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -159,16 +160,10 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
if jsonContent, ok := ExtractJSONFromJSSpec(bodyBytes); ok {
|
||||
checkSpec := UnmarshalSpec(jsonContent)
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
printInfo("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
@@ -191,6 +186,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
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ var convertCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
@@ -30,7 +30,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
|
||||
|
||||
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 {
|
||||
@@ -44,7 +44,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
|
||||
}
|
||||
}
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
GenerateRequests(bodyBytes, client, nil)
|
||||
}
|
||||
},
|
||||
}
|
||||
+3
-3
@@ -29,13 +29,13 @@ This enables you to test specific API functions for common vulnerabilities or mi
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
fmt.Printf("\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 {
|
||||
@@ -50,7 +50,7 @@ This enables you to test specific API functions for common vulnerabilities or mi
|
||||
}
|
||||
|
||||
specBytes, _ := io.ReadAll(specFile)
|
||||
GenerateRequests(specBytes, client)
|
||||
GenerateRequests(specBytes, client, nil)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+69
-3
@@ -65,7 +65,7 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
}
|
||||
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
|
||||
@@ -232,7 +232,7 @@ func CheckContentType(client http.Client, target string) string {
|
||||
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{}
|
||||
@@ -252,5 +252,71 @@ func CheckAndConfigureProxy() (client http.Client) {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
return client
|
||||
|
||||
if replayProxy != "" {
|
||||
replayTransport := &http.Transport{}
|
||||
if insecure {
|
||||
replayTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
replayProxyUrl, err := url.Parse(replayProxy)
|
||||
if err != nil {
|
||||
die("Error parsing replay proxy URL: %v", err)
|
||||
}
|
||||
replayTransport.Proxy = http.ProxyURL(replayProxyUrl)
|
||||
replayClient = &http.Client{
|
||||
Transport: replayTransport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return client, replayClient
|
||||
}
|
||||
|
||||
func ReplayRequest(replayClient *http.Client, method, target string, timeout int64, reqData io.Reader) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequest(method, target, reqData)
|
||||
if err != nil {
|
||||
printWarn("Replay: error creating request for %s - %v", target, err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range Headers {
|
||||
delimIndex := strings.Index(Headers[i], ":")
|
||||
if delimIndex == -1 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(Headers[i][:delimIndex])
|
||||
value := strings.TrimSpace(Headers[i][delimIndex+1:])
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
if accept == "" {
|
||||
req.Header.Set("Accept", "application/json, text/html, */*")
|
||||
} else {
|
||||
req.Header.Set("Accept", accept)
|
||||
}
|
||||
|
||||
if method == "POST" {
|
||||
if contentType == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := replayClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
printWarn("Replay: error sending request to %s - %v", target, err)
|
||||
return
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[Replay] %s %s -> %d\n", method, target, resp.StatusCode)
|
||||
}
|
||||
+5
-1
@@ -9,11 +9,13 @@ 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
|
||||
@@ -51,7 +53,7 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
|
||||
printErr("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "2.5.1",
|
||||
Version: "2.7.0",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -69,12 +71,14 @@ func init() {
|
||||
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
|
||||
|
||||
+85
-9
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -37,7 +38,7 @@ type SchemaNode struct {
|
||||
AdditionalProperties *SchemaNode
|
||||
}
|
||||
|
||||
func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, replayClient *http.Client) {
|
||||
paths, ok := spec["paths"].(map[string]interface{})
|
||||
if !ok || paths == nil {
|
||||
die("Could not find any defined operations. Review the file manually.")
|
||||
@@ -49,6 +50,19 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
}
|
||||
slices.Sort(pathKeys)
|
||||
|
||||
// Snapshot CLI-supplied headers so per-endpoint Content-Type
|
||||
// mutations from EnforceSingleContentType don't leak across endpoints.
|
||||
userHeaders := append([]string(nil), Headers...)
|
||||
var userContentType string
|
||||
for _, h := range userHeaders {
|
||||
if strings.HasPrefix(strings.ToLower(h), "content-type:") {
|
||||
if idx := strings.Index(h, ":"); idx >= 0 {
|
||||
userContentType = strings.TrimSpace(h[idx+1:])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var errorDescriptions = make(map[any]string)
|
||||
for _, pathName := range pathKeys {
|
||||
pathItem := paths[pathName]
|
||||
@@ -68,6 +82,11 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
continue
|
||||
default:
|
||||
if opMap, ok := op.(map[string]interface{}); ok {
|
||||
// Reset header state so the previous endpoint's
|
||||
// Content-Type doesn't leak into this one.
|
||||
Headers = append([]string(nil), userHeaders...)
|
||||
contentType = userContentType
|
||||
|
||||
if responses, ok := opMap["responses"].(map[string]interface{}); ok {
|
||||
for status, respItem := range responses {
|
||||
if respMap, ok := respItem.(map[string]interface{}); ok {
|
||||
@@ -225,23 +244,42 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
if cType == "application/json" {
|
||||
bodyBytes, err := json.Marshal(example)
|
||||
if err == nil {
|
||||
bodyData = string(bodyBytes)
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: application/json\" -d '%s'", bodyBytes)
|
||||
}
|
||||
}
|
||||
if cType == "application/xml" || cType == "text/xml" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
xml := XmlFromObject(obj)
|
||||
bodyData = xml
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, xml)
|
||||
}
|
||||
}
|
||||
if cType == "application/x-www-form-urlencoded" || cType == "multipart/form-data" {
|
||||
if cType == "application/x-www-form-urlencoded" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
var formParts []string
|
||||
for k, v := range obj {
|
||||
formParts = append(formParts, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
formData := strings.Join(formParts, "&")
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, formData)
|
||||
bodyData = strings.Join(formParts, "&")
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, bodyData)
|
||||
}
|
||||
}
|
||||
if cType == "multipart/form-data" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
for k, v := range obj {
|
||||
_ = mw.WriteField(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
_ = mw.Close()
|
||||
bodyData = buf.String()
|
||||
// Replace the bare multipart/form-data Content-Type
|
||||
// (set earlier in the loop) with a boundary-aware one.
|
||||
EnforceSingleContentType(mw.FormDataContentType())
|
||||
for k, v := range obj {
|
||||
curl += fmt.Sprintf(" -F \"%s=%v\"", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,9 +303,8 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
switch os.Args[1] {
|
||||
case "automate":
|
||||
var postBodyData string
|
||||
if strings.ToLower(method) == "post" && strings.Contains(curl, "-d") {
|
||||
dataIndex := strings.Index(curl, "'")
|
||||
postBodyData = curl[dataIndex+1 : len(curl)-1]
|
||||
if strings.ToLower(method) == "post" {
|
||||
postBodyData = bodyData
|
||||
}
|
||||
|
||||
_, resp, sc := MakeRequest(client, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
|
||||
@@ -296,6 +333,9 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
if outputFormat == "console" {
|
||||
writeLog(sc, logURL.Path, strings.ToUpper(method), errorDescriptions[sc], resp[:tempResponsePreviewLength])
|
||||
}
|
||||
if replayClient != nil {
|
||||
ReplayRequest(replayClient, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if jsonResultsStringArray == nil {
|
||||
@@ -306,6 +346,9 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
if outputFormat == "console" {
|
||||
writeLog(sc, logURL.Path, strings.ToUpper(method), errorDescriptions[sc], resp[:tempResponsePreviewLength])
|
||||
}
|
||||
if replayClient != nil {
|
||||
ReplayRequest(replayClient, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
|
||||
}
|
||||
}
|
||||
|
||||
case "endpoints":
|
||||
@@ -533,7 +576,16 @@ func GenerateExample(node *SchemaNode) interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) {
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client, replayClient *http.Client) {
|
||||
// Swagger UI bundles serve the spec wrapped in JavaScript (e.g.
|
||||
// swagger-ui-init.js). Detect that and unwrap before parsing so the
|
||||
// downstream YAML/JSON unmarshal succeeds.
|
||||
if looksLikeJSSpec(bodyBytes) {
|
||||
if extracted, ok := ExtractJSONFromJSSpec(bodyBytes); ok {
|
||||
bodyBytes = extracted
|
||||
}
|
||||
}
|
||||
|
||||
// Ingests the specification file
|
||||
spec := SafelyUnmarshalSpec(bodyBytes)
|
||||
|
||||
@@ -650,7 +702,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client) {
|
||||
}
|
||||
|
||||
// Reviews all defined API routes and builds requests as defined
|
||||
BuildRequestsFromPaths(spec, client)
|
||||
BuildRequestsFromPaths(spec, client, replayClient)
|
||||
}
|
||||
|
||||
func ResolveRef(spec map[string]interface{}, ref string) map[string]interface{} {
|
||||
@@ -801,6 +853,30 @@ func SetScheme(swaggerURL string) (scheme string) {
|
||||
return scheme
|
||||
}
|
||||
|
||||
// looksLikeJSSpec is a cheap content sniff so we only run the JS-extraction
|
||||
// regex against bodies that plausibly contain JavaScript. Normal JSON
|
||||
// (starts with `{`/`[`) and YAML (starts with `openapi:`/`swagger:`/`---`/`#`)
|
||||
// fall through unchanged.
|
||||
func looksLikeJSSpec(b []byte) bool {
|
||||
if strings.HasSuffix(strings.ToLower(swaggerURL), ".js") ||
|
||||
strings.HasSuffix(strings.ToLower(localFile), ".js") ||
|
||||
strings.ToLower(format) == "js" {
|
||||
return true
|
||||
}
|
||||
trimmed := bytes.TrimLeft(b, " \t\r\n")
|
||||
prefixes := [][]byte{
|
||||
[]byte("var "), []byte("let "), []byte("const "),
|
||||
[]byte("(function"), []byte("window."),
|
||||
[]byte("//"), []byte("/*"),
|
||||
}
|
||||
for _, p := range prefixes {
|
||||
if bytes.HasPrefix(trimmed, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
|
||||
|
||||
var doc map[string]interface{}
|
||||
|
||||
Reference in new issue
Block a user