mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-07 17:08:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdea5822e8 | ||
|
|
7bb228d81c | ||
|
|
3258ca341f | ||
|
|
58d4e91760 | ||
|
|
1f5a946011 | ||
|
|
143fdfda1b | ||
|
|
a33eef47f4 | ||
|
|
4c466112ee | ||
|
|
1195ff7fbf | ||
|
|
bd3265d0a4 | ||
|
|
d9fac2e9fe | ||
|
|
10fe0376c0 | ||
|
|
baf4ef6c51 | ||
|
|
8e2e5fbebc | ||
|
|
a77f9f46bb | ||
|
|
5554e2bd30 |
No files matched your search
+18
-13
@@ -45,20 +45,25 @@ 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)
|
||||
} else {
|
||||
if quiet {
|
||||
autoApplyBasicAuth = "n"
|
||||
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
} else {
|
||||
fmt.Println("Basic Authentication is accepted. Supply a username and password? (y/N)")
|
||||
fmt.Scanln(&autoApplyBasicAuth)
|
||||
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
|
||||
if autoApplyBasicAuth == "y" {
|
||||
fmt.Printf("Enter a username.")
|
||||
fmt.Scanln(&basicAuthUser)
|
||||
fmt.Printf("Enter a password.")
|
||||
fmt.Scanln(&basicAuthPass)
|
||||
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
|
||||
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
|
||||
log.Infof("Using %s as the Basic Auth value.", basicAuthString)
|
||||
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
|
||||
} else {
|
||||
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
|
||||
}
|
||||
}
|
||||
case "bearer":
|
||||
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
var getAccessibleEndpoints bool
|
||||
var outputFormat string
|
||||
var responsePreviewLength int
|
||||
var testString string
|
||||
var verbose bool
|
||||
|
||||
var automateCmd = &cobra.Command{
|
||||
@@ -43,6 +45,12 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.Parse("2006-01-02", customDate)
|
||||
if err != nil {
|
||||
fmt.Println("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
@@ -74,6 +82,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
func init() {
|
||||
automateCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "console", "The output format. Only 'console' (default) and 'json' are supported at the moment.")
|
||||
automateCmd.PersistentFlags().BoolVar(&getAccessibleEndpoints, "get-accessible-endpoints", false, "Only output the accessible endpoints (those that return a 200 status code).")
|
||||
automateCmd.PersistentFlags().StringVar(&testString, "test-string", "bishopfox", "The string to use when testing endpoints with string values.")
|
||||
automateCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose mode, which shows a preview of each response.")
|
||||
automateCmd.PersistentFlags().IntVar(&responsePreviewLength, "response-preview-length", 50, "sets the response preview length when using verbose output.")
|
||||
|
||||
|
||||
+25
-10
@@ -26,6 +26,7 @@ var endpointWordlist string
|
||||
var prefixDirs []string = []string{"", "/swagger", "/swagger/docs", "/swagger/latest", "/swagger/v1", "/swagger/v2", "/swagger/v3", "/swagger/static", "/swagger/ui", "/swagger-ui", "/api-docs", "/api-docs/v1", "/api-docs/v2", "/apidocs", "/api", "/api/v1", "/api/v2", "/api/v3", "/v1", "/v2", "/v3", "/doc", "/docs", "/docs/swagger", "/docs/swagger/v1", "/docs/swagger/v2", "/docs/swagger-ui", "/docs/swagger-ui/v1", "/docs/swagger-ui/v2", "/docs/v1", "/docs/v2", "/docs/v3", "/public", "/redoc"}
|
||||
var jsonEndpoints []string = []string{"", "/index", "/swagger", "/swagger-ui", "/swagger-resources", "/swagger-config", "/openapi", "/api", "/api-docs", "/apidocs", "/v1", "/v2", "/v3", "/doc", "/docs", "/apispec", "/apispec_1", "/api-merged"}
|
||||
var javascriptEndpoints []string = []string{"/swagger-ui-init", "/swagger-ui-bundle", "/swagger-ui-standalone-preset", "/swagger-ui", "/swagger-ui.min", "/swagger-ui-es-bundle-core", "/swagger-ui-es-bundle", "/swagger-ui-standalone-preset", "/swagger-ui-layout", "/swagger-ui-plugins"}
|
||||
var priorityURLs []string = []string{"/swagger.json", "/openapi.json", "/api-docs", "/swagger", "/docs", "/api/swagger.json", "/api/openapi.json", "/api-docs/swagger.json", "/api/schema/", "/webjars/swagger-ui/index.html", "/API/swagger/ui/index", "/swagger/ui/index", "/v2/swagger.json", "/v2/openapi.json", "/v2/api-docs", "/v3/api-docs", "/v3/openapi.json", "/public/api-merged.json", "/analytics/v1/swagger", "/api.json", "/api/4.0/swagger.json", "/api/api-doc/openapi.json", "/api/api-doc/openapi.yaml", "/api/doc.json", "/api/docs.json", "/api/swagger", "/api/swagger/ui/index", "/api/v1/swagger", "/api/v2/api-docs", "/api/v2/openapi.json", "/api/v2/swagger.json", "/api/v3/api-docs", "/api/v3/apispec", "/api/workorder/openapi.json", "/apidocs", "/audiences/v1/swagger", "/audittrail/v1/swagger", "/certification/v1/swagger", "/citrixapi/store/swagger.json", "/conferencetool/v1/swagger", "/course/v1/swagger", "/dcl_swagger.yaml", "/doc/doc.json", "/doc/swagger.json", "/docs/swagger.json", "/docs/v1/swagger.json", "/ecommerce/v1/swagger", "/enrollment/v1/swagger", "/externalids/v1/swagger", "/impact/v1/swagger", "/learn/v1/swagger", "/learningplan/v1/swagger", "/manage/v1/swagger", "/management/info", "/marketplace/v1/swagger", "/messenger/v1/swagger", "/notifications/v1/swagger", "/openapi", "/openapi/spec.json", "/otj/v1/swagger", "/pages/v1/swagger", "/poweruser/v1/swagger", "/proctoring/v1/swagger", "/report/v1/swagger", "/swagger-ui/index.html", "/swagger-ui/openapi.json", "/swagger.yaml", "/swagger/0.1.0/swagger.json", "/swagger/doc.json", "/swagger/latest/swagger.json", "/swagger/swagger.json", "/swagger/test/swagger.json", "/swagger/ui/index.html", "/swagger/v1/openapiv2.json", "/swagger/v1/swagger.json", "/swagger/v2/swagger.json", "/swagger/v4/swagger.json", "/v1/openapi.json", "/v1/swagger", "/v1/swagger.json", "/swagger/docs/v1", "/swagger/docs/v1.json", "/Api/swagger/docs/v1", "/swagger/v1/swagger.json", "/api/api-docs/swagger.json", "/api/docs/", "/api/docs", "/swagger-ui"}
|
||||
|
||||
var bruteCmd = &cobra.Command{
|
||||
Use: "brute",
|
||||
@@ -54,10 +55,11 @@ var bruteCmd = &cobra.Command{
|
||||
}
|
||||
target := u.Scheme + "://" + u.Host
|
||||
if endpointWordlist == "" {
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "")...)
|
||||
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js")...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json")...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/")...)
|
||||
allURLs = append(allURLs, makeURLs(target, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/", false)...)
|
||||
} else {
|
||||
endpointList, err := os.Open(endpointWordlist)
|
||||
if err != nil {
|
||||
@@ -114,21 +116,31 @@ 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("No definition file found for:\t%s\n", swaggerURL)
|
||||
log.Errorf("\nNo definition file found for:\t%s\n", swaggerURL)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func makeURLs(target string, endpoints []string, fileExtension string) []string {
|
||||
func makeURLs(target string, endpoints []string, fileExtension string, skipPrefix bool) []string {
|
||||
urls := []string{}
|
||||
for _, dir := range prefixDirs {
|
||||
if !skipPrefix {
|
||||
for _, dir := range prefixDirs {
|
||||
for _, endpoint := range endpoints {
|
||||
if dir == "" && endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + dir + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, endpoint := range endpoints {
|
||||
if dir == "" && endpoint == "" {
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + dir + endpoint + fileExtension
|
||||
targetURL := target + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
|
||||
}
|
||||
}
|
||||
return urls
|
||||
@@ -154,6 +166,9 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
|
||||
log.Infof("Gathering endpoints.\n\n")
|
||||
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
GenerateRequests(bodyBytes, client)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
|
||||
+44
-41
@@ -34,12 +34,14 @@ 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)
|
||||
var err error
|
||||
file, err = os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Output file does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
tempLogger.SetOutput(file)
|
||||
}
|
||||
|
||||
@@ -54,52 +56,53 @@ func writeLog(sc int, target, method, errorMsg, response string) {
|
||||
}
|
||||
|
||||
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 {
|
||||
switch sc {
|
||||
case 0:
|
||||
logVerboseBad(sc, target, method, response, tempLogger)
|
||||
case 1:
|
||||
logDangerous(target, method, tempLogger)
|
||||
case 200:
|
||||
logVerboseAccessible(sc, target, method, response, tempLogger)
|
||||
case 301:
|
||||
logVerboseRedirect(sc, target, method, response, tempLogger)
|
||||
case 302:
|
||||
logVerboseRedirect(sc, target, method, response, tempLogger)
|
||||
case 401:
|
||||
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
|
||||
case 403:
|
||||
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
|
||||
case 404:
|
||||
logNotFound(sc, target, method, errorMsg, tempLogger)
|
||||
case 8899:
|
||||
logVerboseJSON(specTitle, specDescription)
|
||||
default:
|
||||
logVerboseManual(sc, target, method, errorMsg, response, tempLogger)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
switch sc {
|
||||
case 0:
|
||||
logBad(sc, target, method, tempLogger)
|
||||
case 1:
|
||||
logDangerous(target, method, tempLogger)
|
||||
case 200:
|
||||
logAccessible(sc, target, method, tempLogger)
|
||||
case 301:
|
||||
logRedirect(sc, target, method, tempLogger)
|
||||
case 302:
|
||||
logRedirect(sc, target, method, tempLogger)
|
||||
case 401:
|
||||
logUnauth(sc, target, method, errorMsg, tempLogger)
|
||||
case 403:
|
||||
logUnauth(sc, target, method, errorMsg, tempLogger)
|
||||
case 404:
|
||||
logNotFound(sc, target, method, errorMsg, tempLogger)
|
||||
case 8899:
|
||||
logJSON(specTitle, specDescription)
|
||||
default:
|
||||
logManual(sc, target, method, errorMsg, tempLogger)
|
||||
}
|
||||
}
|
||||
responsePreviewLength = tempResponsePreviewLength
|
||||
file.Close()
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string, logger *log.Logger) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -22,6 +23,12 @@ This enables you to test specific API functions for common vulnerabilities or mi
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.Parse("2006-01-02", customDate)
|
||||
if err != nil {
|
||||
fmt.Println("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
+35
-21
@@ -20,6 +20,7 @@ var (
|
||||
avoidDangerousRequests string
|
||||
contentType string
|
||||
dangerousStrings []string = []string{"add", "block", "build", "buy", "change", "clear", "create", "delete", "deploy", "destroy", "drop", "edit", "emergency", "erase", "execute", "insert", "modify", "order", "overwrite", "pause", "purchase", "rebuild", "remove", "replace", "reset", "restart", "revoke", "run", "sell", "send", "set", "start", "stop", "update", "upload", "write"}
|
||||
depth int = 0
|
||||
Headers []string
|
||||
requestStatus int
|
||||
riskSurveyed bool = false
|
||||
@@ -59,7 +60,11 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
}
|
||||
|
||||
// Handling of dangerous keywords
|
||||
u, _ := url.Parse(target)
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || u == nil {
|
||||
log.Printf("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) {
|
||||
@@ -87,8 +92,11 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequest(method, target, reqData)
|
||||
if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
if err != nil {
|
||||
if err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
}
|
||||
return nil, "", 0
|
||||
}
|
||||
|
||||
for i := range Headers {
|
||||
@@ -98,22 +106,19 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
continue
|
||||
}
|
||||
|
||||
key := Headers[i][:delimIndex]
|
||||
value := Headers[i][delimIndex+1:]
|
||||
key := strings.TrimSpace(Headers[i][:delimIndex])
|
||||
value := strings.TrimSpace(Headers[i][delimIndex+1:])
|
||||
|
||||
if len(Headers) == 2 {
|
||||
if key == "User-Agent" {
|
||||
UserAgent = value
|
||||
}
|
||||
if key == "Content-Type" {
|
||||
contentType = value
|
||||
}
|
||||
if key == "Accept" {
|
||||
accept = value
|
||||
}
|
||||
} else {
|
||||
req.Header.Set(key, value)
|
||||
if key == "User-Agent" {
|
||||
UserAgent = value
|
||||
}
|
||||
if key == "Content-Type" {
|
||||
contentType = value
|
||||
}
|
||||
if key == "Accept" {
|
||||
accept = value
|
||||
}
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// User-Agent handling
|
||||
@@ -159,26 +164,35 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") {
|
||||
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") && depth < 10 {
|
||||
depth += 1
|
||||
redirect, _ := resp.Location()
|
||||
bodyBytes, bodyString, requestStatus = MakeRequest(client, method, redirect.Scheme+"://"+redirect.Host+redirect.Path, timeout, reqData)
|
||||
return bodyBytes, bodyString, requestStatus
|
||||
}
|
||||
|
||||
requestStatus = resp.StatusCode
|
||||
depth = 0
|
||||
|
||||
return bodyBytes, bodyString, requestStatus
|
||||
}
|
||||
|
||||
func CheckContentType(client http.Client, target string) string {
|
||||
u, _ := url.Parse(target)
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || u == nil {
|
||||
log.Printf("Error parsing URL '%s': %v - skipping request.", target, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequest("GET", target, nil)
|
||||
if err != nil && err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
if err != nil {
|
||||
if err != context.Canceled && err != io.EOF {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// User-Agent handling
|
||||
|
||||
+7
-1
@@ -7,6 +7,9 @@ import (
|
||||
|
||||
var apiTarget string
|
||||
var basePath string
|
||||
var customDate string
|
||||
var customEmail string
|
||||
var customURL string
|
||||
var format string
|
||||
var insecure bool
|
||||
var localFile string
|
||||
@@ -49,7 +52,7 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
|
||||
log.Error("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "2.0.3",
|
||||
Version: "2.3.2",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -64,6 +67,9 @@ 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().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.")
|
||||
|
||||
+294
-211
@@ -3,7 +3,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -22,103 +21,14 @@ var jsonVerboseResultArray []VerboseResult
|
||||
var specTitle string
|
||||
var specDescription string
|
||||
|
||||
func PrintSpecInfo(spec map[string]interface{}) {
|
||||
info, ok := spec["info"].(map[string]interface{})
|
||||
if !ok || info == nil {
|
||||
log.Info("No information defined in the documentation.")
|
||||
} else {
|
||||
title, ok := info["title"].(string)
|
||||
if ok && title != "" {
|
||||
if outputFormat == "json" {
|
||||
specTitle = title
|
||||
} else {
|
||||
fmt.Printf("Title: %s\n", title)
|
||||
}
|
||||
}
|
||||
|
||||
description, ok := info["description"].(string)
|
||||
if ok && description != "" {
|
||||
if outputFormat == "json" {
|
||||
specDescription = description
|
||||
} else {
|
||||
fmt.Printf("Description: %s\n", description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) {
|
||||
// Ingests the specification file
|
||||
spec := SafelyUnmarshalSpec(bodyBytes)
|
||||
|
||||
// Checks defined security schemes and prompts for authentication
|
||||
CheckSecuritySchemes(spec)
|
||||
|
||||
u, _ := url.Parse(swaggerURL)
|
||||
|
||||
// Gets the target server and base path from the specification file
|
||||
if apiTarget == "" {
|
||||
if v, ok := spec["swagger"].(string); ok && strings.HasPrefix(v, "2") {
|
||||
// Swagger (v2)
|
||||
host, _ := spec["host"].(string)
|
||||
bp, _ := spec["basePath"].(string)
|
||||
if bp != "" {
|
||||
basePath = bp
|
||||
}
|
||||
|
||||
if host != "" && strings.Contains(host, "://") {
|
||||
apiTarget = host
|
||||
} else {
|
||||
if host != "" {
|
||||
apiTarget = u.Scheme + "://" + host
|
||||
}
|
||||
}
|
||||
} else if v, ok := spec["openapi"].(string); ok && strings.HasPrefix(v, "3") {
|
||||
// OpenAPI (v3)
|
||||
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
|
||||
if len(servers) > 1 {
|
||||
if !quiet && (os.Args[1] != "endpoints") && apiTarget == "" {
|
||||
log.Warn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
|
||||
for i := range servers {
|
||||
if srv, ok := servers[i].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
fmt.Println(serverURL)
|
||||
} else {
|
||||
fmt.Println(apiTarget + serverURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if srv, ok := servers[0].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
apiTarget = serverURL
|
||||
} else {
|
||||
basePath = serverURL
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original host at the target if no server found from specification.
|
||||
if apiTarget == "" {
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
if os.Args[1] != "endpoints" {
|
||||
// Prints Title/Description/Version values if they exist
|
||||
PrintSpecInfo(spec)
|
||||
}
|
||||
|
||||
// Reviews all defined API routes and builds requests as defined
|
||||
BuildRequestsFromPaths(spec, client)
|
||||
type SchemaNode struct {
|
||||
Type string
|
||||
Properties map[string]*SchemaNode
|
||||
Items *SchemaNode
|
||||
Required map[string]bool
|
||||
Enum []interface{}
|
||||
Example interface{}
|
||||
Ref string
|
||||
}
|
||||
|
||||
func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
@@ -167,7 +77,7 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
pType = schema["type"].(string)
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if pType == "string" && pMap["name"] != "version" {
|
||||
pValue = "bishopfox"
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
@@ -198,7 +108,7 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
} else if propertyType, ok := propertyName["type"].(string); ok {
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if propertyType == "string" && propertyName["name"] != "version" {
|
||||
pValue = "bishopfox"
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
@@ -223,7 +133,7 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
} else if pType, ok := pMap["type"].(string); ok {
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if pType == "string" && pMap["name"] != "version" {
|
||||
pValue = "bishopfox"
|
||||
pValue = testString
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
@@ -267,94 +177,19 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
|
||||
if ct, ok := contentTypes[cType].(map[string]interface{}); ok {
|
||||
if schema, ok := ct["schema"].(map[string]interface{}); ok {
|
||||
if ref, ok := schema["$ref"].(string); ok {
|
||||
if components, ok := spec["components"].(map[string]interface{}); ok {
|
||||
if schemas, ok := components["schemas"].(map[string]interface{}); ok {
|
||||
schemaName := strings.TrimPrefix(ref, "#/components/schemas/")
|
||||
if s, ok := schemas[schemaName].(map[string]interface{}); ok {
|
||||
if properties, ok := s["properties"].(map[string]interface{}); ok {
|
||||
expanded := ExpandSchema(spec, schema, map[string]bool{})
|
||||
example := GenerateExample(expanded)
|
||||
|
||||
for propertyItem := range properties {
|
||||
var pValue string
|
||||
if propertyName, ok := properties[propertyItem].(map[string]interface{}); ok {
|
||||
if propertyType, ok := propertyName["type"].(string); ok {
|
||||
switch propertyType {
|
||||
case "string":
|
||||
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
|
||||
if propertyName["name"] != "version" {
|
||||
pValue = "bishopfox"
|
||||
} else {
|
||||
pValue = "1"
|
||||
}
|
||||
case "int":
|
||||
pValue = "1"
|
||||
default:
|
||||
pValue = "1"
|
||||
}
|
||||
|
||||
switch cType {
|
||||
case "application/json":
|
||||
if strings.Contains(curl, "-d") && strings.Contains(curl, "application/json") {
|
||||
bodyData = ""
|
||||
curl = strings.TrimSuffix(curl, "}'")
|
||||
if propertyType == "string" {
|
||||
bodyData += fmt.Sprintf(",\"%s\":\"%s\"}'", propertyItem, pValue)
|
||||
} else {
|
||||
bodyData += fmt.Sprintf(",\"%s\":%s}'", propertyItem, pValue)
|
||||
}
|
||||
curl += bodyData
|
||||
} else if !strings.Contains(curl, "Content-Type") {
|
||||
if propertyType == "string" {
|
||||
bodyData += fmt.Sprintf("{\"%s\":\"%s\"}", propertyItem, pValue)
|
||||
} else {
|
||||
bodyData += fmt.Sprintf("{\"%s\":%s}", propertyItem, pValue)
|
||||
}
|
||||
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", contentType, bodyData)
|
||||
}
|
||||
case "application/xml", "text/xml":
|
||||
bodyData = ""
|
||||
type Element struct {
|
||||
XMLName xml.Name
|
||||
Content any `xml:",chardata"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Elements []Element `xml:",any"`
|
||||
}
|
||||
|
||||
var elements []Element
|
||||
elements = append(elements, Element{
|
||||
XMLName: xml.Name{Local: propertyItem},
|
||||
Content: pValue,
|
||||
})
|
||||
|
||||
root := Root{
|
||||
Elements: elements,
|
||||
}
|
||||
|
||||
xmlData, err := xml.Marshal(root)
|
||||
if err != nil {
|
||||
log.Warn("Error marshalling XML data.")
|
||||
}
|
||||
|
||||
if strings.Contains(curl, "-d") && strings.Contains(curl, "xml") {
|
||||
curl = strings.TrimSuffix(curl, "</root>'")
|
||||
bodyData = strings.TrimSuffix(bodyData, "</root>'")
|
||||
bodyData += strings.TrimPrefix(string(xmlData), "<root>")
|
||||
curl += bodyData + "'"
|
||||
} else if !strings.Contains(curl, "Content-Type") {
|
||||
bodyData = string(xmlData)
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", contentType, bodyData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cType == "application/json" {
|
||||
bodyBytes, err := json.Marshal(example)
|
||||
if err == nil {
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: application/json\" -d '%s'\"", bodyBytes)
|
||||
}
|
||||
}
|
||||
if cType == "application/xml" || cType == "text/xml" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
xml := XmlFromObject(obj)
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, xml)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,7 +198,11 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
logURL, _ := url.Parse(targetURL)
|
||||
logURL, parseErr := url.Parse(targetURL)
|
||||
if parseErr != nil || logURL == nil {
|
||||
log.Printf("Error parsing URL '%s': %v - skipping endpoint.", targetURL, parseErr)
|
||||
continue
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "automate":
|
||||
var postBodyData string
|
||||
@@ -454,6 +293,239 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
newContentType = strings.TrimSpace(newContentType)
|
||||
|
||||
// Remove old 'Content-Type' header
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(h), "content-type:")
|
||||
})
|
||||
|
||||
Headers = append(Headers, "Content-Type: "+newContentType)
|
||||
|
||||
// Remove empty elements to avoid repetitions of "-H ''"
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.TrimSpace(h) == ""
|
||||
})
|
||||
}
|
||||
|
||||
func ExpandSchema(
|
||||
spec map[string]interface{},
|
||||
schema map[string]interface{},
|
||||
visited map[string]bool,
|
||||
) *SchemaNode {
|
||||
if schema == nil {
|
||||
return &SchemaNode{Type: "object"}
|
||||
}
|
||||
|
||||
if ref, ok := schema["$ref"].(string); ok {
|
||||
if visited[ref] {
|
||||
return &SchemaNode{Type: "object"} // break cycle
|
||||
}
|
||||
visited[ref] = true
|
||||
|
||||
resolved := ResolveRef(spec, ref)
|
||||
if resolved == nil {
|
||||
return &SchemaNode{Type: "object"}
|
||||
}
|
||||
return ExpandSchema(spec, resolved, visited)
|
||||
}
|
||||
|
||||
node := &SchemaNode{
|
||||
Properties: map[string]*SchemaNode{},
|
||||
Required: map[string]bool{},
|
||||
}
|
||||
|
||||
if t, ok := schema["type"].(string); ok {
|
||||
node.Type = t
|
||||
}
|
||||
|
||||
if props, ok := schema["properties"].(map[string]interface{}); ok {
|
||||
for name, raw := range props {
|
||||
if m, ok := raw.(map[string]interface{}); ok {
|
||||
node.Properties[name] = ExpandSchema(spec, m, visited)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if items, ok := schema["items"].(map[string]interface{}); ok {
|
||||
node.Items = ExpandSchema(spec, items, visited)
|
||||
}
|
||||
|
||||
if allOf, ok := schema["allOf"].([]interface{}); ok {
|
||||
merged := &SchemaNode{Type: "object", Properties: map[string]*SchemaNode{}}
|
||||
for _, entry := range allOf {
|
||||
if m, ok := entry.(map[string]interface{}); ok {
|
||||
sub := ExpandSchema(spec, m, visited)
|
||||
for k, v := range sub.Properties {
|
||||
merged.Properties[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func GenerateExample(node *SchemaNode) interface{} {
|
||||
switch node.Type {
|
||||
case "object", "":
|
||||
obj := map[string]interface{}{}
|
||||
for k, v := range node.Properties {
|
||||
if strings.Contains(strings.ToLower(k), "date") {
|
||||
obj[k] = customDate
|
||||
} else if strings.Contains(strings.ToLower(k), "url") {
|
||||
obj[k] = customURL
|
||||
} else if strings.Contains(strings.ToLower(k), "email") {
|
||||
obj[k] = customEmail
|
||||
} else {
|
||||
obj[k] = GenerateExample(v)
|
||||
}
|
||||
}
|
||||
return obj
|
||||
case "array":
|
||||
if node.Items != nil {
|
||||
return []interface{}{GenerateExample(node.Items)}
|
||||
}
|
||||
return []interface{}{}
|
||||
case "string":
|
||||
return testString
|
||||
case "integer", "number":
|
||||
return 1
|
||||
case "boolean":
|
||||
return true
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) {
|
||||
// Ingests the specification file
|
||||
spec := SafelyUnmarshalSpec(bodyBytes)
|
||||
|
||||
// Checks defined security schemes and prompts for authentication
|
||||
CheckSecuritySchemes(spec)
|
||||
|
||||
u, parseErr := url.Parse(swaggerURL)
|
||||
if parseErr != nil {
|
||||
u = &url.URL{}
|
||||
}
|
||||
|
||||
// Gets the target server and base path from the specification file
|
||||
if apiTarget == "" {
|
||||
if v, ok := spec["swagger"].(string); ok && strings.HasPrefix(v, "2") {
|
||||
// Swagger (v2)
|
||||
host, _ := spec["host"].(string)
|
||||
bp, _ := spec["basePath"].(string)
|
||||
if bp == "/" {
|
||||
basePath = ""
|
||||
} else if bp != "" {
|
||||
basePath = bp
|
||||
}
|
||||
|
||||
if host != "" && strings.Contains(host, "://") {
|
||||
apiTarget = host
|
||||
} else {
|
||||
if host != "" {
|
||||
apiTarget = u.Scheme + "://" + host
|
||||
}
|
||||
}
|
||||
} else if v, ok := spec["openapi"].(string); ok && strings.HasPrefix(v, "3") {
|
||||
// OpenAPI (v3)
|
||||
if servers, ok := spec["servers"].([]interface{}); ok && len(servers) > 0 {
|
||||
if len(servers) > 1 {
|
||||
if !quiet && (os.Args[1] != "endpoints") && apiTarget == "" {
|
||||
log.Warn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
|
||||
for i := range servers {
|
||||
if srv, ok := servers[i].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
fmt.Println(serverURL)
|
||||
} else {
|
||||
fmt.Println(apiTarget + serverURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if srv, ok := servers[0].(map[string]interface{}); ok {
|
||||
if serverURL, ok := srv["url"].(string); ok {
|
||||
if strings.Contains(serverURL, "://") {
|
||||
apiTarget = serverURL
|
||||
} else if serverURL == "/" {
|
||||
basePath = ""
|
||||
} else {
|
||||
basePath = serverURL
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original host at the target if no server found from specification.
|
||||
if apiTarget == "" {
|
||||
apiTarget = u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
if os.Args[1] != "endpoints" {
|
||||
// Prints Title/Description/Version values if they exist
|
||||
PrintSpecInfo(spec)
|
||||
}
|
||||
|
||||
// Reviews all defined API routes and builds requests as defined
|
||||
BuildRequestsFromPaths(spec, client)
|
||||
}
|
||||
|
||||
func ResolveRef(spec map[string]interface{}, ref string) map[string]interface{} {
|
||||
if !strings.HasPrefix(ref, "#") {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(ref[2:], "/")
|
||||
var cur interface{} = spec
|
||||
|
||||
for _, p := range parts {
|
||||
m, ok := cur.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cur = m[p]
|
||||
}
|
||||
|
||||
resolved, _ := cur.(map[string]interface{})
|
||||
return resolved
|
||||
}
|
||||
|
||||
func PrintSpecInfo(spec map[string]interface{}) {
|
||||
info, ok := spec["info"].(map[string]interface{})
|
||||
if !ok || info == nil {
|
||||
log.Info("No information defined in the documentation.")
|
||||
} else {
|
||||
title, ok := info["title"].(string)
|
||||
if ok && title != "" {
|
||||
if outputFormat == "json" {
|
||||
specTitle = title
|
||||
} else {
|
||||
fmt.Printf("Title: %s\n", title)
|
||||
}
|
||||
}
|
||||
|
||||
description, ok := info["description"].(string)
|
||||
if ok && description != "" {
|
||||
if outputFormat == "json" {
|
||||
specDescription = description
|
||||
} else {
|
||||
fmt.Printf("Description: %s\n", description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetScheme(swaggerURL string) (scheme string) {
|
||||
if strings.HasPrefix(swaggerURL, "http://") {
|
||||
scheme = "http"
|
||||
@@ -465,6 +537,17 @@ func SetScheme(swaggerURL string) (scheme string) {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
/*
|
||||
TrimHostScheme trims the scheme from the provided URL if the '-T' flag is supplied to sj.
|
||||
*/
|
||||
@@ -483,29 +566,29 @@ func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
|
||||
return host
|
||||
}
|
||||
|
||||
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
|
||||
func XmlFromObject(obj map[string]interface{}) string {
|
||||
var b strings.Builder
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
|
||||
os.Exit(1)
|
||||
for k, v := range obj {
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
b.WriteString("<" + k + ">")
|
||||
b.WriteString(XmlFromObject(val))
|
||||
b.WriteString("</" + k + ">")
|
||||
case []interface{}:
|
||||
for _, item := range val {
|
||||
b.WriteString("<" + k + ">")
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
b.WriteString(XmlFromObject(m))
|
||||
} else {
|
||||
b.WriteString(XmlFromObject(m))
|
||||
}
|
||||
b.WriteString("</" + k + ">")
|
||||
}
|
||||
default:
|
||||
b.WriteString(fmt.Sprintf("<%s>%v</%s>", k, val, k))
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
newContentType = strings.TrimSpace(newContentType)
|
||||
|
||||
// Remove old 'Content-Type' header
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(h), "content-type:")
|
||||
})
|
||||
|
||||
Headers = append(Headers, "Content-Type: "+newContentType)
|
||||
|
||||
// Remove empty elements to avoid repetitions of "-H ''"
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.TrimSpace(h) == ""
|
||||
})
|
||||
return b.String()
|
||||
}
|
||||
Reference in new issue
Block a user