diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9cf8f7b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: goreleaser +on: + push: + tags: + - "*" + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6eb580d..8c7cadc 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,7 +7,7 @@ # vim: set ts=2 sw=2 tw=0 fo=cnqoj version: 2 -project_name: monnica +project_name: monica before: hooks: # You may remove this if you don't use go modules. diff --git a/go.mod b/go.mod index 8e60389..951f0a0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,13 @@ go 1.22.4 require github.com/rs/zerolog v1.33.0 require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e528856..4120dd2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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= @@ -10,8 +14,17 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index bd0345b..c9ebd59 100644 --- a/main.go +++ b/main.go @@ -1,43 +1,9 @@ package main import ( - "flag" - "github.com/rs/zerolog" - "jmpeax.com/sec/monica/internal/logging" - "jmpeax.com/sec/monica/pkg/runner" - "os" -) - -var ( - verbosity = flag.Int("v", -1, "verbosity level") - file = flag.String("f", "", "Mon File to run") - headerOnly = flag.Bool("h", false, "Output headers only") + "jmpeax.com/sec/monica/pkg/cmd" ) func main() { - flag.Parse() - switch *verbosity { - case 1: - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case 2: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - default: - zerolog.SetGlobalLevel(zerolog.InfoLevel) - } - logging.Log.Info().Msg("Welcome to Monica!") - if *file == "" { - logging.Log.Error().Msg("No Mon File provided") - return - } - monFile, err := os.Stat(*file) - if err != nil { - logging.Log.Error().Msgf("Mon File not found: %s", *file) - return - } - if !monFile.Mode().IsRegular() { - logging.Log.Error().Msgf("Mon File is not a regular file: %s", *file) - return - } - logging.Log.Info().Msgf("Running Mon File: %s", *file) - runner.RunSingleFile(*file, &runner.Opts{HeaderOnly: *headerOnly}) + cmd.Execute() } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go new file mode 100644 index 0000000..fc4150a --- /dev/null +++ b/pkg/cmd/root.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/rs/zerolog" + "github.com/spf13/cobra" +) + +var ( + debug bool + rootCmd = &cobra.Command{ + Use: "monica", + Short: "Monica is a CLI tool to manage Network Requests", + Long: `Monica is a CLI tool to manage Network Requests like http, grpc, etc.`, + Version: "0.1.0", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if debug { + zerolog.SetGlobalLevel(zerolog.TraceLevel) + } else { + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + }, + } +) + +func init() { + rootCmd.AddCommand(run) + rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable Debug logging") +} + +func Execute() error { + return rootCmd.Execute() +} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go new file mode 100644 index 0000000..3f76e01 --- /dev/null +++ b/pkg/cmd/run.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "jmpeax.com/sec/monica/internal/logging" + "jmpeax.com/sec/monica/pkg/runner" +) + +var ( + singleFile string + recursiveLevel int + run = &cobra.Command{ + Use: "run", + Short: "Runs all mon files", + Long: "Runs all mon files found in the current working directory", + Run: func(cmd *cobra.Command, args []string) { + if singleFile != "" { + runSingleFile(singleFile) + } else { + runAllFiles(recursiveLevel) + } + }, + } +) + +func init() { + run.Flags().StringVarP(&singleFile, "single", "s", "", "runs a single mon file, path can be absolute o relative to current working directory") + run.Flags().IntVarP(&recursiveLevel, "recursive", "r", 1, "runs all mon files in the current working directory and subdirectories up to the specified level") +} + +func runSingleFile(file string) { + runner.RunSingleFile(file, &runner.Opts{ + HeaderOnly: false, + }) +} + +func runAllFiles(level int) { + files, err := FindMonFiles(".", level) + logging.Log.Info().Msgf("Found %d mon files", len(files)) + if err != nil { + logging.Log.Error().Err(err).Msg("Error finding mon files") + os.Exit(1) + } + for _, file := range files { + runner.RunSingleFile(file, &runner.Opts{ + HeaderOnly: false, + }) + } +} + +func FindMonFiles(root string, maxDepth int) ([]string, error) { + var files []string + + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + // Calculate the current depth + relativePath, err := filepath.Rel(root, path) + if err != nil { + return err + } + depth := len(strings.Split(filepath.ToSlash(relativePath), "/")) + + // If the current depth exceeds maxDepth, skip this directory + if d.IsDir() { + if depth > maxDepth { + return filepath.SkipDir + } + } else { + // Check if the file ends with .mon + if strings.HasSuffix(d.Name(), ".mon") { + files = append(files, path) + } + } + + return nil + }) + + return files, err +} diff --git a/pkg/net/http.go b/pkg/net/http.go index b758e76..cdc5d06 100644 --- a/pkg/net/http.go +++ b/pkg/net/http.go @@ -9,10 +9,11 @@ import ( "jmpeax.com/sec/monica/pkg/request" ) -func HTTPRequest(request *request.Request, headerOnly bool) *HTTPResponse { +func HTTPRequest(request *request.Request, headerOnly bool) (*HTTPResponse, error) { req, err := http.NewRequest(request.Method, request.URL, strings.NewReader(request.Body)) if err != nil { logging.Log.Error().Err(err).Msg("Failed to create HTTP request") + return nil, err } for k, v := range request.Headers { req.Header.Add(k, v) @@ -22,11 +23,12 @@ func HTTPRequest(request *request.Request, headerOnly bool) *HTTPResponse { resp, err := client.Do(req) if err != nil { logging.Log.Error().Err(err).Msg("Failed to send HTTP request") + return nil, err } return parseHTTPResponse(resp, headerOnly) } -func parseHTTPResponse(resp *http.Response, headerOnly bool) *HTTPResponse { +func parseHTTPResponse(resp *http.Response, headerOnly bool) (*HTTPResponse, error) { response := &HTTPResponse{ Status: resp.Proto + " " + resp.Status, StatusCode: resp.StatusCode, @@ -42,10 +44,11 @@ func parseHTTPResponse(resp *http.Response, headerOnly bool) *HTTPResponse { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { logging.Log.Error().Err(err).Msg("Failed to read response body") + return nil, err } response.Body = string(bodyBytes) } - return response + return response, nil } func parseResponseHeaders(headers http.Header) map[string]string { diff --git a/pkg/request/parser.go b/pkg/request/parser.go index 2e42b2c..2341d00 100644 --- a/pkg/request/parser.go +++ b/pkg/request/parser.go @@ -2,16 +2,16 @@ package request import ( "bufio" - "jmpeax.com/sec/monica/internal/logging" - "log" "os" "strings" + + "jmpeax.com/sec/monica/internal/logging" ) // ParseRequest parses a raw HTTP request and returns a Request object. func ParseRequest(raw string) *Request { logging.Log.Debug().Msg("Parsing request") - var request = Request{} + request := Request{} scanner := bufio.NewScanner(strings.NewReader(raw)) for scanner.Scan() { line := scanner.Text() @@ -59,7 +59,6 @@ func parseRequestHeader(line string, r *Request) { } func parseRequestLine(requestLine string, r *Request) { - log.Default().Println("Parsing request line") parts := strings.Split(requestLine, " ") if len(parts) >= 2 { r.Method = parts[0] diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 515a812..53a5a8e 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -2,13 +2,17 @@ package runner import ( "fmt" + "jmpeax.com/sec/monica/pkg/net" "jmpeax.com/sec/monica/pkg/request" ) func RunSingleFile(file string, r *Opts) { req := request.ParseMonFile(file) - res := net.HTTPRequest(req, r.HeaderOnly) + res, err := net.HTTPRequest(req, r.HeaderOnly) + if err != nil { + return + } if res != nil { fmt.Println(res) }