From 291edfe60c5638298f2f4379c1a795da8a9b5c16 Mon Sep 17 00:00:00 2001 From: Tony West <125916820+twest-bf@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:08:07 -0400 Subject: [PATCH] Added the 'convert' subcommand --- README.md | 17 ++++++- cmd/convert.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 9 ++-- cmd/utils.go | 1 - 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 cmd/convert.go diff --git a/README.md b/README.md index 37f8391..3c4160c 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ sj is a command line tool designed to assist with auditing of exposed Swagger/OpenAPI definition files by checking the associated API endpoints for weak authentication. It also provides command templates for manual vulnerability testing. -It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of four commands: +It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of five sub-commands: - `automate` - Crafts a series of requests and analyzes the status code of the response. - `prepare` - Generates a list of commands to use for manual testing. - `endpoints` - Generates a list of raw API routes. *Path values will not be replaced with test data*. - `brute` - Sends a series of requests to a target to find operation definitions based on commonly used file paths. +- `convert` - Converts a definition file from v2 to v3. ## Build @@ -132,6 +133,16 @@ INFO[0015] Definition file found: https://petstore.swagger.io/v2/swagger {"...SNIP..."} ``` +> Use the `convert` command to convert a definition file from version 2 to version 3. + +```bash +$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json + +INFO[0000] Gathering API details. + +INFO[0000] Wrote file to /current/directory/openapi.json +``` + ## Help A full list of commands can be found by using the `--help` flag: @@ -157,6 +168,9 @@ $ sj endpoints -u https://petstore.swagger.io/v2/swagger.json Perform a brute-force attack against the target to identify hidden definition files: $ sj brute -u https://petstore.swagger.io +Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file: +$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json + Usage: sj [flags] sj [command] @@ -164,6 +178,7 @@ Usage: Available Commands: automate Sends a series of automated requests to the discovered endpoints. brute Sends a series of automated requests to discover hidden API operation definitions. + convert Converts a Swagger definition file to an OpenAPI v3 definition file. endpoints Prints a list of endpoints from the target. help Help about any command prepare Prepares a set of commands for manual testing of each endpoint. diff --git a/cmd/convert.go b/cmd/convert.go new file mode 100644 index 0000000..72e2bc3 --- /dev/null +++ b/cmd/convert.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi2conv" + "github.com/getkin/kin-openapi/openapi3" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var convertCmd = &cobra.Command{ + Use: "convert", + Short: "Converts a Swagger definition file to an OpenAPI v3 definition file.", + Long: `The convert command converts a provided definition file from the Swagger specification (v2) to the OpenAPI specification (v3) and stores it into an output file.`, + Run: func(cmd *cobra.Command, args []string) { + + var bodyBytes []byte + + client := CheckAndConfigureProxy() + + if strings.ToLower(outputFormat) != "json" { + fmt.Printf("\n") + log.Infof("Gathering API details.\n\n") + } + + if swaggerURL != "" { + bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil) + } else { + specFile, err := os.Open(localFile) + if err != nil { + log.Fatal("Error opening definition file:", err) + } + + bodyBytes, _ = io.ReadAll(specFile) + } + + var doc openapi2.T + var doc3 openapi3.T + + format = strings.ToLower(format) + if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") { + _ = yaml.Unmarshal(bodyBytes, &doc) + _ = yaml.Unmarshal(bodyBytes, &doc3) + } else { + _ = json.Unmarshal(bodyBytes, &doc) + _ = json.Unmarshal(bodyBytes, &doc3) + } + + if strings.HasPrefix(doc3.OpenAPI, "3") { + log.Fatal("Definition file is already version 3.") + } 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) + } + + if format == "json" { + if strings.HasSuffix(outfile, "yaml") || strings.HasSuffix(outfile, "yml") { + log.Warn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).") + } + converted, err := json.Marshal(newDoc) + if err != nil { + log.Fatal("Error converting definition file to v3:", err) + } + if !strings.HasSuffix(string(converted), "}") { + if outfile == "" { + fmt.Println(string(converted)) + } else { + WriteConvertedDefinitionFile(converted) + } + } else { + endOfJSON := strings.LastIndex(string(converted), "}") + 1 + if outfile == "" { + fmt.Println(string(converted)[:endOfJSON]) + } else { + WriteConvertedDefinitionFile(converted) + } + } + } else if format == "yaml" || format == "yml" { + converted, err := yaml.Marshal(newDoc) + if err != nil { + log.Fatal("Error converting definition file to v3:", err) + } + if outfile == "" { + fmt.Println(string(converted)) + } else { + WriteConvertedDefinitionFile(converted) + } + } + + } else { + log.Fatal("Error parsing definition file.") + } + }, +} + +func WriteConvertedDefinitionFile(data []byte) { + file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Errorf("Error opening file: %s\n", err) + } + + defer file.Close() + + _, err = file.Write(data) + if err != nil { + log.Errorf("Error writing file: %s\n", err) + } else { + f, _ := filepath.Abs(outfile) + log.Infof("Wrote file to %s\n", f) + } +} diff --git a/cmd/root.go b/cmd/root.go index 7c502ee..c06c705 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,7 +5,6 @@ import ( "github.com/spf13/cobra" ) -var accessibleEndpointFound bool var apiTarget string var basePath string var format string @@ -39,14 +38,17 @@ Generate a list of raw API routes for use with custom scripts: $ sj endpoints -u https://petstore.swagger.io/v2/swagger.json Perform a brute-force attack against the target to identify hidden definition files: -$ sj brute -u https://petstore.swagger.io`, +$ sj brute -u https://petstore.swagger.io + +Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file: +$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`, Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { log.Error("Command not specified. See the --help flag for usage.") } }, - Version: "1.7.4", + Version: "1.8.0", } func Execute() { @@ -58,6 +60,7 @@ func init() { rootCmd.AddCommand(endpointsCmd) rootCmd.AddCommand(prepareCmd) rootCmd.AddCommand(bruteCmd) + 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(&format, "format", "f", "json", "Declare the format of the definition file (json/yaml/yml/js).") diff --git a/cmd/utils.go b/cmd/utils.go index b937013..813920a 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -183,7 +183,6 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string, if os.Args[1] == "automate" { _, resp, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData)) if sc == 200 { - accessibleEndpointFound = true accessibleEndpoints = append(accessibleEndpoints, s.URL.String()) } if strings.ToLower(outputFormat) != "console" {