Compare commits

...
13 Commits
Author SHA1 Message Date
Tony West 4f256efca6 Incremented version string 2026-09-11 21:44:23 -04:00
Tony West 48c4d83f54 fix: Moved remaining diagnostic output to stderr so stdout stays pipeable 2026-09-11 21:44:15 -04:00
Tony West 365f68696f Merge pull request #59 from emilloc/fix/auth-stderr
fix: send authentication diagnostics to stderr
2026-09-11 21:36:58 -04:00
rootkid fbf484c3a1 fix: send authentication diagnostics to stderr 2026-09-10 18:56:14 -06:00
Tony West bc708d69f8 Incremented version string 2026-08-24 17:34:11 -04:00
Tony West 56346e759e Merge pull request #58 from BishopFox/dependabot/go_modules/github.com/getkin/kin-openapi-0.144.0
Bump github.com/getkin/kin-openapi from 0.132.0 to 0.144.0
2026-08-24 17:33:38 -04:00
dependabot[bot] a69b4dcae1 Bump github.com/getkin/kin-openapi from 0.132.0 to 0.144.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.132.0 to 0.144.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.132.0...v0.144.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.144.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-19 17:26:47 +00:00
Tony West 7d9fa54a58 Incremented version string 2026-07-22 08:52:23 -04:00
Tony West 09a1183e6f Added more discoverability features and basic WAF detection to the brute command 2026-07-22 08:52:05 -04:00
Tony West 90e9c78d9a Incremented version string 2026-07-09 11:30:07 -04:00
Tony West 3f99ffb48c Added docker support for releases 2026-07-09 11:29:51 -04:00
Tony West 331bb1a05c Incremented version string 2026-05-13 19:31:55 -04:00
Tony West 3c2a3b807d added fix to auto-extract JS-wrapped specs in automate/endpoints/prepare; fixed POST body leak, Content-Type carryover, and multipart boundary issues 2026-05-13 19:31:22 -04:00
16 changed files with 644 additions and 192 deletions

No files matched your search

+18
View File
@@ -30,6 +30,24 @@ archives:
- goos: windows
format: zip
dockers_v2:
- images:
- "ghcr.io/bishopfox/sj"
dockerfile: Dockerfile
platforms:
- linux/amd64
- linux/arm64
tags:
- "{{ .Version }}"
- "latest"
labels:
org.opencontainers.image.title: "{{ .ProjectName }}"
org.opencontainers.image.description: "Swagger Jacker"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.source: "https://github.com/BishopFox/sj"
org.opencontainers.image.licenses: "MIT"
changelog:
disable: false
sort: asc
+7
View File
@@ -0,0 +1,7 @@
FROM gcr.io/distroless/static-debian12:nonroot
ARG TARGETOS
ARG TARGETARCH
COPY ${TARGETOS}/${TARGETARCH}/sj /usr/local/bin/sj
ENTRYPOINT ["/usr/local/bin/sj"]
+10 -12
View File
@@ -22,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
@@ -47,13 +45,13 @@ func CheckSecuritySchemes(spec map[string]interface{}) {
autoApplyBasicAuth = "n"
printWarn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
} else {
fmt.Println("Basic Authentication is accepted. Supply a username and password? (y/N)")
printInfo("Basic Authentication is accepted. Supply a username and password? (y/N)\n")
fmt.Scanln(&autoApplyBasicAuth)
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
if autoApplyBasicAuth == "y" {
fmt.Printf("Enter a username.")
printInfo("Enter a username.")
fmt.Scanln(&basicAuthUser)
fmt.Printf("Enter a password.")
printInfo("Enter a password.")
fmt.Scanln(&basicAuthPass)
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
@@ -83,7 +81,7 @@ 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)
printInfo("Using %s=%s as the API key in all requests.\n", apiKeyName, apiKey)
}
@@ -97,7 +95,7 @@ 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 {
@@ -114,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)
}
@@ -126,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)
}
}
}
+59
View File
@@ -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)
}
}
}
+2 -4
View File
@@ -1,7 +1,6 @@
package cmd
import (
"fmt"
"io"
"os"
"path/filepath"
@@ -46,8 +45,7 @@ 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)
die("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
}
var bodyBytes []byte
@@ -55,7 +53,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
client, replayClient := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
printInfo("\n")
printInfo("Gathering API details.\n")
}
+148 -49
View File
@@ -21,9 +21,23 @@ import (
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{
@@ -54,9 +68,13 @@ var bruteCmd = &cobra.Command{
target := u.Scheme + "://" + u.Host
normalizedBasePath := normalizeBasePath(basePath)
if endpointWordlist == "" {
// 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, jsonEndpoints, "", false)...)
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 {
@@ -145,60 +163,98 @@ func makeURLs(target string, basePath string, endpoints []string, fileExtension
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("")
printInfo("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|const|var)\s+(\w+)\s*=\s*({.*?});`)
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
for _, match := range matches {
if len(match) < 3 {
continue
}
jsonContent := []byte(match[2])
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
}
// Swagger UI commonly nests the spec one level deep under
// keys like swaggerDoc (swagger-ui-express / NestJS),
// spec (Swagger UI Bundle config), or openapi.
var wrapper map[string]json.RawMessage
if err := json.Unmarshal(jsonContent, &wrapper); err != nil {
continue
}
for _, key := range []string{"swaggerDoc", "spec", "openapi"} {
inner, ok := wrapper[key]
if !ok {
continue
}
checkSpec := UnmarshalSpec(inner)
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
}
}
}
}
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
@@ -210,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
@@ -279,7 +378,7 @@ 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" {
+2 -2
View File
@@ -32,7 +32,7 @@ var convertCmd = &cobra.Command{
client, _ := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
printInfo("\n")
printInfo("Gathering API details.\n\n")
}
@@ -65,7 +65,7 @@ var convertCmd = &cobra.Command{
} 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 {
+240
View File
@@ -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
}
+1 -2
View File
@@ -1,7 +1,6 @@
package cmd
import (
"fmt"
"io"
"os"
"path/filepath"
@@ -25,7 +24,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
var bodyBytes []byte
fmt.Printf("\n")
printInfo("\n")
printInfo("Gathering endpoints.\n\n")
if swaggerURL != "" {
+22
View File
@@ -30,16 +30,38 @@ 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)
}
+2 -4
View File
@@ -1,7 +1,6 @@
package cmd
import (
"fmt"
"io"
"os"
"path/filepath"
@@ -25,13 +24,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)
die("An invalid date was supplied. Please supply a date in '2006-01-02' format.")
}
client, _ := CheckAndConfigureProxy()
fmt.Printf("\n")
printInfo("\n")
printInfo("Gathering API details.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
+21 -67
View File
@@ -52,7 +52,16 @@ 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"
}
@@ -61,14 +70,14 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
u, err := url.Parse(target)
if err != nil || u == nil {
printWarn("Error parsing URL '%s': %v - skipping request.", target, err)
return nil, "", 0
return nil, "", 0, ""
}
endpoint := u.RawPath + "?" + u.RawQuery
for _, v := range dangerousStrings {
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 {
printWarn("Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
fmt.Scanln(&userChoice)
@@ -80,7 +89,7 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
avoidDangerousRequests = strings.ToLower(avoidDangerousRequests)
riskSurveyed = true
}
return nil, "", 0
return nil, "", 0, ""
}
}
}
@@ -94,7 +103,7 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
if err != context.Canceled && err != io.EOF {
die("Error: could not create HTTP request - %v", err)
}
return nil, "", 0
return nil, "", 0, ""
}
for i := range Headers {
@@ -145,91 +154,36 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
resp, err := client.Do(req.WithContext(ctx))
if err == context.DeadlineExceeded {
printWarn("Error: %s - skipping request.", err)
return nil, "", 0
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") {
die("Try supplying the --insecure flag.")
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such 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 {
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>") && 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
}
func CheckContentType(client http.Client, target string) string {
u, err := url.Parse(target)
if err != nil || u == nil {
printWarn("Error parsing URL '%s': %v - skipping request.", target, err)
return ""
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
req, err := http.NewRequest("GET", target, nil)
if err != nil {
if err != context.Canceled && err != io.EOF {
die("Error: could not create HTTP request - %v", err)
}
return ""
}
for i := range Headers {
delimIndex := strings.Index(Headers[i], ":")
if delimIndex == -1 {
continue
}
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
if key == "User-Agent" {
UserAgent = value
}
req.Header.Set(key, value)
}
// User-Agent handling
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 {
printWarn("Error: %s - skipping request.", err)
return ""
} else if err != nil && err != context.Canceled && err != io.EOF {
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
die("Try supplying the --insecure flag.")
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such 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 {
printErr("Error: response not received.\n%v", err)
}
return ""
}
return resp.Header.Get("Content-Type")
return bodyBytes, bodyString, requestStatus, contentTypeHeader
}
func CheckAndConfigureProxy() (client http.Client, replayClient *http.Client) {
+1 -1
View File
@@ -53,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.6.2",
Version: "2.8.2",
}
func Execute() {
+80 -15
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"os"
@@ -49,6 +50,19 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, rep
}
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, rep
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, rep
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, rep
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)))
@@ -540,6 +577,15 @@ func GenerateExample(node *SchemaNode) interface{} {
}
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)
@@ -593,11 +639,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client, replayClient *http.C
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)
}
printInfo("%s\n", serverURL)
}
}
}
@@ -781,7 +823,7 @@ func PrintSpecInfo(spec map[string]interface{}) {
if outputFormat == "json" {
specTitle = title
} else {
fmt.Printf("Title: %s\n", title)
printInfo("Title: %s\n", title)
}
}
@@ -790,7 +832,7 @@ func PrintSpecInfo(spec map[string]interface{}) {
if outputFormat == "json" {
specDescription = description
} else {
fmt.Printf("Description: %s\n", description)
printInfo("Description: %s\n", description)
}
}
}
@@ -807,12 +849,35 @@ 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{}
if err := yaml.Unmarshal(data, &doc); err != nil {
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
os.Exit(1)
die("Failed to unmarshal API documentation: %v", err)
}
return doc
+9 -12
View File
@@ -1,28 +1,25 @@
module github.com/BishopFox/sj
go 1.22.5
toolchain go1.23.4
go 1.25
require (
github.com/fatih/color v1.18.0
github.com/getkin/kin-openapi v0.132.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/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/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/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.25.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
+22 -24
View File
@@ -1,56 +1,54 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/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.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/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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/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/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/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/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-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=