From 89a7d36df32d7c984611c2b0fef6c6fba9e81df2 Mon Sep 17 00:00:00 2001 From: j3ssie Date: Thu, 7 Sep 2023 21:21:14 +0700 Subject: [PATCH] Release v4.6.0 --- .github/FUNDING.yml | 12 + .gitignore | 5 + LICENSE | 22 + Makefile | 38 + README.md | 157 +++ cmd/cloud.go | 185 ++++ cmd/config.go | 100 ++ cmd/exec.go | 56 ++ cmd/health.go | 283 ++++++ cmd/provider.go | 266 ++++++ cmd/queue.go | 87 ++ cmd/report.go | 162 ++++ cmd/root.go | 147 +++ cmd/scan.go | 80 ++ cmd/server.go | 49 + cmd/update.go | 77 ++ cmd/usage.go | 313 ++++++ cmd/utils.go | 139 +++ cmd/version.go | 176 ++++ cmd/workflow.go | 200 ++++ core/backup.go | 68 ++ core/banner.go | 63 ++ core/config.go | 382 ++++++++ core/cron.go | 30 + core/db.go | 229 +++++ core/external.go | 298 ++++++ core/flow.go | 222 +++++ core/import.go | 574 +++++++++++ core/import_mics.go | 238 +++++ core/markdown.go | 256 +++++ core/mode_test.go | 24 + core/module.go | 370 ++++++++ core/parse.go | 322 +++++++ core/parse_test.go | 71 ++ core/queue.go | 147 +++ core/reference.go | 104 ++ core/report.go | 215 +++++ core/runner.go | 373 ++++++++ core/runtime.go | 488 ++++++++++ core/step.go | 167 ++++ core/tmux.go | 85 ++ core/token.go | 288 ++++++ core/update.go | 336 +++++++ core/validate.go | 144 +++ core/validate_test.go | 53 ++ database/connect.go | 63 ++ database/models.go | 99 ++ database/select.go | 100 ++ distribute/clean.go | 54 ++ distribute/cloud_runner.go | 201 ++++ distribute/command.go | 175 ++++ distribute/create.go | 153 +++ distribute/db_cloud.go | 35 + distribute/health.go | 159 ++++ distribute/mics.go | 115 +++ distribute/routine.go | 94 ++ distribute/scan.go | 267 ++++++ distribute/ssh.go | 430 +++++++++ distribute/wizard.go | 240 +++++ execution/clean.go | 527 ++++++++++ execution/clean_test.go | 19 + execution/git.go | 183 ++++ execution/git_test.go | 25 + execution/gitlab.go | 206 ++++ execution/gitlab_test.go | 9 + execution/noti.go | 352 +++++++ execution/noti_test.go | 44 + execution/process.go | 93 ++ execution/process_test.go | 9 + execution/remote.go | 126 +++ execution/request.go | 227 +++++ execution/require.go | 106 +++ execution/s3_cdn.go | 125 +++ execution/scripts.go | 225 +++++ execution/scripts_test.go | 19 + execution/split.go | 126 +++ execution/wildcard.go | 61 ++ go.mod | 113 +++ go.sum | 737 ++++++++++++++ libs/cloud.go | 115 +++ libs/flow.go | 69 ++ libs/mics.go | 61 ++ libs/noti.go | 26 + libs/options.go | 172 ++++ libs/queue.go | 22 + libs/step.go | 30 + libs/update.go | 28 + libs/version.go | 26 + main.go | 7 + provider/action.go | 201 ++++ provider/building.go | 157 +++ provider/parser.go | 127 +++ provider/provider.go | 265 ++++++ provider/provider_aws.go | 516 ++++++++++ provider/provider_digitalocean.go | 294 ++++++ provider/provider_linode.go | 477 ++++++++++ provider/provider_test.go | 95 ++ server/auth.go | 68 ++ server/builder.go | 170 ++++ server/docs/docs.go | 120 +++ server/docs/swagger.json | 53 ++ server/docs/swagger.yaml | 33 + server/mics.go | 100 ++ server/ping.go | 41 + server/router.go | 152 +++ server/scan.go | 130 +++ server/ssl.go | 191 ++++ server/workspace.go | 118 +++ test-workflows/flow-with-params.yaml | 11 + test-workflows/general.yaml | 31 + test-workflows/general/archive.yaml | 10 + test-workflows/general/cloudbrute.yaml | 10 + test-workflows/general/credintel.yaml | 10 + test-workflows/general/dirbscan.yaml | 10 + test-workflows/general/fingerprint.yaml | 13 + test-workflows/general/ipspace.yaml | 10 + test-workflows/general/portscan.yaml | 10 + test-workflows/general/probing.yaml | 10 + test-workflows/general/spider.yaml | 10 + test-workflows/general/subdomain.yaml | 10 + test-workflows/general/summary.yaml | 10 + test-workflows/general/vulnscan.yaml | 11 + test-workflows/parallel.yaml | 14 + test-workflows/pre-run-cloud.yaml | 17 + test-workflows/sample/parallel.yaml | 20 + test-workflows/sample/parallel2.yaml | 12 + test-workflows/sample/timeout-module.yaml | 6 + test-workflows/serial.yaml | 12 + test-workflows/test-module/loop-step.yaml | 20 + .../test-module/markdown-generate.yaml | 15 + test-workflows/test-module/ose.yaml | 20 + test-workflows/test-module/s3-cdn.yaml | 8 + test-workflows/test-module/set-var.yaml | 9 + test-workflows/test-module/test-parallel.yaml | 18 + .../test-module/with-threadshold.yaml | 14 + utils/helper.go | 897 ++++++++++++++++++ utils/log.go | 128 +++ utils/request.go | 122 +++ 138 files changed, 18980 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/cloud.go create mode 100644 cmd/config.go create mode 100644 cmd/exec.go create mode 100644 cmd/health.go create mode 100644 cmd/provider.go create mode 100644 cmd/queue.go create mode 100644 cmd/report.go create mode 100644 cmd/root.go create mode 100644 cmd/scan.go create mode 100644 cmd/server.go create mode 100644 cmd/update.go create mode 100644 cmd/usage.go create mode 100644 cmd/utils.go create mode 100644 cmd/version.go create mode 100644 cmd/workflow.go create mode 100644 core/backup.go create mode 100644 core/banner.go create mode 100644 core/config.go create mode 100644 core/cron.go create mode 100644 core/db.go create mode 100644 core/external.go create mode 100644 core/flow.go create mode 100644 core/import.go create mode 100644 core/import_mics.go create mode 100644 core/markdown.go create mode 100644 core/mode_test.go create mode 100644 core/module.go create mode 100644 core/parse.go create mode 100644 core/parse_test.go create mode 100644 core/queue.go create mode 100644 core/reference.go create mode 100644 core/report.go create mode 100644 core/runner.go create mode 100644 core/runtime.go create mode 100644 core/step.go create mode 100644 core/tmux.go create mode 100644 core/token.go create mode 100644 core/update.go create mode 100644 core/validate.go create mode 100644 core/validate_test.go create mode 100644 database/connect.go create mode 100644 database/models.go create mode 100644 database/select.go create mode 100644 distribute/clean.go create mode 100644 distribute/cloud_runner.go create mode 100644 distribute/command.go create mode 100644 distribute/create.go create mode 100644 distribute/db_cloud.go create mode 100644 distribute/health.go create mode 100644 distribute/mics.go create mode 100644 distribute/routine.go create mode 100644 distribute/scan.go create mode 100644 distribute/ssh.go create mode 100644 distribute/wizard.go create mode 100644 execution/clean.go create mode 100644 execution/clean_test.go create mode 100644 execution/git.go create mode 100644 execution/git_test.go create mode 100644 execution/gitlab.go create mode 100644 execution/gitlab_test.go create mode 100644 execution/noti.go create mode 100644 execution/noti_test.go create mode 100644 execution/process.go create mode 100644 execution/process_test.go create mode 100644 execution/remote.go create mode 100644 execution/request.go create mode 100644 execution/require.go create mode 100644 execution/s3_cdn.go create mode 100644 execution/scripts.go create mode 100644 execution/scripts_test.go create mode 100644 execution/split.go create mode 100644 execution/wildcard.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 libs/cloud.go create mode 100644 libs/flow.go create mode 100644 libs/mics.go create mode 100644 libs/noti.go create mode 100644 libs/options.go create mode 100644 libs/queue.go create mode 100644 libs/step.go create mode 100644 libs/update.go create mode 100644 libs/version.go create mode 100644 main.go create mode 100644 provider/action.go create mode 100644 provider/building.go create mode 100644 provider/parser.go create mode 100644 provider/provider.go create mode 100644 provider/provider_aws.go create mode 100644 provider/provider_digitalocean.go create mode 100644 provider/provider_linode.go create mode 100644 provider/provider_test.go create mode 100644 server/auth.go create mode 100644 server/builder.go create mode 100644 server/docs/docs.go create mode 100644 server/docs/swagger.json create mode 100644 server/docs/swagger.yaml create mode 100644 server/mics.go create mode 100644 server/ping.go create mode 100644 server/router.go create mode 100644 server/scan.go create mode 100644 server/ssl.go create mode 100644 server/workspace.go create mode 100644 test-workflows/flow-with-params.yaml create mode 100644 test-workflows/general.yaml create mode 100644 test-workflows/general/archive.yaml create mode 100644 test-workflows/general/cloudbrute.yaml create mode 100644 test-workflows/general/credintel.yaml create mode 100644 test-workflows/general/dirbscan.yaml create mode 100644 test-workflows/general/fingerprint.yaml create mode 100644 test-workflows/general/ipspace.yaml create mode 100644 test-workflows/general/portscan.yaml create mode 100644 test-workflows/general/probing.yaml create mode 100644 test-workflows/general/spider.yaml create mode 100644 test-workflows/general/subdomain.yaml create mode 100644 test-workflows/general/summary.yaml create mode 100644 test-workflows/general/vulnscan.yaml create mode 100644 test-workflows/parallel.yaml create mode 100644 test-workflows/pre-run-cloud.yaml create mode 100644 test-workflows/sample/parallel.yaml create mode 100644 test-workflows/sample/parallel2.yaml create mode 100644 test-workflows/sample/timeout-module.yaml create mode 100644 test-workflows/serial.yaml create mode 100644 test-workflows/test-module/loop-step.yaml create mode 100644 test-workflows/test-module/markdown-generate.yaml create mode 100644 test-workflows/test-module/ose.yaml create mode 100644 test-workflows/test-module/s3-cdn.yaml create mode 100644 test-workflows/test-module/set-var.yaml create mode 100644 test-workflows/test-module/test-parallel.yaml create mode 100644 test-workflows/test-module/with-threadshold.yaml create mode 100644 utils/helper.go create mode 100644 utils/log.go create mode 100644 utils/request.go diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ccf10a2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: j3ssie +open_collective: osmedeus +ko_fi: j3ssie +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: [ 'https://paypal.me/j3ssiejjj' ] \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9571790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_STORE +.idea +out* +osmedeus +dist/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c69c3a0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 j3ssie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..48226d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +TARGET ?= osmedeus +GO ?= go +GOFLAGS ?= +VERSION := $(shell cat libs/version.go | grep 'VERSION =' | cut -d '"' -f 2) + +build: + go install + go build -ldflags="-s -w" -tags netgo -trimpath -buildmode=pie -o dist/$(TARGET) + +release: + go install + @echo "==> Clean up old builds" + rm -rf ./dist/* ~/myGit/premium-$(TARGET)-base/dist/* ~/org-$(TARGET)/$(TARGET)-base/dist/* + @echo "==> building binaries for for mac intel" + GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -tags netgo -trimpath -buildmode=pie -o dist/$(TARGET) + zip -9 -j dist/$(TARGET)-macos-amd64.zip dist/$(TARGET) && rm -rf ./dist/$(TARGET) + @echo "==> building binaries for for mac M1 chip" + CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -tags netgo -trimpath -buildmode=pie -o dist/$(TARGET) + zip -9 -j dist/$(TARGET)-macos-arm64.zip dist/$(TARGET)&& rm -rf ./dist/$(TARGET) + @echo "==> building binaries for linux intel build on mac" + GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -tags netgo -trimpath -buildmode=pie -o dist/$(TARGET) + zip -j dist/$(TARGET)-linux.zip dist/$(TARGET)&& rm -rf ./dist/$(TARGET) + cp dist/* ~/myGit/premium-$(TARGET)-base/dist/ + cp dist/* ~/org-$(TARGET)/$(TARGET)-base/dist/ + @echo "==> Generating metadata info" + $(TARGET) update --gen dist/public.json + mv dist/$(TARGET)-macos-amd64.zip dist/$(TARGET)-$(VERSION)-macos-amd64.zip + mv dist/$(TARGET)-macos-arm64.zip dist/$(TARGET)-$(VERSION)-macos-arm64.zip + mv dist/$(TARGET)-linux.zip dist/$(TARGET)-$(VERSION)-linux.zip +run: + $(GO) $(GOFLAGS) run *.go + +fmt: + $(GO) $(GOFLAGS) fmt ./...; \ + echo "Done." + +test: + $(GO) $(GOFLAGS) test ./... -v% \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..559aa9d --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# Osmedeus Core Engine + +

+ Osmedeus +
+ Osmedeus - A Workflow Engine for Offensive Security + +

+ + + + + +

+

+ +*** + +## šŸ”„ What is Osmedeus? + +Osmedeus is a Workflow Engine for Offensive Security. It was designed to build a foundation with the capability and +flexibility that allows you to build your own reconnaissance system and run it on a large number of targets. + +## šŸ“– Documentation & FAQ + +You can check out the documentation at [**docs.osmedeus.org**](https://docs.osmedeus.org) and the Frequently Asked +Questions at [**here**](https://docs.osmedeus.org/faq) for more information. + +## šŸ“¦ Installation + +> NOTE that you need some essential tools like `curl, wget, git, zip` and login as **root** to start + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/osmedeus/osmedeus-base/master/install.sh) +``` + +### Build the engine from the source + +Make sure you installed `golang >= v1.17` + +```bash +go install -v github.com/j3ssie/osmedeus@latest +``` + +Check out [**this page**](https://docs.osmedeus.org/installation/) for more the install on other platforms and [**docker +image**](https://docs.osmedeus.org/installation/using-docker/). + +## šŸš€ Key Features of Osmedeus + +- [x] Significantly speed up your recon process +- [x] Organize your scan results +- [x] Efficiently to customize and optimize your recon process +- [x] Seamlessly integrate with new public and private tools +- [x] Easy to scale across large number of targets +- [x] Easy to synchronize the results across many places + +## šŸ’” Usage + +```bash +# Example Scan Commands: + ## Start a simple scan with default 'general' flow + osmedeus scan -t sample.com + + ## Start a general scan but exclude some of the module + osmedeus scan -t sample.com -x screenshot -x spider + + ## Start a scan directly with a module with inputs as a list of http domains like this https://sub.example.com + osmedeus scan -m content-discovery -t http-file.txt + + ## Initiate the scan using a speed option other than the default setting + osmedeus scan -f vuln --tactic gently -t sample.com + osmedeus scan --threads-hold=10 -t sample.com + osmedeus scan -B 5 -t sample.com + + ## Start a simple scan with other flow + osmedeus scan -f vuln -t sample.com + osmedeus scan -f extensive -t sample.com -t another.com + osmedeus scan -f urls -t list-of-urls.txt + + ## Scan list of targets + osmedeus scan -T list_of_targets.txt + osmedeus scan -f vuln -T list-of-targets.txt + + ## Performing static vulnerability scan and secret scan on a git repo + osmedeus scan -m repo-scan -t https://github.com/j3ssie/sample-repo + osmedeus scan -m repo-scan -t /tmp/source-code-folder + osmedeus scan -m repo-scan -T list-of-repo.txt + + ## Scan for CIDR with file contains CIDR with the format '1.2.3.4/24' + osmedeus scan -f cidr -t list-of-ciders.txt + osmedeus scan -f cidr -t '1.2.3.4/24' # this will auto convert the single input to the file and run + + ## Directly run on vuln scan and directory scan on list of domains + osmedeus scan -f domains -t list-of-domains.txt + osmedeus scan -f vuln-and-dirb -t list-of-domains.txt + + ## Use a custom wordlist + osmedeus scan -t sample.com -p 'wordlists={{Data}}/wordlists/content/big.txt' + + ## Use a custom wordlist + cat list_of_targets.txt | osmedeus scan -c 2 + + ## Start a normal scan and backup entire workflow folder to the backup folder + osmedeus scan --backup -f domains -t list-of-subdomains.txt + + ## Start the scan with chunk inputs to review the output way more much faster + osmedeus scan --chunk --chunk-parts 20 -f cidr -t list-of-100-cidr.txt + + ## Continuously run the scan on a target right after it finished + osmedeus utils cron --for --cmd 'osmedeus scan -t example.com' + + ## Backing up all workspaces + ls ~/workspaces-osmedeus | osmedeus report compress + + +# Scan Usage: + osmedeus scan -f [flowName] -t [target] + osmedeus scan -m [modulePath] -T [targetsFile] + osmedeus scan -f /path/to/flow.yaml -t [target] + osmedeus scan -m /path/to/module.yaml -t [target] --params 'port=9200' + osmedeus scan -m /path/to/module.yaml -t [target] -l /tmp/log.log + osmedeus scan --tactic aggressive -m module -t [target] + cat targets | osmedeus scan -f sample + +# Practical Scan Usage: + osmedeus scan -T list_of_targets.txt -W custom_workspaces + osmedeus scan -t target.com -w workspace_name --debug + osmedeus scan -f general -t sample.com + osmedeus scan --tactic aggressive -f general -t sample.com + osmedeus scan -f extensive -t sample.com -t another.com + cat list_of_urls.txt | osmedeus scan -f urls + osmedeus scan --threads-hold=15 -f cidr -t 1.2.3.4/24 + osmedeus scan -m ~/.osmedeus/core/workflow/test/dirbscan.yaml -t list_of_urls.txt + osmedeus scan --wfFolder ~/custom-workflow/ -f your-custom-workflow -t list_of_urls.txt + osmedeus scan --chunk --chunk-part 40 -c 2 -f cidr -t list-of-cidr.txt + +šŸ’” For full help message, please run: osmedeus --hh or osmedeus scan --hh +šŸ“– Documentation can be found here: https://docs.osmedeus.org +``` + +Check out [**this page**](https://docs.osmedeus.org/installation/usage/) for full usage and the [**Practical Usage**](https://docs.osmedeus.org/installation/practical-usage/) to see how to use Osmedeus in a practical way. + +## šŸ’¬ Community & Discussion + +Join Our Discord server [here](https://discord.gg/mtQG2FQsYA) + +## šŸ’Ž Donation & Sponsor + +

+ Osmedeus + +

Check out for a couple of donation methods here to get a premium package

+

+ +## License + +`Osmedeus` is made with ♄ by [@j3ssiejjj](https://twitter.com/j3ssiejjj) and it is released under the MIT license. diff --git a/cmd/cloud.go b/cmd/cloud.go new file mode 100644 index 0000000..061c876 --- /dev/null +++ b/cmd/cloud.go @@ -0,0 +1,185 @@ +package cmd + +import ( + "fmt" + "io" + "os" + "path" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/distribute" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func init() { + var cloudCmd = &cobra.Command{ + Use: "cloud", + Short: "Perform a scan using the distributed cloud mode", + Long: core.Banner(), + RunE: runCloud, + } + + // core options + cloudCmd.Flags().StringVarP(&options.Cloud.Module, "module", "m", "", "module name for running") + cloudCmd.Flags().StringVarP(&options.Cloud.Flow, "flow", "f", "general", "Flow name for running (default: general)") + cloudCmd.Flags().StringVarP(&options.Cloud.Workspace, "workspace", "w", "", "Name of workspace (default is same as target)") + cloudCmd.Flags().StringSliceVarP(&options.Cloud.Params, "params", "p", []string{}, "Custom params -p='foo=bar' (Multiple -p flags are accepted)") + + // chunk inputs + cloudCmd.Flags().BoolVar(&options.Cloud.EnablePrivateIP, "privateIP", false, "Enable Private IP") + cloudCmd.Flags().BoolVar(&options.Cloud.TargetAsFile, "as-file", false, "Run target as file (use -T targets.txt file instead of -t targets.txt at cloud instance)") + cloudCmd.Flags().StringVar(&options.Cloud.LocalSyncFolder, "rfolder", "", "Remote Folder to sync back to local") + + // commands on cloud + cloudCmd.Flags().IntVar(&options.Cloud.Threads, "cloud-threads", 1, "Concurrency level on remote cloud") + cloudCmd.Flags().StringVar(&options.Cloud.Extra, "extra", "", "append raw command after the command builder") + cloudCmd.Flags().StringVar(&options.Cloud.RawCommand, "cmd", "", "specific raw command and override everything (eg: --cmd 'curl {{Target}}')") + cloudCmd.Flags().StringVar(&options.Cloud.ClearTime, "clear", "10m", "time to wait before next clear check") + cloudCmd.Flags().StringVar(&options.Cloud.TempTarget, "tempTargets", "/tmp/osm-tmp-inputs/", "Temp Folder to store targets file") + + // mics option + cloudCmd.Flags().BoolVar(&options.Cloud.EnableSyncWorkflow, "sync-workflow", false, "Enable Sync Workflow folder to remote machine first before starting the scan") + cloudCmd.Flags().BoolVarP(&options.Cloud.CopyWorkspaceToGit, "gws", "G", false, "Enable Copy Workspace to Git (run -f sync after done)") + cloudCmd.Flags().BoolVarP(&options.Cloud.DisableLocalSync, "no-lsync", "z", false, "Disable sync back data to local machine") + cloudCmd.Flags().BoolVar(&options.Cloud.BackgroundRun, "bg", false, "Send command to instance without checking if process is done or not") + cloudCmd.Flags().BoolVar(&options.Cloud.EnableTerraform, "tf", false, "Use terraform to create cloud instance") + cloudCmd.Flags().BoolVar(&options.Cloud.NoDelete, "no-del", false, "Don't delete instance after done (you can run 'osmedeus provider health' to clean up later)") + cloudCmd.Flags().BoolVar(&options.Cloud.IgnoreProcess, "no-ps", false, "Disable checking process on remote machine") + cloudCmd.Flags().IntVar(&options.Cloud.Retry, "retry", 10, "Number of retry when command is error") + cloudCmd.SetHelpFunc(CloudHelp) + RootCmd.AddCommand(cloudCmd) + cloudCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runCloud(cmd *cobra.Command, _ []string) error { + // DBInit() + utils.GoodF("%v %v by %v", cases.Title(language.Und, cases.NoLower).String(libs.BINARY), libs.VERSION, color.HiMagentaString(libs.AUTHOR)) + utils.InforF("Storing the log file to: %v", color.CyanString(options.LogFile)) + + // parse some arguments + threads, _ := cmd.Flags().GetInt("thread") + if threads > 1 || options.Cloud.Threads <= 1 { + options.Cloud.Threads = threads + } + + // get pre run commands + getPreRun(&options) + + // change targets list if chunk mode enable + if options.Cloud.EnableChunk { + utils.InforF("Running cloud scan in chunk mode") + for _, target := range options.Scan.Inputs { + chunkTargets := HandleChunksInputs(target) + if len(chunkTargets) == 0 { + continue + } + + distribute.InitCloud(options, chunkTargets) + // remove chunk inputs + utils.DebugF("Remove chunk inputs file") + for _, ctarget := range chunkTargets { + os.RemoveAll(ctarget) + } + } + return nil + } + + // @NOTE: pro-tips + if options.Concurrency > 1 && len(options.Scan.Inputs) == 1 { + if !utils.FileExists(options.Scan.Inputs[0]) { + utils.WarnF("You're using %v in cloud scan but your input %v is just a single domain", color.HiMagentaString(`'-c %v'`, options.Concurrency), color.HiMagentaString(`'-t %v'`, options.Scan.Inputs[0])) + utils.WarnF("Consider running: osmedeus cloud -c 5 -T list-of-targets.txt") + } + } + + distribute.InitCloud(options, options.Scan.Inputs) + return nil +} + +// HandleChunksInputs split the inputs to multiple file first +func HandleChunksInputs(target string) []string { + var chunkTargets []string + utils.MakeDir(options.Cloud.ChunkInputs) + + if !utils.FileExists(target) { + utils.ErrorF("error to split input file: %v", target) + return chunkTargets + } + + if options.Cloud.NumberOfParts == 0 { + options.Cloud.NumberOfParts = options.Concurrency + } + + utils.DebugF("Splitting %v to %v part", target, options.Cloud.NumberOfParts) + rawChunks, err := utils.SplitLineChunks(target, options.Cloud.NumberOfParts) + if err != nil || len(rawChunks) == 0 { + utils.ErrorF("error to split input file: %v", target) + return chunkTargets + } + fp, err := os.Open(target) + if err != nil { + utils.ErrorF("error to open input file: %v", target) + return chunkTargets + } + for index, offset := range rawChunks { + targetName := fmt.Sprintf("%s-chunk-%v", utils.CleanPath(target), index) + targetName = path.Join(options.Cloud.ChunkInputs, targetName) + + sectionReader := io.NewSectionReader(fp, offset.Start, offset.Stop-offset.Start) + targetFile, err := os.OpenFile(targetName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + utils.ErrorF("error when create chunk file: %v", target) + continue + } + + _, err = io.Copy(targetFile, sectionReader) + if err != nil { + utils.ErrorF("error to read chunk file: %s", err) + continue + } + targetFile.Close() + chunkTargets = append(chunkTargets, targetName) + } + + return chunkTargets +} + +func getPreRun(options *libs.Options) { + if options.Cloud.Module != "" { + module := core.DirectSelectModule(*options, options.Cloud.Module) + if module == "" { + utils.ErrorF("Error to select module: %s", options.Cloud.Module) + return + } + parsedModule, err := core.ParseModules(module) + if err == nil { + options.Cloud.RemotePreRun = parsedModule.RemotePreRun + options.Cloud.LocalPostRun = parsedModule.LocalPostRun + options.Cloud.LocalPreRun = parsedModule.LocalPreRun + options.Cloud.LocalSteps = parsedModule.LocalSteps + } + return + } + + if options.Cloud.Flow != "" { + flows := core.SelectFlow(options.Cloud.Flow, *options) + for _, flow := range flows { + parseFlow, err := core.ParseFlow(flow) + if err == nil { + options.Cloud.RemotePreRun = parseFlow.RemotePreRun + options.Cloud.LocalPostRun = parseFlow.LocalPostRun + options.Cloud.LocalPreRun = parseFlow.LocalPreRun + } + } + } +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..3244a84 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/execution" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" +) + +func init() { + var configCmd = &cobra.Command{ + Use: "config", + Short: "Do some configuration from CLI", + Long: core.Banner(), + RunE: runConfig, + } + + configCmd.Flags().StringP("action", "a", "", "Action") + configCmd.Flags().String("pluginsRepo", "git@gitlab.com:j3ssie/osmedeus-plugins.git", "Osmedeus Plugins repository") + // for cred action + configCmd.Flags().String("user", "", "Username") + configCmd.Flags().String("pass", "", "Password") + configCmd.Flags().StringP("workspace", "w", "", "Name of workspace") + configCmd.SetHelpFunc(ConfigHelp) + RootCmd.AddCommand(configCmd) +} + +func runConfig(cmd *cobra.Command, args []string) error { + sort.Strings(args) + action, _ := cmd.Flags().GetString("action") + workspace, _ := cmd.Flags().GetString("workspace") + + // backward compatible + if action == "" && len(args) > 0 { + action = args[0] + } + + switch action { + case "init": + if utils.FolderExists(fmt.Sprintf("%vcore", options.Env.RootFolder)) { + utils.GoodF("Look like you got properly setup.") + } + break + case "cred": + username, _ := cmd.Flags().GetString("user") + password, _ := cmd.Flags().GetString("pass") + utils.GoodF("Create new credentials %v:%v \n", username, password) + break + + case "reload": + fmt.Println("šŸ’¬ Reload the configuration will replace current settings with new ones based on the current environment") + var input string + fmt.Printf(color.HiRedString("šŸŒ€ Do you want to proceed? (y/N): ")) + fmt.Scan(&input) + input = strings.ToLower(input) + if input == "yes" || input == "y" { + utils.InforF("Delete current config and generate a new one") + os.Remove(options.ConfigFile) + os.Remove(options.TokenConfigFile) + core.InitConfig(&options) + core.ParsingConfig(&options) + } + break + + case "delete", "del": + options.Scan.Input = workspace + options.Scan.ROptions = core.ParseInput(options.Scan.Input, options) + utils.InforF("Delete Workspace: %v", options.Scan.ROptions["Workspace"]) + os.RemoveAll(options.Scan.ROptions["Output"]) + break + + case "pull": + for repo := range options.Storages { + execution.PullResult(repo, options) + } + break + + case "set": + core.SetTactic(&options) + break + case "update": + core.Update(options) + break + + case "clean", "cl", "c": + break + default: + utils.ErrorF("Unknown action: %v", color.HiRedString(action)) + fmt.Println(ConfigUsage()) + } + + return nil +} diff --git a/cmd/exec.go b/cmd/exec.go new file mode 100644 index 0000000..6b8c1d5 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "os" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" +) + +func init() { + var execCmd = &cobra.Command{ + Use: "exec", + Short: "Execute inline osmedeus scripts", + Long: core.Banner(), + RunE: runExec, + } + + execCmd.Flags().String("script", "", "Scripts to run (Multiple -s flags are accepted)") + execCmd.Flags().StringP("scriptFile", "S", "", "File contain list of scripts") + RootCmd.AddCommand(execCmd) +} + +func runExec(cmd *cobra.Command, _ []string) error { + script, _ := cmd.Flags().GetString("script") + scriptFile, _ := cmd.Flags().GetString("scriptFile") + + var scripts []string + if script != "" { + scripts = append(scripts, script) + } + if scriptFile != "" { + moreScripts := utils.ReadingFileUnique(scriptFile) + if len(moreScripts) > 0 { + scripts = append(scripts, moreScripts...) + } + } + + if len(scripts) == 0 { + utils.ErrorF("No scripts provided") + os.Exit(0) + } + runner, _ := core.InitRunner("example.com", options) + + for _, t := range options.Scan.Inputs { + // start to run scripts + options.Scan.ROptions = core.ParseInput(t, options) + for _, rscript := range scripts { + script = core.ResolveData(rscript, options.Scan.ROptions) + utils.InforF("Script: %v", script) + runner.RunScript(script) + } + } + + return nil +} diff --git a/cmd/health.go b/cmd/health.go new file mode 100644 index 0000000..5fc0c84 --- /dev/null +++ b/cmd/health.go @@ -0,0 +1,283 @@ +package cmd + +import ( + "fmt" + "os" + "path" + "sort" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" +) + +func init() { + var healthCmd = &cobra.Command{ + Use: "health", + Aliases: []string{"hea", "heal", "health", "healht"}, + Short: "Run diagnostics to check configurations", + Long: core.Banner(), + RunE: runHealth, + } + RootCmd.AddCommand(healthCmd) + healthCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runHealth(_ *cobra.Command, args []string) error { + if options.PremiumPackage { + fmt.Printf("šŸ’  Osmedeus Premium %s: Run diagnostics to ensure that everything is in arrange.\n", libs.VERSION) + } else { + fmt.Printf("šŸš€ Osmedeus %s: Run diagnostics to ensure that everything is in arrange.\n", libs.VERSION) + } + + sort.Strings(args) + var err error + for _, arg := range args { + switch arg { + case "store", "git", "storages", "stora": + err = checkStorages() + case "cloud", "dist", "provider": + err = checkCloud() + case "all", "a", "full": + err = checkStorages() + if err != nil { + fmt.Println(color.YellowString("āš ļøļø There is might be something wrong with your storages: %v\n", err)) + } + err = checkCloud() + if err != nil { + fmt.Println(color.YellowString("%s If you install osmedeus on a single machine then it's okay to ignore the cloud setup\n", "[!] Cloud config setup incorrectly.")) + } + err = generalCheck() + if err != nil { + fmt.Printf("ā€¼ļø There is might be something wrong with your setup: %v\n", color.HiRedString("%v", err)) + return nil + } + break + } + if err != nil { + fmt.Println(color.YellowString("āš ļøļø There is might be something wrong with your cloud or storages setup: %v\n", err)) + return nil + } + } + if len(args) > 0 { + return nil + } + + if err = generalCheck(); err != nil { + fmt.Printf("ā€¼ļø There is might be something wrong with your setup: %v\n", err) + return nil + } + + if err = listFlows(); err != nil { + fmt.Printf("ā€¼ļø There is might be something wrong with your workflow setup: %v\n", err) + return nil + } + + if err = listDefaultModules(); err != nil { + fmt.Printf("ā€¼ļø There is might be something wrong with your workflow setup: %v\n", err) + return nil + } + fmt.Printf(color.GreenString("\n🦾 Everything is in order. Happy Hacking 🦾\n")) + return nil +} + +func checkCloud() error { + // check packer program + if _, err := utils.RunCommandWithErr("packer -h"); err != nil { + if _, err := utils.RunCommandWithErr(fmt.Sprintf("%s -h", path.Join(options.Env.BinariesFolder, "packer"))); err != nil { + color.Red("[-] Packer program setup incorrectly") + return fmt.Errorf("error checking core programs: %v", fmt.Sprintf("%s -h", path.Join(options.Env.BinariesFolder, "packer"))) + } + } + + // check config files + if !utils.FileExists(options.CloudConfigFile) { + return fmt.Errorf("distributed cloud config doesn't exist: %v", path.Join(options.Env.CloudConfigFolder, "provider.yaml")) + } + if utils.DirLength(path.Join(options.Env.CloudConfigFolder, "providers")) == 0 { + return fmt.Errorf("providers file doesn't exist: %v", path.Join(options.Env.CloudConfigFolder, "providers")) + } + + // check SSH Keys + if !utils.FileExists(options.Cloud.SecretKey) { + keysDir := path.Dir(options.Cloud.SecretKey) + os.RemoveAll(keysDir) + utils.MakeDir(keysDir) + utils.DebugF("Generate SSH Key at: %v", options.Cloud.SecretKey) + if _, err := utils.RunCommandWithErr(fmt.Sprintf(`ssh-keygen -t ed25519 -f %s -q -N ''`, options.Cloud.SecretKey)); err != nil { + color.Red("[-] error generated SSH Key for cloud config at: %v", options.Cloud.SecretKey) + return fmt.Errorf("[-] error generated SSH Key for cloud config at: %v", options.Cloud.SecretKey) + } + } + if !utils.FileExists(options.Cloud.PublicKey) { + return fmt.Errorf("providers SSH Key missing: %v", options.Cloud.PublicKey) + } + + fmt.Printf("[+] Health Check Cloud Config: %s\n", color.GreenString("āœ”")) + return nil +} + +func checkStorages() error { + utils.DebugF("Checking storages setup") + if !execution.ValidGitURL(options.Storages["summary_repo"]) { + return fmt.Errorf("invalid git summary: %v", options.Storages["summary_repo"]) + } + + utils.DebugF("Check if your summary directory is exist or not: %v", options.Env.StoragesFolder) + if utils.DirLength(options.Env.StoragesFolder) < 1 { + return fmt.Errorf("storages folder doesn't exist: %v", options.Env.StoragesFolder) + } + + utils.DebugF("Check the secret key for git usage: %v", options.Storages["secret_key"]) + if !utils.FileExists(options.Storages["secret_key"]) { + return fmt.Errorf("secret key for git command doesn't exist: %v", options.Storages["secret_key"]) + } + + fmt.Printf("[+] Health Check Storages Config: %s\n", color.GreenString("āœ”")) + return nil +} + +func generalCheck() error { + exist := utils.FolderExists(options.Env.BaseFolder) + if !exist { + color.Red("[-] Core folder setup incorrect: %v", options.Env.BaseFolder) + return fmt.Errorf("error running diagnostics") + } + + // check core programs + var err error + // if _, err = utils.RunCommandWithErr("jaeles -h"); err != nil { + // color.Red("[-] Core program setup incorrectly") + // return fmt.Errorf("error checking core programs: %v", "jaeles") + // } + if _, err = utils.RunCommandWithErr("timeout --help"); err != nil { + color.Red("[-] Core program setup incorrectly") + return fmt.Errorf("error checking core programs: %v", "timeout") + } + if _, err = utils.RunCommandWithErr("amass -h"); err != nil { + color.Red("[-] Core program setup incorrectly") + return fmt.Errorf("error checking core programs: %v", "amass") + } + _, err = utils.RunCommandWithErr(fmt.Sprintf("%s -h", path.Join(options.Env.BinariesFolder, "httprobe"))) + if err != nil { + color.Red("[-] Core program setup incorrectly") + return fmt.Errorf("error checking core programs: %v", fmt.Sprintf("%s -h", path.Join(options.Env.BinariesFolder, "httprobe"))) + } + fmt.Printf("[+] Health Check Core Programs: %s\n", color.GreenString("āœ”")) + + // Check core signatures + okVuln := false + // if utils.DirLength("~/.jaeles/base-signatures/") > 0 || utils.DirLength("~/pro-signatures/") > 0 { + // okVuln = true + // } + + if utils.DirLength("~/nuclei-templates") > 0 { + okVuln = true + } + + if okVuln { + fmt.Printf("[+] Health Check Vulnerability scanning config: %s\n", color.GreenString("āœ”")) + } else { + color.Red("vulnerability scanning config setup incorrectly") + return fmt.Errorf("vulnerability scanning config setup incorrectly") + } + + // check data folder + if utils.FolderExists(options.Env.DataFolder) { + fmt.Printf("[+] Health Check Data Config: %s\n", color.GreenString("āœ”")) + } else { + color.Red("[-] Data setup incorrectly: %v", options.Env.DataFolder) + return fmt.Errorf("[-] Data setup incorrectly: %v", options.Env.DataFolder) + } + return nil +} + +func listFlows() error { + flows := core.ListFlow(options) + if len(flows) == 0 { + color.Red("[-] Error to list workflows: %s", options.Env.WorkFlowsFolder) + return fmt.Errorf("[-] Error to list workflows: %s", options.Env.WorkFlowsFolder) + } + fmt.Printf("[+] Health Check Workflows: %s\n", color.GreenString("āœ”")) + if options.PremiumPackage { + fmt.Printf("šŸ’Ž Making use of the premium workflow\n") + } + + var content [][]string + for _, flow := range flows { + parsedFlow, err := core.ParseFlow(flow) + if err != nil { + utils.ErrorF("Error parsing flow: %v", flow) + continue + } + + if parsedFlow.SkipIndexed { + continue + } + + row := []string{ + parsedFlow.Name, parsedFlow.Desc, + } + + content = append(content, row) + } + fmt.Printf("\nFound %v available workflows at: %s \n\n", color.HiGreenString("%v", len(content)), color.HiCyanString(options.Env.WorkFlowsFolder)) + + table := tablewriter.NewWriter(os.Stdout) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"Flow Name", "Description"}) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetColWidth(120) + table.AppendBulk(content) // Add Bulk Data + table.Render() + + h := color.HiCyanString("\nUsage:\n") + h += color.HiGreenString(" osmedeus scan -f %v", color.HiMagentaString("[flowName]")) + color.HiGreenString(" -t ") + color.HiMagentaString("[target]") + "\n" + fmt.Printf(h) + return nil +} + +func listDefaultModules() error { + defaultModule := path.Join(options.Env.WorkFlowsFolder, "default-modules") + modules := core.DefaultWorkflows(options) + + if len(modules) == 0 { + return fmt.Errorf("[-] Error to list default modules: %s", defaultModule) + } + + var content [][]string + for _, flow := range modules { + parsedModule, err := core.ParseModules(flow) + if err != nil { + utils.ErrorF("Error parsing flow: %v", flow) + continue + } + row := []string{ + parsedModule.Name, parsedModule.Desc, + } + content = append(content, row) + } + fmt.Printf("\nFound %v default modules at: %s \n\n", color.HiGreenString("%v", len(content)), color.HiCyanString(defaultModule)) + + table := tablewriter.NewWriter(os.Stdout) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"Module Name", "Description"}) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetColWidth(120) + table.AppendBulk(content) // Add Bulk Data + table.Render() + + h := color.HiCyanString("\nModule Usage:\n") + h += color.HiGreenString(" osmedeus scan -m %v", color.HiMagentaString("[moduleName]")) + color.HiGreenString(" -t ") + color.HiMagentaString("[target]") + "\n\n" + fmt.Printf(h) + return nil +} diff --git a/cmd/provider.go b/cmd/provider.go new file mode 100644 index 0000000..6a2c029 --- /dev/null +++ b/cmd/provider.go @@ -0,0 +1,266 @@ +package cmd + +import ( + "fmt" + "os" + "sync" + "time" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/distribute" + "github.com/j3ssie/osmedeus/provider" + "github.com/j3ssie/osmedeus/utils" + "github.com/olekukonko/tablewriter" + "github.com/panjf2000/ants" + "github.com/spf13/cobra" +) + +func init() { + var providerCmd = &cobra.Command{ + Use: "provider", + Aliases: []string{"provide", "pro"}, + Short: "Cloud utils for Distributed Mode", + Long: core.Banner(), + RunE: runProvider, + } + + providerCmd.PersistentFlags().StringVar(&options.Cloud.RawCommand, "cmd", "", "raw command") + providerCmd.PersistentFlags().BoolVar(&options.Cloud.CheckingLimit, "check", false, "Only check for limit of config") + providerCmd.PersistentFlags().StringVar(&options.Cloud.InstanceName, "name", "", "override instance name") + providerCmd.PersistentFlags().BoolVar(&options.Cloud.BackgroundRun, "bg", false, "Send command to instance and run it in background") + providerCmd.PersistentFlags().BoolVar(&options.Cloud.IgnoreConfigFile, "ic", false, "Ignore token in the config file") + providerCmd.PersistentFlags().IntVar(&options.Cloud.Retry, "retry", 10, "Number of retry when command is error") + providerCmd.PersistentFlags().StringSlice("id", []string{}, "Instance IDs that will be delete") + providerCmd.Flags().StringVar(&options.Cloud.ClearTime, "clear", "10m", "time to wait before next clear check") + providerCmd.PersistentFlags().BoolVar(&options.Cloud.ForEverHealthCheck, "for", false, "Continuesly running the health check forever") + + var providerWizard = &cobra.Command{ + Use: "wizard", + Aliases: []string{"wi", "wiz", "wizazrd"}, + Short: "Start a cloud config wizard", + Long: core.Banner(), + RunE: runCloudInit, + } + providerWizard.PersistentFlags().BoolVar(&options.Cloud.AddNewProvider, "add", false, "Open wizard to add new provider only") + + var providerBuild = &cobra.Command{ + Use: "build", + Aliases: []string{"buil"}, + Short: "Build snapshot image", + Long: core.Banner(), + RunE: runProviderBuild, + } + var providerCreate = &cobra.Command{ + Use: "create", + Aliases: []string{"cre"}, + Short: "Create cloud instance based on image", + Long: core.Banner(), + RunE: runProviderCreate, + } + + var providerHealth = &cobra.Command{ + Use: "health", + Aliases: []string{"hea", "heal", "health", "healht"}, + Short: "Conduct a health assessment on cloud instances that are currently operational", + Long: core.Banner(), + RunE: runCloudHealth, + } + + var providerValidate = &cobra.Command{ + Use: "validate", + Aliases: []string{"val"}, + Short: "Run validate of the existing cloud configs", + Long: core.Banner(), + RunE: runProviderValidate, + } + var providerList = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List all running instances", + Long: core.Banner(), + RunE: runProviderListing, + } + var providerDel = &cobra.Command{ + Use: "delete", + Aliases: []string{"del"}, + Short: "Delete instances by id", + Long: core.Banner(), + RunE: runProviderDelete, + } + var providerClear = &cobra.Command{ + Use: "clear", + Aliases: []string{"clea", "clr"}, + Short: "Clear all instances in the instances folders", + Long: core.Banner(), + RunE: runProviderClear, + } + providerCmd.AddCommand(providerWizard) + providerCmd.AddCommand(providerList) + providerCmd.AddCommand(providerDel) + providerCmd.AddCommand(providerValidate) + providerCmd.AddCommand(providerHealth) + providerCmd.AddCommand(providerCreate) + providerCmd.AddCommand(providerBuild) + providerCmd.AddCommand(providerClear) + providerCmd.SetHelpFunc(CloudHelp) + RootCmd.AddCommand(providerCmd) + providerCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runCloudHealth(_ *cobra.Command, _ []string) error { + distribute.CheckingCloudInstance(options) + if options.Cloud.ForEverHealthCheck { + for { + distribute.CheckingCloudInstance(options) + waitTime := utils.CalcTimeout(options.Cloud.ClearTime) + time.Sleep(time.Duration(waitTime) * time.Second) + } + } + + return nil +} + +func runProviderClear(_ *cobra.Command, _ []string) error { + distribute.ClearAllInstances(options) + return nil +} + +func runCloudInit(_ *cobra.Command, _ []string) error { + // interactive mode to show config file here + distribute.InitCloudSetup(options) + return nil +} + +func runProvider(_ *cobra.Command, args []string) error { + if len(args) == 0 { + fmt.Println(CloudUsage()) + } + return nil +} + +func runProviderBuild(_ *cobra.Command, _ []string) error { + options.Cloud.OnlyCreateDroplet = true + options.Cloud.ReBuildBaseImage = true + + // building multiple tokens + options.Cloud.TokensFile = utils.NormalizePath(options.Cloud.TokensFile) + if options.Cloud.TokensFile != "" { + tokens := utils.ReadingFileUnique(options.Cloud.TokensFile) + if len(tokens) == 0 { + utils.ErrorF("token file not found: %v", options.Cloud.TokensFile) + return nil + } + + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(options.Concurrency, func(i interface{}) { + lOptions := options + lOptions.Cloud.Token = i.(string) + + distribute.InitCloud(lOptions, lOptions.Scan.Inputs) + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + for _, token := range tokens { + wg.Add(1) + _ = p.Invoke(token) + } + wg.Wait() + return nil + + } + + distribute.InitCloud(options, options.Scan.Inputs) + return nil +} + +func runProviderCreate(_ *cobra.Command, _ []string) error { + options.Cloud.OnlyCreateDroplet = true + if len(options.Scan.Inputs) == 0 { + options.Scan.Inputs = append(options.Scan.Inputs, utils.RandomString(4)) + } + + distribute.InitCloud(options, options.Scan.Inputs) + return nil +} + +func runProviderValidate(_ *cobra.Command, _ []string) error { + cloudValidate() + return nil +} + +func runProviderListing(_ *cobra.Command, _ []string) error { + options.Cloud.BackgroundRun = true + utils.InforF("Listing all instances in the cloud provider(s) ...") + cloudRunners := distribute.GetClouds(options) + cloudListing(cloudRunners) + return nil +} + +func runProviderDelete(cmd *cobra.Command, _ []string) error { + options.Cloud.OnlyCreateDroplet = true + options.Cloud.BackgroundRun = true + cloudRunners := distribute.GetClouds(options) + InstanceIDs, _ := cmd.Flags().GetStringSlice("id") + + for _, InstanceID := range InstanceIDs { + for _, cloudRunner := range cloudRunners { + cloudRunner.Provider.DeleteInstance(InstanceID) + } + } + + cloudListing(cloudRunners) + return nil +} + +func cloudListing(cloudRunners []distribute.CloudRunner) { + var content [][]string + for _, cloudRunner := range cloudRunners { + cloudRunner.Provider.Action(provider.ListInstance) + for _, instance := range cloudRunner.Provider.Instances { + row := []string{ + cloudRunner.Provider.ProviderName, + cloudRunner.Provider.RedactedToken, + instance.InstanceID, + instance.InstanceName, + instance.IPAddress, + } + content = append(content, row) + } + } + table := tablewriter.NewWriter(os.Stderr) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"Provider", "Token", "Instance ID", "Instance Name", "IP Address"}) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.AppendBulk(content) // Add Bulk Data + table.Render() +} + +func cloudValidate() { + cloudRunners := distribute.GetClouds(options) + + var content [][]string + for _, cloudRunner := range cloudRunners { + row := []string{ + cloudRunner.Provider.ProviderName, + cloudRunner.Provider.RedactedToken, + cloudRunner.Provider.SSHKeyID, + cloudRunner.Provider.SnapshotID, + } + content = append(content, row) + } + table := tablewriter.NewWriter(os.Stderr) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"Provider", "Token", "SSH Key ID", "Osmedeus Snapshot ID"}) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.AppendBulk(content) // Add Bulk Data + table.Render() + +} diff --git a/cmd/queue.go b/cmd/queue.go new file mode 100644 index 0000000..bb79070 --- /dev/null +++ b/cmd/queue.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" + "github.com/spf13/cobra" +) + +func init() { + var queueCmd = &cobra.Command{ + Use: "queue", + Short: "Running the scan with input from queue file", + Aliases: []string{"queq", "quee", "queu", "que"}, + Long: core.Banner(), + RunE: runQueue, + } + queueCmd.PersistentFlags().StringVarP(&options.Queue.QueueFile, "queue-file", "Q", fmt.Sprintf("~/.%s/queue/queue-mimic.txt", libs.BINARY), "File contain list of target to simulate the queue") + queueCmd.PersistentFlags().BoolVar(&options.Queue.Add, "add", false, "Add new input to the queue file") + queueCmd.PersistentFlags().BoolVarP(&options.Queue.InputAsFile, "as-file", "F", false, "treat input as a file") + queueCmd.PersistentFlags().StringVar(&options.Queue.RawCommand, "cmd", "", "Raw Command to run") + queueCmd.SetHelpFunc(QueueHelp) + RootCmd.AddCommand(queueCmd) + queueCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runQueue(_ *cobra.Command, _ []string) error { + options.Queue.QueueFile = utils.NormalizePath(options.Queue.QueueFile) + options.Queue.QueueFolder = path.Dir(options.Queue.QueueFile) + if !utils.FolderExists(options.Queue.QueueFolder) { + utils.MakeDir(options.Queue.QueueFolder) + } + + if options.Queue.Add { + addInput() + return nil + } + + if !utils.FileExists(options.Queue.QueueFile) { + utils.WriteToFile(options.Queue.QueueFile, "") + } + + content := utils.ReadingFileUnique(options.Queue.QueueFile) + if len(content) == 0 { + utils.WarnF("Queue file is empty: %v", options.Queue.QueueFile) + utils.WarnF("Consider to add a input to it:" + color.HiGreenString(" osmedeus queue --add -t example.com")) + } else { + utils.InforF("Queue file is not empty: %v", color.HiCyanString(options.Queue.QueueFile)) + utils.InforF("Consider to delete it if you want a fresh scan") + } + + core.QueueWatcher(options) + return nil +} + +func addInput() { + utils.InforF("Adding new input to the queue file: %v", color.HiCyanString(options.Queue.QueueFile)) + // osmedeus queue --add -t example.com + if options.Queue.RawCommand == "" { + utils.WriteToFile(options.Queue.QueueFile, strings.Join(options.Scan.Inputs, "\n")) + return + } + + // osmedeus queue --add -t /tmp/cidr --cmd "osmedeus -t {{Input}} -m recon -w" + for _, target := range options.Scan.Inputs { + queueInput := libs.InputFormat{ + Input: target, + Command: options.Queue.RawCommand, + InputAsFile: options.Queue.InputAsFile, + } + if line, ok := jsoniter.MarshalToString(queueInput); ok == nil { + utils.AppendToContent(options.Queue.QueueFile, line) + } + } +} diff --git a/cmd/report.go b/cmd/report.go new file mode 100644 index 0000000..b132efe --- /dev/null +++ b/cmd/report.go @@ -0,0 +1,162 @@ +package cmd + +import ( + "io" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" +) + +func init() { + var reportCmd = &cobra.Command{ + Use: "report", + Short: "Show report of existing workspace", + Long: core.Banner(), + RunE: runReport, + } + + var lsCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List all current existing workspace", + Long: core.Banner(), + RunE: runReportList, + } + reportCmd.AddCommand(lsCmd) + + var viewCmd = &cobra.Command{ + Use: "view", + Aliases: []string{"vi", "v"}, + Short: "View all reports of existing workspace", + Long: core.Banner(), + RunE: runReportView, + } + reportCmd.AddCommand(viewCmd) + + var extractCmd = &cobra.Command{ + Use: "extract", + Aliases: []string{"ext", "ex", "e"}, + Short: "Extract a compressed workspace", + Long: core.Banner(), + RunE: runReportExtract, + } + extractCmd.Flags().StringVar(&options.Report.ExtractFolder, "dest", "", "Destination folder to extract data to") + reportCmd.AddCommand(extractCmd) + + var compressCmd = &cobra.Command{ + Use: "compress", + Aliases: []string{"com", "compr", "compres", "c"}, + Short: "Create a backup of the selected workspace", + Long: core.Banner(), + RunE: runReportCompress, + } + reportCmd.AddCommand(compressCmd) + + reportCmd.PersistentFlags().BoolVar(&options.Report.Raw, "raw", false, "Show all the file in the workspace") + reportCmd.PersistentFlags().StringVar(&options.Report.PublicIP, "ip", "", "Show downloadable file with the given IP address") + reportCmd.PersistentFlags().BoolVar(&options.Report.Static, "static", false, "Show report file with Prefix Static") + reportCmd.SetHelpFunc(ReportHelp) + RootCmd.AddCommand(reportCmd) + reportCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runReportList(_ *cobra.Command, _ []string) error { + core.ListWorkspaces(options) + return nil +} + +func runReportView(_ *cobra.Command, _ []string) error { + if options.Report.PublicIP == "" { + if utils.GetOSEnv("IPAddress", "127.0.0.1") == "127.0.0.1" { + options.Report.PublicIP = utils.GetOSEnv("IPAddress", "127.0.0.1") + } + } + + if options.Report.PublicIP == "0" || options.Report.PublicIP == "0.0.0.0" { + options.Report.PublicIP = getPublicIP() + } + + if len(options.Scan.Inputs) == 0 { + core.ListWorkspaces(options) + utils.InforF("Please select workspace to view report. Try %s", color.HiCyanString(`'osmedeus report view -t target.com'`)) + return nil + } + + for _, target := range options.Scan.Inputs { + core.ListSingleWorkspace(options, target) + } + return nil +} + +func runReportExtract(_ *cobra.Command, _ []string) error { + var err error + if options.Report.ExtractFolder == "" { + options.Report.ExtractFolder = options.Env.WorkspacesFolder + } else { + options.Report.ExtractFolder, err = filepath.Abs(filepath.Dir(options.Report.ExtractFolder)) + if err != nil { + return err + } + } + + for _, input := range options.Scan.Inputs { + core.ExtractBackup(input, options) + + target := strings.ReplaceAll(path.Base(input), ".tar.gz", "") + core.ListSingleWorkspace(options, target) + } + + return nil +} + +func runReportCompress(_ *cobra.Command, _ []string) error { + for _, target := range options.Scan.Inputs { + core.CompressWorkspace(target, options) + } + return nil +} + +func runReport(_ *cobra.Command, args []string) error { + if options.Report.PublicIP == "" { + if utils.GetOSEnv("IPAddress", "127.0.0.1") == "127.0.0.1" { + options.Report.PublicIP = utils.GetOSEnv("IPAddress", "127.0.0.1") + } + } + + if options.Report.PublicIP == "0" || options.Report.PublicIP == "0.0.0.0" { + options.Report.PublicIP = getPublicIP() + } + + if len(args) == 0 { + core.ListWorkspaces(options) + } + + return nil +} + +func getPublicIP() string { + utils.DebugF("getting Public IP Address") + req, err := http.Get("https://api.ipify.org") + if err != nil { + return "127.0.0.1" + } + defer req.Body.Close() + + body, err := io.ReadAll(req.Body) + if err != nil { + return "127.0.0.1" + } + return string(body) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..57e07ec --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,147 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" +) + +var options = libs.Options{} +var RootCmd = &cobra.Command{ + Use: libs.BINARY, + Short: fmt.Sprintf("%s - %s", libs.BINARY, libs.DESC), + Long: core.Banner(), +} + +// Execute main function +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + RootCmd.PersistentFlags().StringVar(&options.Env.RootFolder, "rootFolder", fmt.Sprintf("~/.%s/", libs.BINARY), "The main folder where all configurations are stored") + RootCmd.PersistentFlags().StringVar(&options.Env.BaseFolder, "baseFolder", fmt.Sprintf("~/%s-base/", libs.BINARY), "Base Folder which is store data, binaries and workflows") + RootCmd.PersistentFlags().StringVar(&options.Env.DataFolder, "dataFolder", fmt.Sprintf("~/%s-base/data", libs.BINARY), "Data folder which is store wordlists, payloads, etc") + RootCmd.PersistentFlags().StringVar(&options.Env.WorkFlowsFolder, "wfFolder", "", fmt.Sprintf("Custom Workflow folder (default will get from '$HOME/%s-base/workflow')", libs.BINARY)) + RootCmd.PersistentFlags().StringVar(&options.ConfigFile, "configFile", fmt.Sprintf("~/.%s/config.yaml", libs.BINARY), "Main configurations file") + + // Workspace folder + RootCmd.PersistentFlags().StringVarP(&options.Env.WorkspacesFolder, "wsFolder", "W", fmt.Sprintf("~/workspaces-%s", libs.BINARY), "The main data folder within the workspaces where all scan results are stored") + + // parse target as global flag + RootCmd.PersistentFlags().StringSliceVarP(&options.Scan.Inputs, "target", "t", []string{}, "The target you want to run/execute") + RootCmd.PersistentFlags().StringVarP(&options.Scan.InputList, "targets", "T", "", "List of target as a file") + + // Scan command + RootCmd.PersistentFlags().IntVarP(&options.Concurrency, "concurrency", "c", 1, "Concurrency level (recommend to keep it as 1 on machine has RAM smaller than 2GB)") + RootCmd.PersistentFlags().StringSliceVarP(&options.Scan.Modules, "module", "m", []string{}, "Target to running") + RootCmd.PersistentFlags().StringVarP(&options.Scan.Flow, "flow", "f", "general", "Flow name for running (default: general)") + RootCmd.PersistentFlags().StringVarP(&options.Scan.CustomWorkspace, "workspace", "w", "", "Name of workspace (default is same as target)") + RootCmd.PersistentFlags().StringSliceVarP(&options.Scan.Params, "params", "p", []string{}, "Custom params -p='foo=bar' (Multiple -p flags are accepted)") + RootCmd.PersistentFlags().StringVar(&options.Scan.SuffixName, "suffix", "", "Suffix string for file converted (default: randomly)") + RootCmd.PersistentFlags().IntVarP(&options.Threads, "threads-hold", "B", 0, "Threads hold for each module (default: number of CPUs)") + RootCmd.PersistentFlags().StringVar(&options.Tactics, "tactic", "default", "Choosing the tactic for running workflow from [default aggressive gently]") + RootCmd.PersistentFlags().StringVar(&options.Scan.ParamsFile, "params-file", "", "Custom file params --params-file=params.yaml") + + // cloud flags + RootCmd.PersistentFlags().BoolVar(&options.Cloud.EnableChunk, "chunk", false, "Enable chunk mode") + RootCmd.PersistentFlags().BoolVar(&options.Scan.RemoteCall, "from-remote", false, "Invocation from a remote machine") + RootCmd.PersistentFlags().IntVarP(&options.Cloud.NumberOfParts, "chunk-parts", "P", 0, "Number of chunks file to split (default: equal with concurrency)") + RootCmd.PersistentFlags().StringVar(&options.Cloud.ChunkInputs, "chunkFolder", "/tmp/chunk-inputs/", "Temp Folder to store chunk inputs") + RootCmd.PersistentFlags().StringVar(&options.Timeout, "timeout", "", "Global timeout for each step (e.g: 60s, 30m, 2h)") + RootCmd.PersistentFlags().StringVar(&options.Cloud.Size, "size", "", "Override Size of cloud provider (default will get from 'cloud/provider.yaml')") + RootCmd.PersistentFlags().StringVar(&options.Cloud.Region, "region", "", "Override Region of cloud provider (default will get from 'cloud/provider.yaml')") + RootCmd.PersistentFlags().StringVar(&options.Cloud.Token, "token", "", "Override token of cloud provider (default will get from 'cloud/provider.yaml')") + RootCmd.PersistentFlags().StringVar(&options.Cloud.TokensFile, "token-file", "", "File contains list token of cloud providers") + RootCmd.PersistentFlags().StringVar(&options.Cloud.Provider, "provider", "", "Provider config file (default will get from 'cloud/provider.yaml')") + RootCmd.PersistentFlags().BoolVar(&options.Cloud.ReBuildBaseImage, "rebuild", false, "Forced to rebuild the images event though the version didn't change") + + // mics option + RootCmd.PersistentFlags().StringVar(&options.LogFile, "log", "", fmt.Sprintf("Log File (default will store in '%s')", libs.LDIR)) + RootCmd.PersistentFlags().StringVarP(&options.ScanID, "sid", "s", "", "Scan ID to continue the scan without create new scan record") + RootCmd.PersistentFlags().BoolVarP(&options.Resume, "resume", "R", false, "Enable Resume mode to skip modules that have already been finished") + RootCmd.PersistentFlags().BoolVar(&options.Debug, "debug", false, "Enable Debug output") + RootCmd.PersistentFlags().BoolVarP(&options.Quite, "quite", "q", false, "Show only essential information") + RootCmd.PersistentFlags().BoolVar(&options.FullHelp, "hh", false, "Show full help message") + RootCmd.PersistentFlags().BoolVar(&options.WildCardCheck, "ww", false, "Check for wildcard target") + RootCmd.PersistentFlags().BoolVar(&options.DisableValidateInput, "nv", false, "Disable Validate Input") + RootCmd.PersistentFlags().BoolVar(&options.Update.NoUpdate, "nu", false, "Disable Update options") + RootCmd.PersistentFlags().BoolVarP(&options.EnableFormatInput, "format-input", "J", false, "Enable special input format") + RootCmd.PersistentFlags().IntVar(&options.MDCodeBlockLimit, "block-size", 10000, "Size limit for code block tags to before it's automatic truncation") + + // disable options + RootCmd.PersistentFlags().BoolVar(&options.NoNoti, "nn", false, "No notification") + RootCmd.PersistentFlags().BoolVar(&options.NoBanner, "nb", false, "No banner") + RootCmd.PersistentFlags().BoolVarP(&options.NoGit, "no-git", "N", false, "No git storage") + RootCmd.PersistentFlags().BoolVarP(&options.NoClean, "no-clean", "C", false, "No clean junk output") + RootCmd.PersistentFlags().BoolVar(&options.NoPreRun, "no-prerun", false, "Disable pre run scripts") + RootCmd.PersistentFlags().BoolVar(&options.NoPostRun, "no-postrun", false, "Disable post run scripts") + RootCmd.PersistentFlags().StringSliceVarP(&options.Exclude, "exclude", "x", []string{}, "Exclude module name (Multiple -x flags are accepted)") + RootCmd.PersistentFlags().BoolVarP(&options.CustomGit, "git", "g", false, "Use custom Git repo") + + // sync options + RootCmd.PersistentFlags().BoolVar(&options.EnableDeStorage, "des", false, "Enable Dedicated Storages") + RootCmd.PersistentFlags().BoolVar(&options.GitSync, "sync", false, "Enable Sync Check before doing git push") + RootCmd.PersistentFlags().IntVar(&options.SyncTimes, "sync-timee", 15, "Number of times to check before force push") + RootCmd.PersistentFlags().IntVar(&options.PollingTime, "poll-timee", 100, "Number of seconds to sleep before do next sync check") + RootCmd.PersistentFlags().BoolVar(&options.NoCdn, "no-cdn", false, "Disable CDN feature") + RootCmd.PersistentFlags().BoolVarP(&options.EnableBackup, "backup", "b", false, "Backup the result after the scan is done") + + // update options + RootCmd.PersistentFlags().BoolVar(&options.Update.IsUpdateBin, "bin", false, "Update binaries too") + RootCmd.PersistentFlags().BoolVar(&options.Update.EnableUpdate, "update", false, "Enable auto update") + RootCmd.PersistentFlags().StringVar(&options.Update.UpdateFolder, "update-folder", "/tmp/osm-update", "Folder to clone the update folder") + + RootCmd.SetHelpFunc(RootHelp) + cobra.OnInitialize(initConfig) + RootCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if options.JsonOutput { + options.Quite = true + } + + /* Really Start the program */ + utils.InitLog(&options) + utils.InitHTTPClient() + if err := core.InitConfig(&options); err != nil { + utils.ErrorF("config file does not writable: %v", options.ConfigFile) + utils.BlockF("fatal", "Make sure you are login as 'root user' if your installation done via root user") + } + core.ParsingConfig(&options) + + // parse inputs + if options.Scan.InputList != "" { + if utils.FileExists(options.Scan.InputList) { + options.Scan.Inputs = append(options.Scan.Inputs, utils.ReadingFileUnique(options.Scan.InputList)...) + } + } + + // detect if anything came from stdin + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + sc := bufio.NewScanner(os.Stdin) + for sc.Scan() { + target := strings.TrimSpace(sc.Text()) + if err := sc.Err(); err == nil && target != "" { + options.Scan.Inputs = append(options.Scan.Inputs, target) + } + } + } +} diff --git a/cmd/scan.go b/cmd/scan.go new file mode 100644 index 0000000..24156ee --- /dev/null +++ b/cmd/scan.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "os" + "strings" + "sync" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/panjf2000/ants" + "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func init() { + var scanCmd = &cobra.Command{ + Use: "scan", + Short: "Conduct a scan following a predetermined flow/module", + Long: core.Banner(), + RunE: runScan, + } + + scanCmd.SetHelpFunc(ScanHelp) + RootCmd.AddCommand(scanCmd) + scanCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runScan(_ *cobra.Command, _ []string) error { + utils.GoodF("%v %v by %v", cases.Title(language.Und, cases.NoLower).String(libs.BINARY), libs.VERSION, color.HiMagentaString(libs.AUTHOR)) + utils.InforF("Storing the log file to: %v", color.CyanString(options.LogFile)) + + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(options.Concurrency, func(i interface{}) { + // really start to scan + CreateRunner(i) + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + if options.Cloud.EnableChunk { + for _, target := range options.Scan.Inputs { + chunkTargets := HandleChunksInputs(target) + for _, chunkTarget := range chunkTargets { + wg.Add(1) + _ = p.Invoke(chunkTarget) + } + } + } else { + for _, target := range options.Scan.Inputs { + wg.Add(1) + _ = p.Invoke(strings.TrimSpace(target)) + } + } + + wg.Wait() + return nil +} + +func CreateRunner(j interface{}) { + target := j.(string) + if core.IsRootDomain(target) && options.Scan.Flow == "general" && len(options.Scan.Modules) == 0 { + utils.WarnF("looks like you scanning a subdomain '%s' with general flow. The result might be much less than usual", color.HiCyanString(target)) + utils.WarnF("Better input should be root domain with TLD like '-t target.com'") + } + + runner, err := core.InitRunner(target, options) + if err != nil { + utils.ErrorF("Error init runner with: %s", target) + return + } + runner.Start() +} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..1c4ebb6 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/server" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func init() { + var serverCmd = &cobra.Command{ + Use: "server", + Short: "Start Web Server", + Long: core.Banner(), + RunE: runServer, + } + serverCmd.Flags().String("host", "0.0.0.0", "IP address to bind the server") + serverCmd.Flags().String("port", "8000", "Port") + serverCmd.Flags().IntVar(&options.Server.PollingTime, "poll-time", 60, "Polling time to check next task") + serverCmd.Flags().BoolVar(&options.Server.DisableSSL, "disable-ssl", false, "Disable workspaces directory listing") + serverCmd.Flags().BoolVar(&options.Server.DisableWorkspaceListing, "disable-listing", false, "Disable workspaces directtory listing") + serverCmd.Flags().BoolVar(&options.Server.PreFork, "prefork", false, "Enable Prefork mode for the api server") + serverCmd.Flags().BoolVarP(&options.Server.NoAuthen, "no-auth", "A", false, "Disable authentication for the api server") + + serverCmd.SetHelpFunc(ServerHelp) + RootCmd.AddCommand(serverCmd) + serverCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runServer(cmd *cobra.Command, _ []string) error { + host, _ := cmd.Flags().GetString("host") + port, _ := cmd.Flags().GetString("port") + options.Server.Bind = fmt.Sprintf("%v:%v", host, port) + utils.GoodF("%v %v by %v", cases.Title(language.Und, cases.NoLower).String(libs.BINARY), libs.VERSION, color.HiMagentaString(libs.AUTHOR)) + server.StartServer(options) + return nil +} diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..a32daae --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "os" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cobra" +) + +func init() { + var updateCmd = &cobra.Command{ + Use: "update", + Short: "Check latest Update", + Long: core.Banner(), + RunE: runUpdate, + } + updateCmd.Flags().String("meta", "", "Custom MetaData URL") + updateCmd.Flags().Bool("F", false, "Shortcut for force update and clean old data at the same time") + updateCmd.Flags().BoolVar(&options.Update.ForceUpdate, "force", false, "Force update") + updateCmd.Flags().BoolVar(&options.Update.CleanOldData, "clean", false, "Clean up old Data") + updateCmd.Flags().BoolVar(&options.Update.VulnUpdate, "vuln", false, "Update Vulnerability Database only") + // generate update meta data + updateCmd.Flags().StringVar(&options.Update.UpdateURL, "update-url", "", "The script URL to download update") + updateCmd.Flags().StringVar(&options.Update.GenerateMeta, "gen", "", "Generate metadata for update") + RootCmd.AddCommand(updateCmd) + updateCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runUpdate(cmd *cobra.Command, _ []string) error { + meta, _ := cmd.Flags().GetString("meta") + if meta != "" { + options.Update.MetaDataURL = meta + } + + forcedUpdateandClean, _ := cmd.Flags().GetBool("F") + if forcedUpdateandClean { + options.Update.ForceUpdate = true + options.Update.CleanOldData = true + } + + if options.Update.GenerateMeta != "" { + core.GenerateMetaData(options) + return nil + } + + if options.Update.VulnUpdate { + core.UpdateVuln(options) + return nil + } + + var shouldUpdate bool + if options.Update.UpdateURL == "" { + options.Update.UpdateURL = core.GetUpdateURL(options) + } + + if options.Update.ForceUpdate { + shouldUpdate = true + utils.InforF("Force to Update latest release") + } else { + shouldUpdate = core.CheckUpdate(&options) + } + + if shouldUpdate { + err := core.RunUpdate(options) + if err != nil { + return err + } + } + + return nil +} diff --git a/cmd/usage.go b/cmd/usage.go new file mode 100644 index 0000000..e7a325e --- /dev/null +++ b/cmd/usage.go @@ -0,0 +1,313 @@ +package cmd + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/spf13/cobra" +) + +// RootUsage base help +func RootUsage() { + var h string + h += ScanUsage() + h += CloudUsage() + h += QueueUsage() + h += ReportUsage() + h += UtilsUsage() + fmt.Println(h) +} + +func ScanExmaples() string { + h := color.HiCyanString("Example Scan Commands:") + h += color.HiBlueString("\n ## Start a simple scan with default 'general' flow\n") + h += " osmedeus scan -t sample.com\n" + + h += color.HiBlueString("\n ## Start a general scan but exclude some of the module\n") + h += " osmedeus scan -t sample.com -x screenshot -x spider\n" + + h += color.HiBlueString("\n ## Start a scan directly with a module with inputs as a list of http domains like this https://sub.example.com\n") + h += " osmedeus scan -m content-discovery -t http-file.txt\n" + + h += color.HiBlueString("\n ## Initiate the scan using a speed option other than the default setting\n") + h += " osmedeus scan -f vuln --tactic gently -t sample.com\n" + h += " osmedeus scan --threads-hold=10 -t sample.com\n" + h += " osmedeus scan -B 5 -t sample.com\n" + + h += color.HiBlueString("\n ## Start a simple scan with other flow\n") + h += " osmedeus scan -f vuln -t sample.com\n" + h += " osmedeus scan -f extensive -t sample.com -t another.com\n" + h += " osmedeus scan -f urls -t list-of-urls.txt\n" + + h += color.HiBlueString("\n ## Scan list of targets\n") + h += " osmedeus scan -T list_of_targets.txt\n" + h += " osmedeus scan -f vuln -T list-of-targets.txt\n" + + h += color.HiBlueString("\n ## Performing static vulnerability scan and secret scan on a git repo\n") + h += " osmedeus scan -m repo-scan -t https://github.com/j3ssie/sample-repo\n" + h += " osmedeus scan -m repo-scan -t /tmp/source-code-folder\n" + h += " osmedeus scan -m repo-scan -T list-of-repo.txt\n" + + h += color.HiBlueString("\n ## Scan for CIDR with file contains CIDR with the format '1.2.3.4/24'\n") + h += " osmedeus scan -f cidr -t list-of-ciders.txt\n" + h += " osmedeus scan -f cidr -t '1.2.3.4/24' # this will auto convert the single input to the file and run\n" + + h += color.HiBlueString("\n ## Directly run on vuln scan and directory scan on list of domains\n") + h += " osmedeus scan -f domains -t list-of-domains.txt\n" + h += " osmedeus scan -f vuln-and-dirb -t list-of-domains.txt\n" + + h += color.HiBlueString("\n ## Use a custom wordlist\n") + h += " osmedeus scan -t sample.com -p 'wordlists={{Data}}/wordlists/content/big.txt'\n" + + h += color.HiBlueString("\n ## Use a custom wordlist\n") + h += " cat list_of_targets.txt | osmedeus scan -c 2\n" + + h += color.HiBlueString("\n ## Start a normal scan and backup entire workflow folder to the backup folder\n") + h += " osmedeus scan --backup -f domains -t list-of-subdomains.txt\n" + + h += color.HiBlueString("\n ## Start the scan with chunk inputs to review the output way more much faster\n") + h += " osmedeus scan --chunk --chunk-parts 20 -f cidr -t list-of-100-cidr.txt\n" + + h += color.HiBlueString("\n ## Continuously run the scan on a target right after it finished\n") + h += " osmedeus utils cron --for --cmd 'osmedeus scan -t example.com'\n" + + h += color.HiBlueString("\n ## Backing up all workspaces\n") + h += " ls ~/workspaces-osmedeus | osmedeus report compress\n" + + h += "\n" + return h +} + +func ScanUsage() string { + h := ScanExmaples() + h += color.HiCyanString("\nScan Usage:\n") + h += " osmedeus scan -f [flowName] -t [target] \n" + h += " osmedeus scan -m [modulePath] -T [targetsFile] \n" + h += " osmedeus scan -f /path/to/flow.yaml -t [target] \n" + h += " osmedeus scan -m /path/to/module.yaml -t [target] --params 'port=9200'\n" + h += " osmedeus scan -m /path/to/module.yaml -t [target] -l /tmp/log.log\n" + h += " osmedeus scan --tactic aggressive -m module -t [target] \n" + h += " cat targets | osmedeus scan -f sample\n" + + h += color.HiCyanString("\nPractical Scan Usage:\n") + h += " osmedeus scan -T list_of_targets.txt -W custom_workspaces\n" + h += " osmedeus scan -t target.com -w workspace_name --debug\n" + h += " osmedeus scan -f general -t sample.com\n" + h += " osmedeus scan --tactic aggressive -f general -t sample.com\n" + h += " osmedeus scan -f extensive -t sample.com -t another.com\n" + h += " cat list_of_urls.txt | osmedeus scan -f urls\n" + h += " osmedeus scan --threads-hold=15 -f cidr -t 1.2.3.4/24\n" + h += " osmedeus scan -m ~/.osmedeus/core/workflow/test/dirbscan.yaml -t list_of_urls.txt\n" + h += " osmedeus scan --wfFolder ~/custom-workflow/ -f your-custom-workflow -t list_of_urls.txt\n" + h += " osmedeus scan --chunk --chunk-part 40 -c 2 -f cidr -t list-of-cidr.txt\n" + return h +} + +func UtilsUsage() string { + h := color.HiCyanString("\nUtilities Usage:\n") + h += color.HiBlueString(" ## Health Utility\n") + h += " osmedeus health \n" + h += " osmedeus health git\n" + h += " osmedeus health cloud\n" + h += " osmedeus version --json \n" + h += "\n" + + h += color.HiBlueString(" ## Set the base threads hold\n") + h += " osmedeus config set --threads-hold=10\n" + h += "\n" + + h += color.HiBlueString(" ## Update utilities\n") + h += " osmedeus update \n" + h += " osmedeus update --vuln\n" + h += " osmedeus update --force --clean \n" + h += "\n" + + h += color.HiBlueString(" ## Workflow utilities\n") + h += " osmedeus workflow list \n" + h += " osmedeus workflow view -f general\n" + h += "\n" + + h += color.HiBlueString(" ## Tmux utilities\n") + h += " osmedeus utils tmux ls \n" + h += " osmedeus utils tmux logs -A -l 10 \n" + h += "\n" + + h += color.HiBlueString(" ## Process utilities\n") + h += " osmedeus utils ps \n" + h += " osmedeus utils ps --proc 'jaeles' \n" + h += "\n" + + h += color.HiBlueString(" ## List all the sub proccess running by osmedeus\n") + h += " osmedeus utils ps --osm \n" + h += "\n" + + h += color.HiBlueString(" ## Kill all the sub proccess running by osmedeus\n") + h += " osmedeus utils ps --osm --kill \n" + h += "\n" + + h += color.HiBlueString(" ## Cron utilities\n") + h += " osmedeus utils cron --cmd 'osmdeus scan -t example.com' --sch 60\n" + h += " osmedeus utils cron --for --cmd 'osmedeus scan -t example.com'\n" + + return h +} + +func ConfigUsage() string { + h := color.HiCyanString("\nConfig Usage:\n") + h += " osmedeus config [action] [OPTIONS] \n" + h += " osmedeus config init -p https://github.com/j3ssie/osmedeus-plugins\n" + h += " osmedeus config --user newusser --pass newpassword\n" + h += " osmedeus config clean \n" + h += " osmedeus config delete -t woskapce \n" + h += " osmedeus config delete -w workspace_name \n" + h += " osmedeus config set --threads-hold=10 \n" + return h +} + +func QueueUsage() string { + h := color.HiCyanString("\nQueue Usage:\n") + h += " osmedeus queue -Q /tmp/queue-file.txt -c 2\n" + h += " osmedeus queue --add -t example.com -Q /tmp/queue-file.txt \n" + return h +} + +func CloudUsage() string { + h := color.HiCyanString("\nProvider Usage:\n") + h += " osmedeus provider wizard \n" + h += " osmedeus provider validate \n" + h += " osmedeus provider build --token xxx --rebuild --ic\n" + + h += " osmedeus provider health --debug \n" + h += " osmedeus provider health --for \n" + h += " osmedeus provider create --name 'sample' \n" + h += " osmedeus provider delete --id 34317111 --id 34317112 \n" + h += " osmedeus provider list \n" + + h += color.HiCyanString("\nCloud Usage:\n") + h += " osmedeus cloud -f [flowName] -t [target] \n" + h += " osmedeus cloud -f [flowName] -T [targetFile] --no-del\n" + h += " osmedeus cloud -m [modulePath] -t [target] \n" + h += " osmedeus cloud -c 5 -f [flowName] -T [targetsFile] \n" + h += " osmedeus cloud --token xxx -c 5 -f [flowName] -T [targetsFile] \n" + h += " osmedeus cloud --chunk -c 5 -f [flowName] -t [targetsFile] \n" + + return h +} + +func ReportUsage() string { + h := color.HiCyanString("\nReport Usage:\n") + h += " osmedeus report list\n" + h += " osmedeus report extract -t target.com.tar.gz\n" + h += " osmedeus report extract -t target.com.tar.gz --dest .\n" + h += " osmedeus report compress -t target.com\n" + h += " osmedeus report view --raw -t target.com\n" + h += " osmedeus report view --static -t target.com\n" + h += " osmedeus report view --static --ip 0 -t target.com\n" + return h +} + +func ServerUsage() string { + h := color.HiCyanString("\nServer Usage:\n") + h += " osmedeus server --port 5000\n" + h += " osmedeus server --disable-ssl\n" + h += " osmedeus server -A --disable-ssl\n" + return h +} + +func QueueHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + fmt.Println(cmd.UsageString()) + h := QueueUsage() + fmt.Println(h) + printDocs(cmd) +} + +// ScanHelp scan help message +func ScanHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + h := ScanUsage() + fmt.Println(h) + printDocs(cmd) +} + +// CloudHelp scan help message +func CloudHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + h := CloudUsage() + fmt.Println(h) + printDocs(cmd) +} + +// ServerHelp scan help message +func ServerHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + h := ServerUsage() + fmt.Println(h) + printDocs(cmd) +} + +// ConfigHelp config help message +func ConfigHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + h := ConfigUsage() + + fmt.Println(h) + printDocs(cmd) +} + +// UtilsHelp utils help message +func UtilsHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + h := UtilsUsage() + fmt.Println(h) + printDocs(cmd) +} + +// ReportHelp utils help message +func ReportHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + h := ReportUsage() + fmt.Println(h) + printDocs(cmd) +} + +// RootHelp print help message +func RootHelp(cmd *cobra.Command, _ []string) { + fmt.Println(core.Banner()) + if options.FullHelp { + fmt.Println(cmd.UsageString()) + } + RootUsage() + printDocs(cmd) +} + +func printDocs(cmd *cobra.Command) { + if !options.FullHelp { + if cmd.Use == libs.BINARY { + fmt.Printf("šŸ’” For full help message, please run: %s\n", color.GreenString("osmedeus --hh")) + } else { + fmt.Printf("šŸ’” For full help message, please run: %s or %s\n", color.GreenString("osmedeus --hh"), color.GreenString("osmedeus "+cmd.Use+" --hh")) + } + } + fmt.Printf("šŸ“– Documentation can be found here: %s\n", color.GreenString(libs.DOCS)) +} diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..fe8541f --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,139 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" + "github.com/spf13/cobra" +) + +func init() { + var utilsCmd = &cobra.Command{ + Use: "utils", + Aliases: []string{"u", "util"}, + Short: "Utils to get some information from the system", + Long: core.Banner(), + RunE: runUtils, + } + + var psCmd = &cobra.Command{ + Use: "ps", + Short: "Utility to get information about running process", + Long: core.Banner(), + RunE: runPs, + } + psCmd.Flags().StringSlice("proc", []string{}, "Process name") + psCmd.Flags().Bool("kill", false, "Kill the all processes") + psCmd.Flags().Bool("osm", false, "Osmedeus related process only") + + var tmuxCmd = &cobra.Command{ + Use: "tmux", + Short: "Utility to get info from tmux", + Long: core.Banner(), + RunE: runTmux, + } + + tmuxCmd.Flags().BoolVarP(&options.Tmux.ApplyAll, "all", "A", false, "Apply for all tmux sessions") + tmuxCmd.Flags().StringVarP(&options.Tmux.SelectedWindow, "name", "n", "", "Apply for all tmux sessions") + tmuxCmd.Flags().StringVarP(&options.Tmux.Exclude, "exclude", "e", "server", "Exclude tmux session") + tmuxCmd.Flags().IntVarP(&options.Tmux.Limit, "limit", "l", 0, "Size of output content") + + var cronCmd = &cobra.Command{ + Use: "cron", + Short: "Utility to run command schedule", + Long: core.Banner(), + RunE: runCron, + } + cronCmd.Flags().IntVar(&options.Cron.Schedule, "sch", 0, "Number of minutes to schedule the job") + cronCmd.Flags().BoolVar(&options.Cron.Forever, "for", false, "Keep running forever right after the command done") + cronCmd.Flags().StringVar(&options.Cron.Command, "cmd", "", "Command to run") + + // add command + utilsCmd.PersistentFlags().BoolVar(&options.JsonOutput, "json", false, "Output as JSON") + utilsCmd.AddCommand(cronCmd) + utilsCmd.AddCommand(tmuxCmd) + utilsCmd.AddCommand(psCmd) + utilsCmd.SetHelpFunc(UtilsHelp) + RootCmd.AddCommand(utilsCmd) + + utilsCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runUtils(_ *cobra.Command, _ []string) error { + fmt.Println(UtilsUsage()) + return nil +} + +func runPs(cmd *cobra.Command, _ []string) error { + processes, _ := cmd.Flags().GetStringSlice("process") + osmRelated, _ := cmd.Flags().GetBool("osm") + killProcess, _ := cmd.Flags().GetBool("kill") + + if osmRelated { + pids := execution.ListAllOsmedeusProcess() + for _, pid := range pids { + if killProcess { + utils.RunOSCommand(fmt.Sprintf("kill -9 %v", pid)) + } + } + return nil + } + + if len(processes) == 0 { + processes = append(processes, libs.BINARY) + } + + for _, process := range processes { + pss := execution.GetOsmProcess(process) + for _, ps := range pss { + if options.JsonOutput { + if data, err := jsoniter.MarshalToString(ps); err == nil { + fmt.Println(data) + } + continue + } + fmt.Printf("pid:%v %s %v\n", color.HiCyanString("%v", ps.PID), color.HiMagentaString("--"), ps.Command) + } + } + + return nil +} + +func runTmux(_ *cobra.Command, args []string) error { + tmux, err := core.InitTmux(options) + if err != nil { + return err + } + + for _, argument := range args { + switch argument { + case "l", "ls", "list": + tmux.ListTmux() + case "t", "log", "logs", "tai", "tail": + tmux.CatchSession() + } + } + return nil +} + +func runCron(_ *cobra.Command, _ []string) error { + if options.Cron.Schedule == 0 && options.Cron.Forever == false { + return fmt.Errorf("missing '--sche' flag") + } + if options.Cron.Forever { + options.Cron.Schedule = -1 + } + core.RunCron(options.Cron.Command, options.Cron.Schedule) + return nil +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..7369feb --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,176 @@ +package cmd + +import ( + "fmt" + "os" + "runtime" + "strings" + "time" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" + + //"github.com/mackerelio/go-osstat/cpu" + //"github.com/mackerelio/go-osstat/memory" + "github.com/spf13/cobra" + //"github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/mem" +) + +func init() { + var versionCmd = &cobra.Command{ + Use: "version", + Short: "Show core version", + Long: core.Banner(), + RunE: runVersion, + } + versionCmd.Flags().BoolVarP(&options.Verbose, "verbose", "V", false, "Show stat info too") + versionCmd.Flags().BoolVar(&options.JsonOutput, "json", false, "Output as JSON") + RootCmd.AddCommand(versionCmd) +} + +func runVersion(_ *cobra.Command, _ []string) error { + if options.JsonOutput { + fmt.Println(PrintStat()) + return nil + } + + if !options.Verbose { + fmt.Printf("osmedeus %s by %s\n", libs.VERSION, libs.AUTHOR) + } else { + statInfo := PrintStat() + fmt.Printf("osmedeus %s by %s -- %s\n", libs.VERSION, libs.AUTHOR, statInfo) + } + return nil +} + +// StatData overview struct +type StatData struct { + CPU string `json:"cpu"` + Mem string `json:"mem"` + Name string `json:"name"` + Version string `json:"version"` +} + +// PrintStat print status +func PrintStat() string { + data := GetStat() + if data.CPU.Idle == 0.0 { + return strings.TrimSpace(utils.Emojif(":thought_balloon:", "not responding")) + } + var cpu string + cpuUsage := 100.0 - data.CPU.Idle + if cpuUsage <= 20.0 { + cpu = utils.Emojif(":green_circle:", " cpu: %0.2f", cpuUsage) + } else if (cpuUsage > 20.0) && (cpuUsage <= 50.0) { + cpu = utils.Emojif(":green_circle:", " cpu: %0.2f", cpuUsage) + } else if (cpuUsage > 50.0) && (cpuUsage <= 80.0) { + cpu = utils.Emojif(":orange_circle:", " cpu: %0.2f", cpuUsage) + } else { + cpu = utils.Emojif(":red_circle:", " cpu: %0.2f", cpuUsage) + } + + var mem string + memUsage := 100.0 - (data.Mem.Free/data.Mem.Total)*100 + if memUsage <= 20.0 { + mem = utils.Emojif(":green_circle:", " mem: %0.2f", memUsage) + } else if (memUsage > 20.0) && (memUsage <= 50.0) { + mem = utils.Emojif(":green_circle:", " mem: %0.2f", memUsage) + } else if (memUsage > 50.0) && (memUsage <= 80.0) { + mem = utils.Emojif(":orange_circle:", " mem: %0.2f", memUsage) + } else { + mem = utils.Emojif(":red_circle:", " mem: %0.2f", memUsage) + } + + name, _ := os.Hostname() + if options.JsonOutput { + stat := StatData{ + CPU: fmt.Sprintf("%v", cpuUsage), + Mem: fmt.Sprintf("%v", memUsage), + Name: name, + Version: fmt.Sprintf("osmedeus %s by %s", libs.VERSION, libs.AUTHOR), + } + if data, err := jsoniter.MarshalToString(stat); err == nil { + return data + } + } + + return fmt.Sprintf("%s: %12s - %s", name, strings.TrimSpace(cpu), strings.TrimSpace(mem)) +} + +type ServerStatData struct { + CPU struct { + System float64 + User float64 + Idle float64 + } + Mem struct { + Total float64 + Used float64 + Free float64 + Cached float64 + } +} + +// GetStat get stat data +// func GetStat() ServerStatData { +// var stat ServerStatData + +// before, err := cpu.Get() +// if err != nil { +// return stat +// } +// time.Sleep(time.Duration(1) * time.Second) +// after, err := cpu.Get() +// if err != nil { +// return stat +// } +// total := float64(after.Total - before.Total) +// stat.CPU.User = float64(after.User-before.User) / total * 100 +// stat.CPU.System = float64(after.System-before.System) / total * 100 +// stat.CPU.Idle = float64(after.Idle-before.Idle) / total * 100 +// // memory part +// memory, err := memory.Get() +// if err != nil { +// return stat +// } +// stat.Mem.Total = float64(memory.Total+memory.SwapTotal) / (1024 * 1024 * 1024) +// stat.Mem.Used = float64(memory.Used+memory.SwapUsed) / (1024 * 1024 * 1024) +// stat.Mem.Used = float64(memory.Used+memory.SwapUsed) / (1024 * 1024 * 1024) +// stat.Mem.Cached = float64(memory.Cached) / (1024 * 1024 * 1024) +// stat.Mem.Free = float64(memory.Free+memory.SwapFree) / (1024 * 1024 * 1024) +// return stat +// } + +// GetStat gets stat data +func GetStat() ServerStatData { + var stat ServerStatData + + var before runtime.MemStats + runtime.ReadMemStats(&before) + + time.Sleep(time.Duration(1) * time.Second) + + var after runtime.MemStats + runtime.ReadMemStats(&after) + + totalAlloc := float64(after.TotalAlloc - before.TotalAlloc) + sys := float64(after.Sys - before.Sys) + stat.CPU.User = totalAlloc / sys * 100 + stat.CPU.System = sys / sys * 100 + stat.CPU.Idle = 100 - stat.CPU.User - stat.CPU.System + + // Memory part + memory, err := mem.VirtualMemory() + if err != nil { + return stat + } + stat.Mem.Total = float64(memory.Total) / (1024 * 1024 * 1024) + stat.Mem.Used = float64(memory.Used) / (1024 * 1024 * 1024) + stat.Mem.Cached = float64(memory.Cached) / (1024 * 1024 * 1024) + stat.Mem.Free = float64(memory.Free) / (1024 * 1024 * 1024) + + return stat +} diff --git a/cmd/workflow.go b/cmd/workflow.go new file mode 100644 index 0000000..3eebf1b --- /dev/null +++ b/cmd/workflow.go @@ -0,0 +1,200 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/utils" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" +) + +func init() { + + var workflowCmd = &cobra.Command{ + Use: "workflow", + Aliases: []string{"wf", "wl", "workflows", "wfs", "work", "works"}, + Short: "Listing all available workflows", + Long: core.Banner(), + } + + var workflowListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "Listing all available workflows", + Long: core.Banner(), + RunE: runWorkflow, + } + + var workflowViewCmd = &cobra.Command{ + Use: "view", + Aliases: []string{"viwe", "ve", "vi", "v"}, + Short: "View details of a workflow", + Long: core.Banner(), + RunE: runWorkflowView, + } + workflowViewCmd.Flags().Bool("all", false, "View all of the workflows") + + workflowCmd.AddCommand(workflowViewCmd) + workflowCmd.AddCommand(workflowListCmd) + workflowCmd.SetHelpFunc(UtilsHelp) + RootCmd.AddCommand(workflowCmd) + + workflowCmd.PreRun = func(cmd *cobra.Command, args []string) { + if options.FullHelp { + cmd.Help() + os.Exit(0) + } + } +} + +func runWorkflow(cmd *cobra.Command, _ []string) error { + listFlows() + fmt.Printf("\n------------------------------------------------------------\n") + listDefaultModules() + fmt.Printf("šŸ’” For full help message, please run: %s or %s\n", color.GreenString("osmedeus --hh"), color.GreenString("osmedeus scan --hh")) + return nil +} + +func runWorkflowView(cmd *cobra.Command, _ []string) error { + allFlows := core.ListFlow(options) + viewAll, _ := cmd.Flags().GetBool("all") + + if viewAll { + for _, flow := range allFlows { + err := viewWorkflow(flow) + if err != nil { + utils.ErrorF("Error viewing workflow: %v", err) + } + fmt.Printf("\n------------------------------------------------------------\n\n") + } + } else { + err := viewWorkflow(options.Scan.Flow) + if err != nil { + utils.ErrorF("Error viewing workflow: %v", err) + } + } + + h := color.HiCyanString("\nšŸ“„ Sample Usage:\n") + h += color.HiGreenString(" osmedeus scan -f %v", color.HiMagentaString(options.Scan.Flow)) + color.HiGreenString(" -t ") + color.HiMagentaString("[target]") + "\n" + h += color.HiGreenString(" osmedeus scan -f %v", color.HiMagentaString(options.Scan.Flow)) + color.HiGreenString(" -t ") + color.HiMagentaString("[target]") + color.HiGreenString(" -p ") + color.HiMagentaString("'enableSomething=false'") + "\n\n" + fmt.Printf(h) + + fmt.Printf("šŸ’” To list all of the workflows available, please run: %s\n", color.GreenString("osmedeus workflow ls")) + fmt.Printf("šŸ’” For full help message, please run: %s or %s\n", color.GreenString("osmedeus --hh"), color.GreenString("osmedeus scan --hh")) + return nil +} + +func viewWorkflow(workflowName string) error { + fmt.Printf("šŸ“– Viewing workflow detail: %v\n\n", color.GreenString(workflowName)) + allFlows := core.ListFlow(options) + flows := core.SelectFlow(workflowName, options) + if len(flows) == 0 { + utils.ErrorF("Flow not found in any of existing workflow [%v]", color.HiYellowString(strings.Join(allFlows, ", "))) + return fmt.Errorf("Flow %s not found", workflowName) + } + selectedWorkflow := flows[0] + + var content [][]string + parsedFlow, err := core.ParseFlow(selectedWorkflow) + if err != nil { + utils.ErrorF("Error parsing flow: %v", selectedWorkflow) + return err + } + + var totalSteps, totalModules int + parameters := make(map[string]string) + for _, param := range parsedFlow.Params { + for k, v := range param { + parameters[k] = v + } + } + + for _, routine := range parsedFlow.Routines { + // select module depend on the flow type + if routine.FlowFolder != "" { + parsedFlow.Type = routine.FlowFolder + } else { + parsedFlow.Type = parsedFlow.DefaultType + } + + modules := core.SelectModules(routine.Modules, options) + + // loop through all modules to get the parameters + for _, module := range modules { + parsedModule, err := core.ParseModules(module) + if err != nil || parsedModule.Name == "" { + continue + } + for _, param := range parsedModule.Params { + for k, v := range param { + + _, exist := parameters[k] + if parsedFlow.ForceParams && exist { + utils.DebugF("Skip override param: %v --> %v", k, v) + continue + } + parameters[k] = v + } + + } + totalSteps += len(parsedModule.Steps) + totalModules++ + } + } + + var toggleFlags, skippingFlags []string + for key, value := range parameters { + if value == "true" { + value = color.GreenString(value) + } else if value == "false" { + value = color.RedString(value) + } else { + + value = color.CyanString(value) + } + + if strings.HasPrefix(key, "enable") { + toggleFlags = append(toggleFlags, fmt.Sprintf("%v=%v", key, value)) + } + + if strings.HasPrefix(key, "skip") { + skippingFlags = append(skippingFlags, fmt.Sprintf("%v=%v", key, value)) + } + } + + workflowInfo := fmt.Sprintf("Name: %v", color.HiCyanString(parsedFlow.Name)) + ", " + fmt.Sprintf("Total Steps: %v", color.HiCyanString("%v", totalSteps)) + ", " + fmt.Sprintf("Total Modules: %v", color.HiCyanString("%v", totalModules)) + content = append(content, []string{ + "Workflow Information", workflowInfo, + }) + content = append(content, []string{ + "Description", parsedFlow.Desc, + }) + + content = append(content, []string{ + "Toggleable parameter", strings.Join(toggleFlags, ", "), + }) + + content = append(content, []string{ + "Skippable parameter", strings.Join(skippingFlags, ", "), + }) + + if parsedFlow.Usage != "" { + content = append(content, []string{ + "Examples Commands", strings.TrimSpace(parsedFlow.Usage), + }) + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetRowLine(true) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetColWidth(120) + table.SetAutoWrapText(false) + table.AppendBulk(content) + table.Render() + + return nil +} diff --git a/core/backup.go b/core/backup.go new file mode 100644 index 0000000..2f422fb --- /dev/null +++ b/core/backup.go @@ -0,0 +1,68 @@ +package core + +import ( + "os" + "path" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +// in workflow file +// Compress('{{Backup}}/{{Workspace}}.tar.gz', '{{Output}}') +// Decompress('{{Output}}', '{{Backup}}/{{Workspace}}.tar.gz') + +func (r *Runner) BackupWorkspace() { + utils.InforF("Backing up the workspace: %v", r.Target["Workspace"]) + outputDir := r.Target["Output"] + dest := path.Join(r.Opt.Env.BackupFolder, r.Target["Workspace"]) + ".tar.gz" + if utils.FileExists(dest) { + os.Remove(dest) + } + + execution.Compress(dest, outputDir) + if utils.FileExists(dest) { + utils.GoodF("Backup workspace save at %s", color.HiMagentaString(dest)) + } +} + +func CompressWorkspace(target string, opt libs.Options) { + utils.InforF("Backing up the workspace: %v", color.HiCyanString(target)) + outputDir := path.Join(opt.Env.WorkspacesFolder, target) + if utils.FolderLength(outputDir) == 0 { + utils.ErrorF("Workspace is empty: %s", outputDir) + return + } + + dest := path.Join(opt.Env.BackupFolder, target) + ".tar.gz" + if utils.FileExists(dest) { + os.Remove(dest) + } + + execution.Compress(dest, outputDir) + if utils.FileExists(dest) { + utils.InforF("The workspace has been backed up and saved in %s", color.HiMagentaString(dest)) + } +} + +func ExtractBackup(src string, opt libs.Options) { + if !utils.FileExists(src) { + utils.ErrorF("Backup file not found: %s", src) + return + } + + target := strings.ReplaceAll(path.Base(src), ".tar.gz", "") + dest := path.Join(opt.Report.ExtractFolder, target) + if !strings.HasSuffix(dest, "/") { + dest += "/" + } + + if utils.FolderExists(dest) { + utils.MakeDir(dest) + } + execution.Decompress(dest, src) + utils.GoodF("Extracting the %v to %s", color.HiCyanString(target), color.HiMagentaString(dest)) +} diff --git a/core/banner.go b/core/banner.go new file mode 100644 index 0000000..95e0ff5 --- /dev/null +++ b/core/banner.go @@ -0,0 +1,63 @@ +package core + +import ( + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" +) + +// Banner print ascii banner +func Banner() string { + version := color.HiWhiteString(libs.VERSION) + author := color.MagentaString(libs.AUTHOR) + //W := color.HiWhiteString(``) + b := color.GreenString(``) + + b += color.GreenString(` + + .;1tfLCL1, + .,,..;i;f0G; + ,:,tCC. ... + ;i:fCL,1LLtf1i;, + .,::tCL1LC1::;, .,, + ;1:tCL,tLt,1: + ,::tLf, 1Lf;::. + .ii:tLt. .1Lf;i1. + ,:;tf1 1ft;:: + .1;:tf1 `) + color.HiWhiteString(` ,i1t1, `) + color.GreenString(` ift;;1, + ,i:t;f.`) + color.HiWhiteString(` ,LLffLL: `) + color.GreenString(` tft;i: + .;:;fff `) + color.HiWhiteString(` .LCLLLf,`) + color.GreenString(` 1ffi:;. + :fi;Lff1. `) + color.HiWhiteString(`,;;:`) + color.GreenString(` ifffi;f; + .:::tCLLfi:,,:ifLfLt::;. + ,11:1CCCCCLLLLLLf1;1t: + .it;:;1fLLLLfft1;:;ti. + ,:;::;;;;;;;;;;, + .,::::::::,. + `) + + // + // + //b += "\n\t" + color.GreenString(` @@@@@@`) + //b += "\n\t" + color.GreenString(` .@@' '@@.`) + //b += "\n\t" + color.GreenString(` :@ @:`) + //b += "\n\t" + color.GreenString(` :@ %v:@`, W) + color.GreenString(` @:`) + //b += "\n\t" + color.GreenString(` :@ %v:@`, W) + color.GreenString(` @:`) + // + //b += "\n\t" + color.GreenString(` :@ @:`) + //b += "\n\t" + color.GreenString(` '@@. .@@'`) + //b += "\n\t" + color.GreenString(` @@@@@@`) + //b += "\n\t" + color.GreenString(` @@`) + //b += "\n\t" + color.HiCyanString(` @ `) + color.GreenString(`@@`) + color.HiCyanString(` @`) + //b += "\n\t" + color.HiWhiteString(` +@@`) + color.GreenString(` @@ `) + color.HiWhiteString(` @@+`) + //b += "\n\t" + color.GreenString(` @@:@#@,@@,@#@:@@`) + //b += "\n\t" + color.GreenString(` ;@+@@'#@@@@#'@@+@;`) + //b += "\n\t" + color.GreenString(` @+ #@@ @@ @@# +@`) + //b += "\n\t" + color.GreenString(` @@ @+'@@@@@@'+@ @@`) + //b += "\n\t" + color.GreenString(` @. @ ;@@; @ .@`) + //b += "\n\t" + color.BlueString(` #@ '@ @; @#`) + + b += "\n\n\t" + color.GreenString(` Osmedeus Next Generation %v`, version) + color.GreenString(` by %v`, author) + b += "\n\n" + color.HiCyanString(` %s`, libs.DESC) + "\n" + b += "\n" + color.HiWhiteString(` ĀÆ\_(惄)_/ĀÆ`) + "\n\n" + color.Unset() + return b +} diff --git a/core/config.go b/core/config.go new file mode 100644 index 0000000..f86ffac --- /dev/null +++ b/core/config.go @@ -0,0 +1,382 @@ +package core + +import ( + "fmt" + "os" + "path" + "path/filepath" + "runtime" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/provider" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cast" + "github.com/spf13/viper" +) + +var v *viper.Viper + +// InitConfig Init the config +func InitConfig(options *libs.Options) error { + // ~/.osmedeus + RootFolder := filepath.Dir(utils.NormalizePath(options.ConfigFile)) + if !utils.FolderExists(RootFolder) { + if err := os.MkdirAll(RootFolder, 0750); err != nil { + return err + } + } + + // Base folder + // ~/osmedeus-base + BaseFolder := utils.NormalizePath(options.Env.BaseFolder) + if !utils.FolderExists(BaseFolder) { + fmt.Printf("%v Base folder not found at: %v\n", color.RedString("[Panic]"), color.HiGreenString(BaseFolder)) + fmt.Printf(color.HiYellowString("[!]")+" Consider running the installation script first: %v\n", color.HiGreenString("bash <(curl -fsSL %v)", libs.INSTALL)) + fmt.Printf(color.HiYellowString("[!]")+" Or better visit the installation guide at: %v\n", color.HiMagentaString("https://docs.osmedeus.org/installation/")) + os.Exit(-1) + } + + // load all the tokens + options.TokenConfigFile = path.Join(BaseFolder, "token/osm-var.yaml") + if !utils.FolderExists(path.Dir(options.TokenConfigFile)) { + utils.MakeDir(path.Dir(options.TokenConfigFile)) + } + + options.Env.WorkspacesFolder = utils.NormalizePath(options.Env.WorkspacesFolder) + if !utils.FolderExists(options.Env.WorkspacesFolder) { + utils.MakeDir(options.Env.WorkspacesFolder) + } + + // init config + options.ConfigFile = utils.NormalizePath(options.ConfigFile) + v = viper.New() + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(options.ConfigFile) + v.AddConfigPath(path.Dir(options.ConfigFile)) + + err := v.ReadInConfig() + if err != nil { + secret := utils.GenHash(utils.RandomString(8) + utils.GetTS())[:32] // only 32 char + prefix := secret[len(secret)-20 : len(secret)-1] + + // set some default config if config file doesn't exist + v.SetDefault("Server", map[string]string{ + "bind": "0.0.0.0:8000", + "cors": "*", + "secret": secret, + "prefix": prefix, + "ui": path.Join(RootFolder, "server/ui"), + "cert_file": path.Join(RootFolder, "server/ssl/cert.pem"), + "key_file": path.Join(RootFolder, "server/ssl/key.pem"), + "master_pass": "", + }) + + v.SetDefault("Tactic", map[string]any{ + "default": runtime.NumCPU() * 8, // 8,16,32,64 + "aggressive": runtime.NumCPU() * 16, + "gently": runtime.NumCPU() * 2, + }) + + v.SetDefault("Mics", map[string]string{ + "docs": utils.GetOSEnv("OSM_DOCS", libs.DOCS), + }) + + // DB connection config + dbPath := utils.NormalizePath(path.Join(RootFolder, "sqlite.db")) + v.SetDefault("Database", map[string]string{ + "db_host": utils.GetOSEnv("DB_HOST", "127.0.0.1"), + "db_port": utils.GetOSEnv("DB_PORT", "3306"), + "db_name": utils.GetOSEnv("DB_NAME", "osm-core"), + "db_user": utils.GetOSEnv("DB_USER", "root"), + "db_pass": utils.GetOSEnv("DB_PASS", ""), + // default will be file system + "db_path": utils.GetOSEnv("DB_PATH", dbPath), + // sqlite or mysql + "db_type": utils.GetOSEnv("DB_TYPE", "filesystem"), + }) + + // default user + password := utils.GenHash(utils.GetTS())[:15] + v.SetDefault("Client", map[string]string{ + "username": "osmedeus", + "password": password, + "jwt": "", + "dest": "http://127.0.0.1:8000", + }) + + v.SetDefault("Environments", map[string]string{ + // RootFolder --> ~/.osmedeus/ + "storages": path.Join(RootFolder, "storages"), + "backups": path.Join(RootFolder, "backups"), + "provider_config": path.Join(RootFolder, "provider"), + "instances": path.Join(RootFolder, "instances"), + + // store all the result + "workspaces": options.Env.WorkspacesFolder, + + // this update occasionally + // BaseFolder --> ~/osmedeus-base/ + "workflows": path.Join(BaseFolder, "workflow"), + "binaries": path.Join(BaseFolder, "binaries"), + "data": path.Join(BaseFolder, "data"), + "cloud_config": path.Join(BaseFolder, "cloud"), + }) + + if err := v.WriteConfigAs(options.ConfigFile); err != nil { + utils.ErrorF("Error writing config file: %s", err) + } + + utils.InforF("Created a new configuration file at %s", color.HiCyanString(options.ConfigFile)) + } + + if isWritable, _ := utils.IsWritable(options.ConfigFile); isWritable { + utils.ErrorF("config file does not writable: %v", color.HiCyanString(options.ConfigFile)) + utils.BlockF("fatal", "Make sure you are login as 'root user' if your installation done via root user") + os.Exit(-1) + } + SetupOSEnv(options) + return nil +} + +func LoadConfig(options *libs.Options) *viper.Viper { + v = viper.New() + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(options.ConfigFile) + v.AddConfigPath(path.Dir(options.ConfigFile)) + + if err := v.ReadInConfig(); err != nil { + utils.ErrorF("Error reading config file, %s", err) + } + return v +} + +func ParsingConfig(options *libs.Options) { + v = LoadConfig(options) + GetEnv(options) + GetServer(options) + GetClient(options) + SetupOpt(options) + // get the config for cloud provider + GetCloud(options) +} + +// GetEnv get environment options +func GetEnv(options *libs.Options) { + envs := v.GetStringMapString("Environments") + + options.Env.BinariesFolder = utils.NormalizePath(envs["binaries"]) + utils.MakeDir(options.Env.BinariesFolder) + + if options.Env.WorkFlowsFolder != "" { + options.Env.WorkFlowsFolder = utils.NormalizePath(options.Env.WorkFlowsFolder) + } + + options.Env.DataFolder = utils.NormalizePath(envs["data"]) + utils.MakeDir(options.Env.DataFolder) + // ose folder + options.Env.OseFolder = path.Join(options.Env.BaseFolder, "ose") + utils.MakeDir(options.Env.DataFolder) + options.Env.BackupFolder = utils.NormalizePath(envs["backups"]) + utils.MakeDir(options.Env.BackupFolder) + options.Env.UIFolder = path.Join(options.Env.BaseFolder, "ui") + + // local data + options.Env.StoragesFolder = utils.NormalizePath(envs["storages"]) + utils.MakeDir(options.Env.StoragesFolder) + options.Env.WorkspacesFolder = utils.NormalizePath(envs["workspaces"]) + utils.MakeDir(options.Env.WorkspacesFolder) + + customWorkflow := utils.GetOSEnv("CUSTOM_OSM_WORKFLOW", "CUSTOM_OSM_WORKFLOW") + if customWorkflow != "CUSTOM_OSM_WORKFLOW" && options.Env.WorkFlowsFolder == "" { + options.Env.WorkFlowsFolder = utils.NormalizePath(customWorkflow) + } + + if options.Env.WorkFlowsFolder == "" { + options.Env.WorkFlowsFolder = utils.NormalizePath(envs["workflows"]) + } + + // @NOTE: well of course you can rebuild the core engine binary to bypass this check + // However, the premium package primarily focuses on exclusive workflow and specialized wordlists. + // see more about it here: https://docs.osmedeus.org/faq/#premium-package-related-questions + // and https://docs.osmedeus.org/premium/ + if utils.FileExists(path.Join(options.Env.WorkFlowsFolder, "premium.md")) { + options.PremiumPackage = true + } + + // cloud stuff + + // ~/.osmedeus/providers/ + options.Env.ProviderFolder = utils.NormalizePath(envs["provider_config"]) + options.Env.InstancesFolder = utils.NormalizePath(envs["instances"]) + if options.Env.InstancesFolder == "" { + options.Env.InstancesFolder = utils.NormalizePath(path.Join(options.Env.RootFolder, "instances")) + } + utils.MakeDir(options.Env.ProviderFolder) + utils.MakeDir(options.Env.InstancesFolder) + + // ~/osmedeus-base/clouds/ + options.Env.CloudConfigFolder = utils.NormalizePath(envs["cloud_config"]) +} + +// SetupOpt get storage repos +func SetupOpt(options *libs.Options) { + // auto append PATH with Plugin folder + osPATH := utils.GetOSEnv("PATH", "PATH") + if !strings.Contains(osPATH, options.Env.BinariesFolder) { + utils.DebugF("Append $PATH with: %s", options.Env.BinariesFolder) + os.Setenv("PATH", fmt.Sprintf("%s:%s", osPATH, strings.TrimRight(options.Env.BinariesFolder, "/"))) + } + + tactics := v.GetStringMap("Tactic") + options.Tactics = strings.ToLower(options.Tactics) + options.ThreadsHold.Default = cast.ToInt(tactics["default"]) + options.ThreadsHold.Aggressive = cast.ToInt(tactics["aggressive"]) + options.ThreadsHold.Gently = cast.ToInt(tactics["gently"]) + + // try to autocorrect the tactic name + switch options.Tactics { + case "aggressive", "agg", "aggr", "aggrsive": + options.Tactics = "aggressive" + case "gently", "gen", "gent", "gentl": + options.Tactics = "gently" + } + defaultThreadsHold := cast.ToInt(tactics[options.Tactics]) + + if defaultThreadsHold == 0 { + utils.ErrorF("tactic %s not found, switching to the %v one", color.HiRedString(options.Tactics), color.HiYellowString("default")) + defaultThreadsHold = cast.ToInt(tactics["default"]) + options.Tactics = "default" + // in case you're still using the old config + if defaultThreadsHold == 0 { + defaultThreadsHold = 4 + } + } + + // override if you put --threads-hold flag + if options.Threads == 0 { + options.Threads = defaultThreadsHold + } + + /* some special conditions below */ + + // change {{.Storage}} from ~/.osmedeus/storages to ~/.osmedeus/destorages + if options.EnableDeStorage { + utils.DebugF("Dedicated Storage Enabled") + options.Env.StoragesFolder = options.Git.DeStorage + if !utils.FolderExists(options.Env.StoragesFolder) { + utils.MakeDir(options.Env.StoragesFolder) + } + } +} + +func GetCloud(options *libs.Options) { + if !options.PremiumPackage { + return + } + + // ~/osemedeus-base/cloud/provider.yaml + cloudConfigFile := path.Join(options.Env.CloudConfigFolder, "provider.yaml") + + options.CloudConfigFile = cloudConfigFile + utils.DebugF("Parsing cloud config from: %s", color.HiCyanString(options.CloudConfigFile)) + providerConfigs, err := provider.ParseProvider(options.CloudConfigFile) + + if err != nil { + utils.InforF("šŸ’” You can start the wizard with the command: %s", color.HiCyanString("%s provider wizard", libs.BINARY)) + } + + options.Cloud.BuildRepo = providerConfigs.Builder.BuildRepo + options.Cloud.SecretKey = utils.NormalizePath(providerConfigs.Builder.SecretKey) + options.Cloud.PublicKey = utils.NormalizePath(providerConfigs.Builder.PublicKey) + if options.Cloud.SecretKey == "" { + options.Cloud.SecretKey = path.Join(options.Env.CloudConfigFolder, "ssh/cloud") + options.Cloud.PublicKey = path.Join(options.Env.CloudConfigFolder, "ssh/cloud.pub") + } + + // check SSH Keys + if !utils.FileExists(options.Cloud.SecretKey) { + keysDir := path.Dir(options.Cloud.SecretKey) + os.RemoveAll(keysDir) + utils.MakeDir(keysDir) + + utils.InforF("Generate SSH Key at: %v", options.Cloud.SecretKey) + var err error + _, err = utils.RunCommandWithErr(fmt.Sprintf(`ssh-keygen -t ed25519 -f %s -q -N ''`, options.Cloud.SecretKey)) + if err != nil { + color.Red("[-] error generated SSH Key for cloud config at: %v", options.Cloud.SecretKey) + return + } + } + + if utils.FileExists(options.Cloud.SecretKey) { + utils.DebugF("Detected secret key: %v", options.Cloud.SecretKey) + utils.DebugF("Detected public key: %v", options.Cloud.PublicKey) + options.Cloud.SecretKeyContent = strings.TrimSpace(utils.GetFileContent(options.Cloud.SecretKey)) + options.Cloud.PublicKeyContent = strings.TrimSpace(utils.GetFileContent(options.Cloud.PublicKey)) + } +} + +// GetServer get server options +func GetServer(options *libs.Options) { + server := v.GetStringMapString("Server") + + options.Server.Bind = server["bind"] + options.Server.Cors = server["cors"] + options.Server.JWTSecret = server["secret"] + options.Server.StaticPrefix = server["prefix"] + options.Server.UIPath = utils.NormalizePath(server["ui"]) + utils.MakeDir(path.Dir(options.Server.UIPath)) + + options.Server.MasterPassword = server["master_pass"] + options.Server.CertFile = utils.NormalizePath(server["cert_file"]) + options.Server.KeyFile = utils.NormalizePath(server["key_file"]) + utils.MakeDir(path.Dir(options.Server.CertFile)) + + db := v.GetStringMapString("Database") + + options.Server.DBPath = utils.NormalizePath(db["db_path"]) + options.Server.DBType = db["db_type"] + // this should be remote one + if options.Server.DBType == "mysql" { + options.Server.DBUser = db["db_user"] + options.Server.DBPass = db["db_pass"] + options.Server.DBHost = db["db_host"] + options.Server.DBPort = db["db_port"] + options.Server.DBName = db["db_name"] + + // ā€œuser:password@/dbname?charset=utf8&parseTime=True&loc=Localā€ + cred := fmt.Sprintf("%v:%v", options.Server.DBUser, options.Server.DBPass) + dest := fmt.Sprintf("%v:%v", options.Server.DBHost, options.Server.DBPort) + dbURL := fmt.Sprintf("%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=Local", cred, dest, options.Server.DBName) + options.Server.DBConnection = dbURL + } +} + +// GetClient get options for client +func GetClient(options *libs.Options) map[string]string { + client := v.GetStringMapString("Client") + options.Client.Username = client["username"] + options.Client.Password = client["password"] + return client +} + +func SetTactic(options *libs.Options) { + v = LoadConfig(options) + if err := v.ReadInConfig(); err != nil { + utils.ErrorF("Error reading config file, %s", err) + } + + baseThreads := options.Threads + utils.InforF("Set base threads to %v", color.HiCyanString("%v", baseThreads)) + v.Set("Tactic", map[string]any{ + "default": baseThreads, // 2,4,8,16 + "aggressive": baseThreads * 4, + "gently": int(baseThreads / 2), + }) + + v.WriteConfig() +} diff --git a/core/cron.go b/core/cron.go new file mode 100644 index 0000000..f3c155d --- /dev/null +++ b/core/cron.go @@ -0,0 +1,30 @@ +package core + +import ( + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/utils" + "github.com/jasonlvhit/gocron" + "github.com/spf13/cast" +) + +func taskWithParams(cmd string) { + utils.InforF("Exec: %v", color.HiMagentaString(cmd)) + _, err := utils.RunCommandSteamOutput(cmd) + if err != nil { + utils.ErrorF("Error running command: %v", err) + } +} + +func RunCron(cmd string, schedule int) { + + if schedule == -1 { + utils.InforF("Run command forever: %v", cmd) + for { + taskWithParams(cmd) + } + } + + utils.InforF("Start cron job with %v seconds: %v", schedule, color.HiCyanString(cmd)) + gocron.Every(cast.ToUint64(schedule)).Minutes().Do(taskWithParams, cmd) + <-gocron.Start() +} diff --git a/core/db.go b/core/db.go new file mode 100644 index 0000000..597f924 --- /dev/null +++ b/core/db.go @@ -0,0 +1,229 @@ +package core + +import ( + "os" + "path" + "strings" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/database" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" + "github.com/robertkrimen/otto" +) + +func (r *Runner) LoadDBScripts() string { + var output string + + r.VM.Set(TotalSubdomain, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalAssets = length + utils.InforF("Total subdomain found: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalDns, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalDns = length + utils.InforF("Total Dns: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalScreenShot, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalScreenShot = length + utils.InforF("Total ScreenShot: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalTech, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalTech = length + utils.InforF("Total Tech: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalVulnerability, func(call otto.FunctionCall) otto.Value { + data := utils.ReadingFileUnique(call.Argument(0).String()) + var length int + for _, line := range data { + if !strings.Contains(line, "-info") { + length += 1 + } + } + r.TargetObj.TotalVulnerability = length + utils.InforF("Total Vulnerability: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalArchive, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalArchive = length + utils.InforF("Total Archive: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalLink, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalLink = length + utils.InforF("Total Link: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(TotalDirb, func(call otto.FunctionCall) otto.Value { + length := utils.FileLength(call.Argument(0).String()) + r.TargetObj.TotalDirb = length + utils.InforF("Total Dirb: %v", color.HiMagentaString("%v", length)) + return otto.Value{} + }) + + r.VM.Set(CreateReport, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + reportPath := args[0].String() + if utils.FileExists(reportPath) { + return otto.Value{} + } + moduleName := "inline" + if len(args) > 1 { + moduleName = args[1].String() + } + reportItem := database.Report{ + ReportPath: reportPath, + Module: moduleName, + } + r.TargetObj.Reports = append(r.TargetObj.Reports, reportItem) + return otto.Value{} + }) + + return output +} + +func (r *Runner) DBNewTarget() { + r.TargetObj = database.Target{ + InputName: r.Input, + Workspace: r.Workspace, + InputType: r.InputType, + } + + r.DBRuntimeUpdate() +} + +func (r *Runner) DBNewScan() { + r.ScanObj = database.Scan{ + TaskType: r.RoutineType, + TaskName: path.Base(r.RoutineName), + + TotalSteps: r.TotalSteps, + InputName: r.Input, + InputType: r.InputType, + + MarkDownSunmmary: path.Join(r.WorkspaceFolder, "summary.md"), + MarkDownReport: path.Join(r.WorkspaceFolder, "summary.html"), + + LogFile: r.Opt.LogFile, + Target: r.TargetObj, + ProcessID: os.Getpid(), + IsRunning: true, + IsDone: false, + IsPrepared: true, + IsStarted: true, + } + + if r.RunnerType == "cloud" { + r.ScanObj.IsCloud = true + } + + if r.Opt.Scan.RemoteCall { + r.ScanObj.IsCloud = true + } + + r.ScanObj.CreatedAt = time.Now() + + if runtimeData, err := jsoniter.MarshalToString(r.ScanObj); err == nil { + utils.WriteToFile(r.RuntimeFile, runtimeData) + } + +} + +func (r *Runner) DBUpdateScan() { + r.ScanObj.DoneStep = r.DoneStep + r.ScanObj.CurrentModule = r.CurrentModule + r.ScanObj.RunningTime = r.RunningTime + r.ScanObj.ProcessID = os.Getpid() + + if r.ScanObj.DoneStep == r.ScanObj.TotalSteps { + r.ScanObj.IsDone = true + r.ScanObj.IsRunning = false + } else { + r.ScanObj.IsRunning = true + r.ScanObj.IsDone = false + + } + + utils.DebugF("[DB] Finished %v steps in the %v module", color.HiCyanString("%v/%v", r.DoneStep, r.TotalSteps), r.CurrentModule) + r.DBRuntimeUpdate() +} + +func (r *Runner) DBDoneScan() { + r.ScanObj.CurrentModule = "done" + r.ScanObj.RunningTime = r.RunningTime + + r.ScanObj.DoneStep = r.TotalSteps + r.ScanObj.IsDone = true + r.ScanObj.IsRunning = false + r.ScanObj.IsStarted = false + r.ScanObj.UpdatedAt = time.Now() + + utils.DebugF("[DB] The scan has been completed: %v -- %v", color.HiCyanString(r.ScanObj.InputName), color.HiCyanString(r.ScanObj.TaskName)) + if runtimeData, err := jsoniter.MarshalToString(r.ScanObj); err == nil { + utils.WriteToFile(r.DoneFile, runtimeData) + utils.WriteToFile(r.RuntimeFile, runtimeData) + } + + if utils.FileExists(r.ScanObj.MarkDownReport) { + utils.InforF("Markdown summary has been generated at: %v", color.GreenString(r.ScanObj.MarkDownReport)) + } + if utils.FileExists(r.ScanObj.MarkDownSunmmary) { + utils.InforF("HTML summary has been generated at: %v", color.GreenString(r.ScanObj.MarkDownSunmmary)) + } +} + +func (r *Runner) DBRuntimeUpdate() { + r.ScanObj.UpdatedAt = time.Now() + r.ScanObj.Target = r.TargetObj + if runtimeData, err := jsoniter.MarshalToString(r.ScanObj); err == nil { + utils.WriteToFile(r.RuntimeFile, runtimeData) + } +} + +func (r *Runner) DBNewReports(module libs.Module) { + r.ScanObj.CurrentModule = r.CurrentModule + r.ScanObj.RunningTime = r.RunningTime + + var reports []string + reports = append(reports, module.Report.Final...) + reports = append(reports, module.Report.Noti...) + reports = append(reports, module.Report.Diff...) + + utils.DebugF("Updating %v report records", len(reports)) + for _, report := range reports { + reportType := "text" + if strings.HasSuffix(report, ".html") { + reportType = "html" + } + + reportObj := database.Report{ + ReportName: path.Base(report), + ModulePath: module.ModulePath, + Module: module.Name, + ReportPath: report, + ReportType: reportType, + } + + r.TargetObj.Reports = append(r.TargetObj.Reports, reportObj) + + } + r.ScanObj.Target = r.TargetObj +} diff --git a/core/external.go b/core/external.go new file mode 100644 index 0000000..733b5fa --- /dev/null +++ b/core/external.go @@ -0,0 +1,298 @@ +package core + +import ( + "fmt" + "path" + "time" + + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/utils" + "github.com/robertkrimen/otto" +) + +func (r *Runner) LoadExternalScripts() string { + var output string + vm := r.VM + + // special scripts + vm.Set(Cleaning, func(call otto.FunctionCall) otto.Value { + if r.Opt.NoClean { + utils.InforF("Disabled Cleaning") + return otto.Value{} + } + execution.Cleaning(call.Argument(0).String(), r.Reports) + return otto.Value{} + }) + + // scripts for cleaning modules + vm.Set(CleanAmass, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanAmass(src, dest) + return otto.Value{} + }) + + vm.Set(CleanRustScan, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanRustScan(src, dest) + return otto.Value{} + }) + + vm.Set(CleanGoBuster, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanGoBuster(src, dest) + return otto.Value{} + }) + vm.Set(CleanMassdns, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanMassdns(src, dest) + return otto.Value{} + }) + + vm.Set(CleanSWebanalyze, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanSWebanalyze(src, dest) + return otto.Value{} + }) + vm.Set(CleanJSONDnsx, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanJSONDnsx(src, dest) + return otto.Value{} + }) + + vm.Set(CleanJSONHttpx, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanJSONHttpx(src, dest) + return otto.Value{} + }) + + // Deprecated + vm.Set(CleanWebanalyze, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + args := call.ArgumentList + + techSum := path.Join(path.Dir(dest), fmt.Sprintf("tech-overview-%v.txt", r.Target["Workspace"])) + if len(args) > 3 { + techSum = args[2].String() + } + execution.CleanWebanalyze(src, dest, techSum) + return otto.Value{} + }) + + vm.Set(CleanArjun, func(call otto.FunctionCall) otto.Value { + // src mean folder contain arjun output + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanArjun(src, dest) + return otto.Value{} + }) + + vm.Set(CleanFFUFJson, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.CleanFFUFJson(src, dest) + return otto.Value{} + }) + + vm.Set(GenNucleiReport, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + args := call.ArgumentList + + templateFile := "" + if len(args) >= 3 { + templateFile = args[2].String() + } + execution.GenNucleiReport(r.Opt, src, dest, templateFile) + return otto.Value{} + }) + + return output +} + +func (r *Runner) LoadGitScripts() string { + var output string + vm := r.VM + options := r.Opt + + // Clone("git@xxx.git", "/tmp/dest") + vm.Set(Clone, func(call otto.FunctionCall) otto.Value { + execution.GitClone(call.Argument(0).String(), call.Argument(1).String(), false, options) + return otto.Value{} + }) + // like clone but delete the destination folder first + vm.Set(FClone, func(call otto.FunctionCall) otto.Value { + execution.GitClone(call.Argument(0).String(), call.Argument(1).String(), true, options) + return otto.Value{} + }) + + vm.Set(PushResult, func(call otto.FunctionCall) otto.Value { + for folder := range options.Storages { + execution.PullResult(folder, options) + time.Sleep(3 * time.Second) + execution.PullResult(folder, options) + commitMess := fmt.Sprintf("%v|%v|%v", options.Module.Name, options.Scan.ROptions["Workspace"], utils.GetCurrentDay()) + execution.PushResult(folder, commitMess, options) + } + return otto.Value{} + }) + // push result but specific folder + vm.Set(PushFolder, func(call otto.FunctionCall) otto.Value { + folder := call.Argument(0).String() + execution.PullResult(folder, options) + time.Sleep(3 * time.Second) + execution.PullResult(folder, options) + commitMess := fmt.Sprintf("%v|%v|%v", options.Module.Name, options.Scan.ROptions["Workspace"], utils.GetCurrentDay()) + execution.PushResult(folder, commitMess, options) + return otto.Value{} + }) + + // push result but specific folder + vm.Set(PullFolder, func(call otto.FunctionCall) otto.Value { + folder := call.Argument(0).String() + execution.PullResult(folder, options) + time.Sleep(3 * time.Second) + execution.PullResult(folder, options) + return otto.Value{} + }) + + vm.Set(DiffCompare, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + output := call.Argument(2).String() + execution.DiffCompare(src, dest, output, options) + return otto.Value{} + }) + + vm.Set(GitDiff, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + src := args[0].String() + output := call.Argument(1).String() + history := "1" + if len(args) < 2 { + history = call.Argument(2).String() + } + execution.GitDiff(src, output, history, options) + return otto.Value{} + }) + vm.Set(LoopGitDiff, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + src := args[0].String() + output := call.Argument(1).String() + execution.LoopGitDiff(src, output, options) + return otto.Value{} + }) + + /* --- CDN S3 Bucket --- */ + + // UploadToS3("/tmp/src", "xxx/xxx") or UploadToS3("/tmp/src", "your-cdn.s3.ap-southeast-1.amazonaws.com") + vm.Set(UploadToS3, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + bucket := "" + src := args[0].String() + if len(args) > 1 { + bucket = args[1].String() + } + + // if bucket is empty, use the default one + if bucket == "" { + bucket = options.Cdn.Bucket + } + + execution.UploadToS3(options, src, bucket) + return otto.Value{} + }) + + // DownloadFromS3("xxx", "/tmp/dest") + vm.Set(DownloadFromS3, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + bucket := "" + src := args[0].String() + dest := args[1].String() + if len(args) > 2 { + bucket = args[2].String() + } + + // if bucket is empty, use the default one + if bucket == "" { + bucket = options.Cdn.Bucket + } + + execution.DownloadFromS3(options, src, dest, bucket) + return otto.Value{} + }) + + // DownloadFile("https://xxx.com", "/tmp/dest") + vm.Set(DownloadFile, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + src := args[0].String() + output := args[1].String() + execution.DownloadFile(options, src, output) + return otto.Value{} + }) + + /* --- end CDN S3 Bucket --- */ + + /* --- Gitlab API --- */ + + // CreateRepo("repo-name") + // CreateRepo("repo-name", "tags") + vm.Set(CreateRepo, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + repoName := args[0].String() + tags := "" + if len(args) > 1 { + tags = args[1].String() + } + execution.CreateGitlabRepo(repoName, tags, options) + return otto.Value{} + }) + + vm.Set(DeleteRepo, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + repoName := args[0].String() + execution.DeleteRepo(repoName, 0, options) + return otto.Value{} + }) + vm.Set(DeleteRepoByPid, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + pid, err := args[0].ToInteger() + if err != nil { + return otto.Value{} + } + execution.DeleteRepo("", int(pid), options) + return otto.Value{} + }) + vm.Set(ListProjects, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + if len(args) > 0 { + uid, err := args[0].ToInteger() + if err == nil { + execution.ListProjects(int(uid), options) + } + return otto.Value{} + } + execution.ListProjects(0, options) + return otto.Value{} + }) + + /* --- end Gitlab API --- */ + + // GenMarkdownReport("markdown.md", "output.html") + vm.Set(GenMarkdownReport, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + markdownFile := args[0].String() + outputHTML := args[1].String() + r.GenMarkdownReport(markdownFile, outputHTML) + return otto.Value{} + }) + + return output +} diff --git a/core/flow.go b/core/flow.go new file mode 100644 index 0000000..496f913 --- /dev/null +++ b/core/flow.go @@ -0,0 +1,222 @@ +package core + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/utils" + "github.com/thoas/go-funk" + + "github.com/j3ssie/osmedeus/libs" +) + +func ListAllFlowName(options libs.Options) (result []string) { + rawWorkflows := ListModules(options) + for _, item := range rawWorkflows { + result = append(result, strings.ReplaceAll(filepath.Base(item), ".yaml", "")) + } + return result +} + +func ListModuleName(options libs.Options) (result []string) { + rawResult := DefaultWorkflows(options) + for _, item := range rawResult { + result = append(result, strings.ReplaceAll(filepath.Base(item), ".yaml", "")) + } + return result +} + +// ListFlow list all available mode +func ListFlow(options libs.Options) (result []string) { + modePath := path.Join(options.Env.WorkFlowsFolder, "/*.yaml") + result, err := filepath.Glob(modePath) + if err != nil { + return result + } + return result +} + +// SelectFlow select flow to run +func SelectFlow(flowName string, options libs.Options) []string { + flows := ListFlow(options) + var selectedFlow []string + + // absolute path like -f customflows/general.yaml + if strings.HasSuffix(flowName, ".yaml") { + if utils.FileExists(flowName) { + selectedFlow = append(selectedFlow, flowName) + return selectedFlow + } + } + + // -f test + if !strings.Contains(flowName, ",") { + selectedFlow = append(selectedFlow, singleMode(flowName, flows)...) + } + + // -f test1,test2 + flowNames := strings.Split(flowName, ",") + for _, item := range flowNames { + selectedFlow = append(selectedFlow, singleMode(item, flows)...) + } + + // default custom flow folder + if !utils.FileExists(flowName) { + flowName = path.Join(options.Env.WorkFlowsFolder, "default-flows", flowName) + if utils.FileExists(flowName) { + selectedFlow = append(selectedFlow, flowName) + } else if utils.FileExists(flowName + ".yaml") { + flowName = flowName + ".yaml" + selectedFlow = append(selectedFlow, flowName) + } + } + + selectedFlow = funk.UniqString(selectedFlow) + return selectedFlow +} + +func singleMode(modeName string, modes []string) (selectedMode []string) { + for _, mode := range modes { + basemodeName := strings.TrimRight(strings.TrimRight(filepath.Base(mode), "yaml"), ".") + // select workflow file in workflow directory + if strings.ToLower(basemodeName) == strings.ToLower(modeName) { + selectedMode = append(selectedMode, mode) + } + } + return selectedMode +} + +// ListModules list all available module +func ListModules(options libs.Options) (modules []string) { + modePath := path.Join(options.Env.WorkFlowsFolder, "general/*.yaml") + if options.Flow.Type != "" { + modePath = path.Join(options.Env.WorkFlowsFolder, fmt.Sprintf("%v/*.yaml", options.Flow.Type)) + } + if strings.HasSuffix(options.Scan.Flow, ".yaml") { + if options.Flow.Type == "" { + options.Flow.Type = "general" + } + modePath = path.Join(path.Dir(options.Scan.Flow), options.Flow.Type) + "/*.yaml" + } + modules, err := filepath.Glob(modePath) + if err != nil { + return modules + } + return modules +} + +// SelectModules return list of modules name +func SelectModules(moduleNames []string, options libs.Options) []string { + if strings.Contains(options.Flow.Type, "{{.") { + options.Flow.Type = ResolveData(options.Flow.Type, options.Scan.ROptions) + } + modules := ListModules(options) + var selectedModules []string + for _, item := range moduleNames { + selectedModules = append(selectedModules, singleSelectModule(item, modules)...) + } + selectedModules = funk.UniqString(selectedModules) + + utils.DebugF("Select module name %v: %v", color.HiCyanString("%v", moduleNames), selectedModules) + return selectedModules +} + +func singleSelectModule(moduleName string, modules []string) (selectedModules []string) { + for _, module := range modules { + baseModuleName := strings.Trim(strings.TrimRight(filepath.Base(module), "yaml"), ".") + if strings.ToLower(baseModuleName) == strings.ToLower(moduleName) { + selectedModules = append(selectedModules, module) + } + } + return selectedModules +} + +// DefaultWorkflows select module from ~/.osmedeus/core/workflow/plugins/ +func DefaultWorkflows(options libs.Options) []string { + defaultModule := path.Join(options.Env.WorkFlowsFolder, "default-modules") + modePath := path.Join(defaultModule, "/*.yaml") + results, err := filepath.Glob(modePath) + if err != nil { + utils.ErrorF("No default module found in %v", defaultModule) + return []string{} + } + return results +} + +// DirectSelectModule select module from ~/osmedeus-base/workflow/default-modules +func DirectSelectModule(options libs.Options, moduleName string) string { + // got absolutely path + if utils.FileExists(moduleName) { + return moduleName + } + + // select in cloud folder first if we're running the cloud scan + // ~/.osmedeus/core/workflow/cloud-modules/ + basePlugin := path.Join(options.Env.WorkFlowsFolder, "cloud-modules") + modulePath := path.Join(basePlugin, moduleName) + if utils.FileExists(modulePath) { + utils.DebugF("Load module path: %v", modulePath) + return modulePath + } + + modulePath = path.Join(basePlugin, moduleName+".yaml") + if utils.FileExists(modulePath) { + utils.DebugF("Load module path: %v", modulePath) + return modulePath + } + + // ~/.osmedeus/core/workflow/default-modules/ + basePlugin = path.Join(options.Env.WorkFlowsFolder, "default-modules") + modulePath = path.Join(basePlugin, moduleName) + utils.DebugF("Load module path: %v", modulePath) + if utils.FileExists(modulePath) { + return modulePath + } + + modulePath = path.Join(basePlugin, moduleName+".yaml") + utils.DebugF("Load module path: %v", modulePath) + if utils.FileExists(modulePath) { + return modulePath + } + utils.DebugF("No plugin found with: %v", moduleName) + return "" +} + +// ListScripts list all available mode +func ListScripts(options libs.Options) (result []string) { + modePath := path.Join(options.Env.OseFolder, "/*.js") + result, err := filepath.Glob(modePath) + if err != nil { + return result + } + + modePath = path.Join(options.Env.OseFolder, "/*/*.js") + DepthResult, err := filepath.Glob(modePath) + if err == nil { + result = append(result, DepthResult...) + } + return result +} + +func SelectScript(scriptName string, options libs.Options) string { + scripts := ListScripts(options) + for _, script := range scripts { + if strings.Contains(scriptName, "/") { + if strings.HasSuffix(script, scriptName) || strings.HasSuffix(script, scriptName+".js") { + return script + } + } + + compareName := path.Base(script) + if compareName == scriptName { + return script + } + if compareName == fmt.Sprintf("%s.js", scriptName) { + return script + } + } + return "" +} diff --git a/core/import.go b/core/import.go new file mode 100644 index 0000000..3da52e3 --- /dev/null +++ b/core/import.go @@ -0,0 +1,574 @@ +package core + +// func (r *Runner) LoadImportScripts() string { +// var output string + +// // DB scripts + +// r.VM.Set("ImportSubdomain", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportSubdomain(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportDns", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportDns(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportTech", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportTech(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportHTTPJson", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportHTTPJson(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportScreenShotJson", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportScreenShotJson(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportPortJson", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportPortJson(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportJaelesVulnJson", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportJaelesVulnJson(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportNucleiVulnJson", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportNucleiVulnJson(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportDirectoryJson", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportDirectoryJson(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportLinks", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportLinks(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportArchive", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportArchive(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportIPRange", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportIPRange(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportCred", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportCred(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportCert", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportCert(src) +// return otto.Value{} +// }) + +// r.VM.Set("ImportCloudBrute", func(call otto.FunctionCall) otto.Value { +// src := call.Argument(0).String() +// r.ImportCloudBrute(src) +// return otto.Value{} +// }) + +// r.VM.Set("SummaryTarget", func(call otto.FunctionCall) otto.Value { +// r.ScanObj.SummaryTarget() +// return otto.Value{} +// }) + +// return output +// } + +// func (r *Runner) ImportSubdomain(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// domains := utils.ReadingLines(src) +// var objs []database.Asset + +// for _, domain := range domains { +// domain = strings.TrimSpace(domain) +// if domain == "" { +// continue +// } + +// obj := database.Asset{ +// AssetValue: domain, +// //Dns: nil, +// HTTP: nil, +// Directory: nil, +// Vulnerability: nil, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// objs = append(objs, obj) +// } +// database.ImportAssets(objs) +// } + +// func (r *Runner) ImportDns(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// line = strings.TrimSpace(line) +// if line == "" || !strings.Contains(line, " ") { +// continue +// } + +// raw := strings.Split(line, " ") +// domain := strings.Trim(raw[0], ".") +// dnsType := strings.TrimSpace(raw[1]) +// dnsValue := strings.Trim(raw[2], ".") + +// if strings.TrimSpace(domain) == "" || strings.TrimSpace(dnsValue) == "" { +// continue +// } + +// dnsChecksum := utils.GenHash(fmt.Sprintf("%s-%s-%s", domain, dnsType, dnsValue)) + +// obj := database.Dns{ +// Domain: domain, +// DnsType: dnsType, +// DnsValue: dnsValue, +// DnsChecksum: dnsChecksum, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// obj.Create() +// } +// } + +// func (r *Runner) ImportTech(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if !strings.Contains(line, ";;") { +// utils.ErrorF("Invalid format: %v", line) +// continue +// } + +// // data should be domain|%v;;techs|%v +// domain := strings.TrimPrefix(strings.Split(line, ";;")[0], "domain|") +// techs := strings.TrimPrefix(strings.Split(line, ";;")[1], "techs|") +// if strings.TrimSpace(techs) == "" { +// utils.ErrorF("Invalid format: %v", line) +// continue +// } + +// obj := database.Asset{ +// AssetValue: domain, +// Technology: techs, +// IsAlive: true, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } + +// obj.UpdateTech() +// } +// } + +// func (r *Runner) ImportHTTPJson(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) + +// baseResult, _ := filepath.Abs(src) +// baseResult = path.Dir(baseResult) +// utils.DebugF("Set Base Dir for content: %v", baseResult) + +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// continue +// } + +// // {"url":"https://sso-na.tesla.com","title":"Blank Title","checksum":"527ef0b39a78caf74f54ca5b2ffb59bcfe688685","content_file":"overview/contents/https___sso-na.tesla.com.txt","status":"302","time":"0.046268348","length":"90","redirect":"https://teamchatgl.tesla.com/"} +// URL := jsonParsed.S("url").Data().(string) +// title := jsonParsed.S("title").Data().(string) +// checksum := jsonParsed.S("checksum").Data().(string) +// contentPath := jsonParsed.S("content_file").Data().(string) +// status := jsonParsed.S("status").Data().(string) +// length := jsonParsed.S("length").Data().(string) +// redirect := jsonParsed.S("redirect").Data().(string) + +// if !utils.FileExists(contentPath) { +// contentPath = path.Join(baseResult, contentPath) +// } + +// // base64 content +// data := "No-Content" +// if !strings.Contains(contentPath, "No-Content") { +// //utils.DebugF("Reading HTML content: %v", contentPath) +// data = utils.ImageAsBase64(contentPath) +// } + +// obj := database.HTTP{ +// URL: URL, +// Title: title, +// Checksum: checksum, +// StatusCode: cast.ToInt(status), +// ContentLength: cast.ToInt(length), +// HTTPContent: data, +// Redirect: redirect, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// obj.CreateHTTP() +// } +// } + +// func (r *Runner) ImportScreenShotJson(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// baseResult, _ := filepath.Abs(src) +// baseResult = path.Dir(baseResult) +// content := utils.ReadingLines(src) +// utils.DebugF("Set Base Dir for screenshot: %v", baseResult) + +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// continue +// } + +// URL := jsonParsed.S("url").Data().(string) +// imgPath := jsonParsed.S("image").Data().(string) +// tech := jsonParsed.S("tech").Data().(string) +// if tech != "" { +// //domain, _ := utils.GetDomain(URL) +// utils.DebugF("more tech: %v", tech) +// //database.NewTech(wsObj, domain, tech) +// } + +// if !utils.FileExists(imgPath) { +// imgPath = path.Join(baseResult, imgPath) +// } +// imgData := utils.ImageAsBase64(imgPath) + +// //database.NewScreenshot(wsObj, URL, data) + +// obj := database.HTTP{ +// URL: URL, +// //Title: tittle, +// //Checksum: "", +// //StatusCode: 0, +// //ContentLength: 0, +// //HTTPContent: "", +// ScreenShotData: imgData, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// obj.CreateScreenShot() +// } +// } + +// type PortObj struct { +// Protocol string +// PortID string +// State string +// Service struct { +// Name string +// Product string +// Cpe string +// } +// Script struct { +// ID string +// Output string +// } +// } + +// func (r *Runner) ImportPortJson(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } +// content := utils.ReadingLines(src) + +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// continue +// } + +// var portObj []PortObj + +// ipAddress := jsonParsed.S("IPAddress").Data().(string) +// rawPorts := jsonParsed.S("Ports").Bytes() +// err = jsoniter.Unmarshal(rawPorts, &portObj) +// if err != nil || len(portObj) == 0 { +// continue +// } + +// var ports []string +// for _, p := range portObj { +// info := fmt.Sprintf("%v/%v/%v", p.PortID, p.Protocol, p.Service.Product) +// ports = append(ports, strings.Trim(info, "/")) +// } +// if len(ports) == 0 { +// continue +// } + +// obj := database.Dns{ +// DnsValue: ipAddress, +// DnsType: "A", +// Ports: strings.Join(ports, ","), +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// obj.UpdatePort() +// } +// } + +// // ImportJaelesVulnJson import new asset to DB +// func (r *Runner) ImportJaelesVulnJson(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } +// content := utils.ReadingLines(src) +// baseDir := path.Dir(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } + +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } +// raw := jsonParsed.S("OutputFile").Data().(string) +// reportPath := raw +// if !utils.FileExists(reportPath) { +// reportPath = path.Join(baseDir, raw) +// if !utils.FileExists(reportPath) { +// reportPath = path.Join(path.Dir(baseDir), raw) +// } +// } +// vulnContent := utils.GetFileContent(reportPath) + +// // parse Data as JSON +// jsonParsed, err = gabs.ParseJSON([]byte(vulnContent)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// obj := database.Vulnerability{ +// URL: jsonParsed.S("URL").Data().(string), +// VulnRequest: jsonParsed.S("Req").Data().(string), +// VulnResponse: jsonParsed.S("Res").Data().(string), +// DetectionString: jsonParsed.S("DetectionString").Data().(string), +// VulnerabilityTitle: jsonParsed.S("SignName").Data().(string), +// SignatureID: jsonParsed.S("SignID").Data().(string), +// Confidence: jsonParsed.S("Confidence").Data().(string), +// Severity: jsonParsed.S("Risk").Data().(string), +// Source: "Jaeles", +// //VulnChecksum: utils.GenHash(vulnData), +// //PluginScan: SelectScanID(workspace, pluginName, scanID), +// //Target: SelectScanByWS(workspace), +// //Asset: SelectAssetByData(domain), +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// obj.Create() +// } +// } + +// // ImportNucleiVulnJson import new asset to DB +// func (r *Runner) ImportNucleiVulnJson(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// obj := database.Vulnerability{ +// URL: jsonParsed.S("host").Data().(string), +// VulnRequest: utils.Base64Encode(jsonParsed.S("request").Data().(string)), +// VulnResponse: utils.Base64Encode(jsonParsed.S("response").Data().(string)), +// DetectionString: jsonParsed.S("matched").Data().(string), + +// SignatureID: jsonParsed.S("templateID").Data().(string), +// Confidence: "Tentative", +// VulnerabilityTitle: jsonParsed.S("info", "name").Data().(string), +// Severity: jsonParsed.S("info", "severity").Data().(string), +// Source: "Nuclei", +// TargetRefer: r.ScanObj.TargetRefer, + +// ScanRefer: r.ScanObj.ID, +// } +// obj.Create() +// } +// } + +// // ImportDirectoryJson import new asset to DB +// func (r *Runner) ImportDirectoryJson(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" || !strings.Contains(line, "url") { +// continue +// } + +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// // some parser here +// URL := jsonParsed.S("url").Data().(string) +// redirect, ok := jsonParsed.S("redirectlocation").Data().(string) +// if ok { +// redirect = "" +// } + +// obj := database.Directory{ +// URL: URL, +// Status: cast.ToInt(jsonParsed.S("status").Data()), +// ContentLength: cast.ToInt(jsonParsed.S("length").Data()), +// Words: cast.ToInt(jsonParsed.S("words").Data()), +// RedirectURL: redirect, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.ScanObj.TargetRefer, +// } +// obj.Create() +// } +// } + +// +//// UpdateJSONDns update dns in db +//func UpdateJSONDns(src string, options libs.Options) { +// content := utils.ReadingLines(src) +// if len(content) == 0 { +// utils.ErrorF("File not found: %v", src) +// return +// } +// +// utils.DebugF("Update JSON DNS: %v", src) +// //wsObj := database.SelectScanByWS(options.Scan.ROptions["Workspace"]) +// target := options.Scan.ROptions["Workspace"] +// +// for _, line := range content { +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// continue +// } +// +// domain, ok := jsonParsed.S("host").Data().(string) +// if !ok { +// continue +// } +// +// // filtered some unrelated domain +// if !strings.HasSuffix(domain, fmt.Sprintf(".%s", target)) { +// if domain != target { +// continue +// } +// } +// +// data := "N/A" +// isError := jsonParsed.S("status_code").Data().(string) +// if isError == "NXDOMAIN" { +// //database.NewAssetWithDns(wsObj, domain, data) +// continue +// } +// var results []string +// a := jsonParsed.S("a") +// if a != nil { +// for _, record := range a.Children() { +// data := fmt.Sprintf("A/%s", cast.ToString(record.Data())) +// results = append(results, data) +// } +// } +// +// cname := jsonParsed.S("cname") +// if cname != nil { +// for _, record := range cname.Children() { +// data := fmt.Sprintf("CNAME/%s", cast.ToString(record.Data())) +// results = append(results, data) +// } +// } +// +// mx := jsonParsed.S("mx") +// if mx != nil { +// for _, record := range mx.Children() { +// data := fmt.Sprintf("MX/%s", cast.ToString(record.Data())) +// results = append(results, data) +// } +// } +// +// ns := jsonParsed.S("ns") +// if ns != nil { +// for _, record := range ns.Children() { +// data := fmt.Sprintf("NS/%s", cast.ToString(record.Data())) +// results = append(results, data) +// } +// } +// results = funk.UniqString(results) +// data = strings.Join(results, ";;") +// //database.NewAssetWithDns(wsObj, domain, data) +// } +//} diff --git a/core/import_mics.go b/core/import_mics.go new file mode 100644 index 0000000..24226d2 --- /dev/null +++ b/core/import_mics.go @@ -0,0 +1,238 @@ +package core + +// "github.com/j3ssie/osmedeus/database" + +// func (r *Runner) StartScanNoti() error { +// data, err := jsoniter.Marshal(&r.ScanObj) +// if err != nil { +// return fmt.Errorf("err marshal object: %v", err) +// } +// noti := database.Notification{ +// NotificationType: "start", +// NotificationSource: "scan", +// NewData: datatypes.JSON(data), +// ObjRefer: r.ScanObj.ID, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.TargetObj.ID, +// } +// return noti.Create() +// } + +// func (r *Runner) ScanDoneNoti() error { +// data, err := jsoniter.Marshal(&r.ScanObj) +// if err != nil { +// return fmt.Errorf("err marshal object: %v", err) +// } +// noti := database.Notification{ +// NotificationType: "done", +// NotificationSource: "scan", +// NewData: datatypes.JSON(data), +// ObjRefer: r.ScanObj.ID, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.TargetObj.ID, +// } +// return noti.Create() +// } + +// // ImportLinks import new link to DB +// func (r *Runner) ImportLinks(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// // {"input":"http://academy.live-test.redacted.io","source":"body","type":"url","output":"http://academy.live-test.redacted.io","status":307,"length":7} +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// obj := database.Link{ +// LinkValue: jsonParsed.S("output").Data().(string), +// LinkSource: jsonParsed.S("source").Data().(string), +// URL: jsonParsed.S("input").Data().(string), +// LinkType: jsonParsed.S("type").Data().(string), +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.TargetObj.ID, +// } +// obj.Create() +// } +// } + +// func (r *Runner) ImportArchive(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } + +// // ignore root domain +// u, err := url.Parse(line) +// if err != nil || u.Path == "" || u.Path == "/" { +// continue +// } + +// obj := database.Archive{ +// ArchiveValue: line, +// ArchiveChecksum: utils.GenHash(line), +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.TargetObj.ID, +// } +// obj.Create() +// } +// } + +// func (r *Runner) ImportIPRange(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// utils.DebugF("Processing: %v", line) +// obj := database.IPRange{ +// ASNumber: cast.ToString(jsonParsed.S("Number").Data()), +// Country: cast.ToString(jsonParsed.S("CountryCode").Data()), +// Value: cast.ToString(jsonParsed.S("CIDR").Data()), +// Info: cast.ToString(jsonParsed.S("Description").Data()), +// Amount: cast.ToUint(jsonParsed.S("Count").Data()), +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.TargetObj.ID, +// } +// obj.Create() +// } +// } + +// func (r *Runner) ImportCert(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// domain := cast.ToString(jsonParsed.S("Domain").Data()) +// isWildCard := false +// if strings.Contains(domain, "*.") { +// isWildCard = true +// domain = strings.TrimLeft(domain, "*.") +// } + +// obj := database.CertInfo{ +// Domain: domain, +// CertInfo: cast.ToString(jsonParsed.S("CertInfo").Data()), +// OrgInfo: cast.ToString(jsonParsed.S("OrgInfo").Data()), +// IsWildcard: isWildCard, +// TargetRefer: r.TargetObj.ID, +// } +// obj.Create() +// } +// } + +// func (r *Runner) ImportCred(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } + +// jsonParsed, err := gabs.ParseJSON([]byte(line)) +// if err != nil { +// utils.ErrorF("Error parse JSON Data") +// continue +// } + +// email := cast.ToString(jsonParsed.S("email").Data()) +// credID := cast.ToString(jsonParsed.S("id").Data()) +// username := cast.ToString(jsonParsed.S("username").Data()) +// password := cast.ToString(jsonParsed.S("password").Data()) +// hashedPassword := cast.ToString(jsonParsed.S("hashed_password").Data()) +// name := cast.ToString(jsonParsed.S("name").Data()) +// ipAddress := cast.ToString(jsonParsed.S("ip_address").Data()) +// phone := cast.ToString(jsonParsed.S("phone").Data()) +// source := cast.ToString(jsonParsed.S("database_name").Data()) + +// obj := database.Credential{ +// CredID: credID, +// Email: email, +// Username: username, +// Password: password, +// HashedPassword: hashedPassword, +// Name: name, +// Phone: phone, +// IPAddress: ipAddress, +// Source: source, +// TargetRefer: r.TargetObj.ID, +// ScanRefer: r.ScanObj.ID, +// } +// obj.Create() +// } +// } + +// func (r *Runner) ImportCloudBrute(src string) { +// if !utils.FileExists(src) { +// utils.ErrorF("file not found: %v", src) +// return +// } + +// content := utils.ReadingLines(src) +// for _, line := range content { +// if strings.TrimSpace(line) == "" { +// continue +// } + +// if !strings.Contains(line, " - ") { +// continue +// } + +// cloudDomain := strings.Split(line, " - ")[1] +// status := strings.Split(line, " - ")[0] +// if strings.Contains(status, ": ") { +// status = strings.Split(strings.Split(line, " - ")[0], ": ")[1] +// } + +// obj := database.CloudBrute{ +// Status: status, +// CloudDomain: cloudDomain, +// RawData: line, +// ScanRefer: r.ScanObj.ID, +// TargetRefer: r.TargetObj.ID, +// } +// obj.Create() +// } +// } diff --git a/core/markdown.go b/core/markdown.go new file mode 100644 index 0000000..e32bde0 --- /dev/null +++ b/core/markdown.go @@ -0,0 +1,256 @@ +package core + +import ( + "path" + + "fmt" + "os" + "regexp" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cast" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +func (r *Runner) GenMarkdownReport(markdownFile string, outputHTML string) { + utils.DebugF("Reading markdown report from: %v", markdownFile) + + // get the markdown template content + mdContent := utils.GetFileContent(markdownFile) + mdContent = ResolveData(mdContent, r.Target) + + // replace all the tag + mdContent = r.ResolveScanInfoTag(mdContent) + // utils.DebugF("ResolveScanInfoTag:\n%v", mdContent) + + // replace all the tag + mdContent = r.ResolveReportsTag(mdContent) + + // replace all the tag + mdContent = r.ResolveContentTag(mdContent) + // fmt.Println("mdContent", mdContent) + + // generating the markdown file first + outputMD := strings.Replace(outputHTML, ".html", ".md", -1) + utils.WriteToFile(outputMD, mdContent) + + utils.InforF("Generate markdown report: %v", outputMD) + utils.InforF("Generate HTML report: %v", outputHTML) + // finally convert to HTML + MarkDownToHTML(r.Opt, r.Input, outputMD, outputHTML) +} + +func (r *Runner) ResolveScanInfoTag(rawMarkdown string) string { + re := regexp.MustCompile(``) + match := re.FindString(rawMarkdown) + if len(match) > 1 { + utils.DebugF("Replace scanInfo tag: %v", match) + scanInfo := fmt.Sprintf(` +| | | +|----------------|-------------| +| Target | **:target** | +| Running Time | **:runningTime** | +| Workflow | **:workflow** | +| Status | **:status** | +| Statistics | :statistics | +`) + + status := "done" + if r.ScanObj.IsRunning { + status = "running" + } + + statistics := fmt.Sprintf("`assets/%v`, `dns/%v`, `vulnerability/%v`, ", r.TargetObj.TotalAssets, r.TargetObj.TotalDns, r.TargetObj.TotalVulnerability) + replacements := map[string]string{ + ":target": r.ScanObj.InputName, + ":runningTime": cast.ToString(int(r.RunningTime)/3600) + " hours", + ":workflow": r.ScanObj.TaskName, + ":statistics": statistics, + ":status": status, + } + // generate the statistics info + for oldStr, newStr := range replacements { + scanInfo = strings.ReplaceAll(scanInfo, oldStr, newStr) + } + + return strings.Replace(rawMarkdown, match, scanInfo, -1) + } + utils.DebugF("No scanInfo tag found") + return rawMarkdown +} + +func (r *Runner) ResolveContentTag(rawData string) string { + finalMarkdown := rawData + // finding all the content tags and replace it with the content + re := regexp.MustCompile(`]*>`) + matchs := re.FindAllString(rawData, -1) + for _, contentTag := range matchs { + utils.DebugF("Replace content tag: %v", color.GreenString(contentTag)) + content := r.ResolveContentSrc(contentTag) + finalMarkdown = strings.Replace(finalMarkdown, contentTag, content, -1) + } + + return finalMarkdown +} + +func (r *Runner) ResolveContentSrc(tag string) string { + re := regexp.MustCompile(`src=\"(\S+)\"`) + match := re.FindStringSubmatch(tag) + if len(match) > 1 { + fileContent := utils.GetFileContent(match[1]) + utils.DebugF("Replace content src: %v", color.GreenString(match[1])) + + if strings.Contains(tag, "expand=true") { + return "```\n" + fileContent + "```" + } + + if strings.Contains(tag, "shorten=true") || len(fileContent) > r.Opt.MDCodeBlockLimit { + return extendTag(fileContent) + } + + return "```\n" + fileContent + "```" + } + return "" +} + +func extendTag(str string) string { + data := "
\nClick to Expand\n\n" + "
\n" + str + "\n
" + "\n
" + return data +} + +func (r *Runner) ResolveReportsTag(rawMarkDown string) string { + // rawData := utils.GetFileContent(markdownFile) + finalMarkdown := rawMarkDown + // finding all the reports tags + re := regexp.MustCompile(``) + matchs := re.FindAllString(rawMarkDown, -1) + if len(matchs) == 0 { + utils.DebugF("No reports tag found") + return rawMarkDown + } + + for _, reportTag := range matchs { + utils.DebugF("Replace content tag: %v", reportTag) + mdContent := "" + + for _, report := range r.TargetObj.Reports { + // add the link if report file is HTML file + if report.ReportType == "html" { + mdContent += fmt.Sprintf("### %s -- [%s](%s) \n\n", report.Module, report.ReportName, report.ReportPath) + mdContent += "\n***\n" + continue + } + // add the full content if report file is a text file + mdContent += fmt.Sprintf("### %s -- *%s* \n\n", report.Module, report.ReportName) + + fileContent := utils.GetFileContent(report.ReportPath) + if len(fileContent) > r.Opt.MDCodeBlockLimit { + mdContent += extendTag(fileContent) + } else { + mdContent += "```\n" + mdContent += fileContent + mdContent += "\n```\n" + } + mdContent += "\n***\n\n" + } + + finalMarkdown = strings.Replace(finalMarkdown, reportTag, mdContent, -1) + } + + return finalMarkdown +} + +func MarkDownToHTML(options libs.Options, target string, markdownFile string, outputFile string) error { + css := path.Join(options.Env.DataFolder, "markdown/style.css") + + var input []byte + var err error + + if input, err = os.ReadFile(markdownFile); err != nil { + utils.ErrorF("Error reading %s: %v", markdownFile, err) + return err + } + + // set up options + var extensions = parser.NoIntraEmphasis | + parser.Tables | + parser.FencedCode | + parser.Autolink | + parser.Strikethrough | + parser.SpaceHeadings + + var renderer markdown.Renderer + // render the data into HTML + var htmlFlags html.Flags + + htmlFlags |= html.Smartypants + htmlFlags |= html.UseXHTML + htmlFlags |= html.CompletePage + htmlFlags |= html.SmartypantsLatexDashes + htmlFlags |= html.SmartypantsFractions + + params := html.RendererOptions{ + Flags: htmlFlags, + CSS: css, + } + renderer = html.NewRenderer(params) + + // parse and render + var output []byte + parser := parser.NewWithExtensions(extensions) + // @NOTE: beware of XSS as I assume you will trust the markdown content you generate + output = markdown.ToHTML(input, parser, renderer) + // html := bluemonday.UGCPolicy().SanitizeBytes(output) // skip the sanitization as we prefer more beautiful output + + cssContent, err := os.ReadFile(css) + if err != nil { + return err + } + + finalHTML := fmt.Sprintf(` + + + + + + + + Osmedeus Executive Summary - %s + + + + + + + + + + + + +%s + + + `, target, string(cssContent), string(output)) + + // output the result + var out *os.File + if out, err = os.Create(outputFile); err != nil { + utils.ErrorF("Error creating %s: %v", outputFile, err) + return err + } + defer out.Close() + + if _, err = out.WriteString(finalHTML); err != nil { + utils.ErrorF("Error writing output: %v", err) + return err + } + + return nil +} diff --git a/core/mode_test.go b/core/mode_test.go new file mode 100644 index 0000000..4d4cbc0 --- /dev/null +++ b/core/mode_test.go @@ -0,0 +1,24 @@ +package core + +import ( + "fmt" + "testing" + + "github.com/j3ssie/osmedeus/libs" +) + +func TestListMode(t *testing.T) { + var options libs.Options + options.Env.WorkFlowsFolder = "~/go/src/github.com/j3ssie/osmedeus/workflow/" + result := ListFlow(options) + fmt.Println(result) + if len(result) == 0 { + t.Errorf("Error ListMode") + } + + selectedMode := SelectFlow("general", options) + fmt.Println(selectedMode) + if len(selectedMode) == 0 { + t.Errorf("Error selectedMode") + } +} diff --git a/core/module.go b/core/module.go new file mode 100644 index 0000000..5b061b9 --- /dev/null +++ b/core/module.go @@ -0,0 +1,370 @@ +package core + +import ( + "context" + "fmt" + "path" + "strings" + "sync" + "time" + + "github.com/spf13/cast" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/panjf2000/ants" +) + +// RunModule run the module +func (r *Runner) RunModule(module libs.Module) { + // get reports path + module = ResolveReports(module, r.Params) + + // check if resume enable or not + if (r.Opt.Resume || module.Resume) && !module.Forced { + if CheckResume(module) { + utils.BlockF(module.Name, "Resume detected") + return + } + } + + r.CurrentModule = module.Name + timeStart := time.Now() + utils.BlockF("Module-Started", fmt.Sprintf("%v - %v", module.Name, module.Desc)) + + // create report record first because I don't want to wait for them to show up in UI until the module done + r.DBNewReports(module) + + // pre-run + if len(module.PreRun) > 0 && r.Opt.NoPreRun == false { + utils.InforF("Running prepare scripts for module %v", color.CyanString(module.Name)) + r.RunScripts(module.PreRun) + } + + // main part + // utils.BlockF(module.Name, "Begin executing primary tasks") + err := r.RunSteps(module.Steps) + if err != nil { + utils.BadBlockF(module.Name, fmt.Sprintf("got exit call")) + } + + // post-run + if len(module.PostRun) > 0 && r.Opt.NoPostRun == false { + utils.InforF("Running conclude scripts for module %v", color.CyanString(module.Name)) + r.RunScripts(module.PostRun) + } + + // print the reports file + utils.PrintLine() + printReports(module) + + // estimate time + elapsedTime := time.Since(timeStart).Seconds() + utils.BlockF("Module-Ended", fmt.Sprintf("Elapsed Time for the module %v in %v", color.HiCyanString(module.Name), color.HiMagentaString("%vs", elapsedTime))) + r.RunningTime += cast.ToInt(elapsedTime) + utils.PrintLine() + r.DBUpdateScan() +} + +// RunScripts run list of scripts +func (r *Runner) RunScripts(scripts []string) string { + if r.Opt.Timeout != "" { + timeout := utils.CalcTimeout(r.Opt.Timeout) + utils.DebugF("Run scripts with %v seconds timeout", timeout) + r.RunScriptsWithTimeOut(r.Opt.Timeout, scripts) + return "" + } + + for _, script := range scripts { + outScript := r.RunScript(script) + if strings.Contains(outScript, "exit") { + return outScript + } + } + return "" +} + +// RunScriptsWithTimeOut run list of scripts with timeout +func (r *Runner) RunScriptsWithTimeOut(timeoutRaw string, scripts []string) string { + timeout := utils.CalcTimeout(timeoutRaw) + utils.DebugF("Run scripts with %v seconds timeout", timeout) + + c := context.Background() + deadline := time.Now().Add(time.Duration(timeout) * time.Second) + c, cancel := context.WithDeadline(c, deadline) + defer cancel() + + go func() { + for _, script := range scripts { + outScript := r.RunScript(script) + if strings.Contains(outScript, "exit") { + return + } + } + cancel() + }() + + select { + case <-c.Done(): + utils.DebugF("Scripts done") + return "" + case <-time.After(time.Duration(timeout) * time.Second): + utils.BadBlockF("timeout", fmt.Sprintf("Scripts got timeout after %v", color.HiMagentaString(timeoutRaw))) + } + return "" +} + +// RunScript really run a script +func (r *Runner) RunScript(script string) string { + return r.ExecScript(script) +} + +// RunSteps run list of steps +func (r *Runner) RunSteps(steps []libs.Step) error { + var stepOut string + for _, step := range steps { + r.DoneStep += 1 + + if step.Timeout != "" { + // timeout should be: 30, 30m, 1h + timeout := utils.CalcTimeout(step.Timeout) + if timeout != 0 { + stepOut, _ = r.RunStepWithTimeout(timeout, step) + if strings.Contains(stepOut, "exit") { + return fmt.Errorf("got exit call") + } + continue + } + } + + stepOut, _ = r.RunStep(step) + if strings.Contains(stepOut, "exit") { + return fmt.Errorf("got an exit call") + } + } + return nil +} + +// RunStepWithTimeout run step with timeout +func (r *Runner) RunStepWithTimeout(timeout int, step libs.Step) (out string, err error) { + utils.DebugF("Run step with %v seconds timeout", timeout) + prefix := fmt.Sprintf("timeout -k 1m %vs ", timeout) + + // prepare the os command with prefix timeout first + var preFixCommands []string + for _, command := range step.Commands { + preFixCommand := command + if !strings.Contains(command, "timeout") { + preFixCommand = prefix + command + } + preFixCommands = append(preFixCommands, preFixCommand) + } + step.Commands = preFixCommands + + // override global timeout + r.Opt.Timeout = step.Timeout + return r.RunStep(step) +} + +func (r *Runner) RunStep(step libs.Step) (string, error) { + var output string + if step.Label != "" { + utils.BlockF("Step", fmt.Sprintf("Initiating Step %v", color.HiGreenString(step.Label))) + } + + // checking required file + err := r.CheckRequired(step.Required) + if err != nil { + return output, fmt.Errorf("missing requirements") + } + + // check conditions and run reverse step + err = r.CheckCondition(step.Conditions) + if err != nil { + if len(step.RCommands) == 0 && len(step.RScripts) == 0 { + return output, fmt.Errorf("conditions not met") + } + + // run reverse commands + utils.InforF("Condition false, run the reverse commands") + if len(step.RCommands) > 0 { + r.RunCommands(step.RCommands, step.Std) + } + // run reverse scripts + if len(step.RScripts) > 0 { + output = r.RunScripts(step.RScripts) + if strings.Contains(output, "exit") { + return output, nil + } + } + return output, nil + } + + // run the step in loop mode + if step.Source != "" { + return r.RunStepWithSource(step) + } + // + + if len(step.Commands) > 0 { + r.RunCommands(step.Commands, step.Std) + } + if len(step.Scripts) > 0 { + output = r.RunScripts(step.Scripts) + if strings.Contains(output, "exit") { + return output, nil + } + } + + // run ose here + if len(step.Ose) > 0 { + for _, ose := range step.Ose { + r.RunOse(ose) + } + } + + // post scripts + if len(step.PConditions) > 0 || len(step.PScripts) > 0 { + err := r.CheckCondition(step.PConditions) + if err == nil { + if len(step.PScripts) > 0 { + r.RunScripts(step.PScripts) + } + } + } + return output, nil + +} + +// RunStepWithSource really run a step +func (r *Runner) RunStepWithSource(step libs.Step) (out string, err error) { + ////// Start to run step but in loop mode + utils.DebugF("Running the step using the source file: %v", step.Source) + data := utils.ReadingLines(step.Source) + if len(data) <= 0 { + return out, fmt.Errorf("missing source") + } + if step.Threads != "" { + step.Parallel = cast.ToInt(step.Threads) + } + if step.Parallel == 0 { + step.Parallel = 1 + } + + // prepare the data first + var newGeneratedSteps []libs.Step + for index, line := range data { + customParams := make(map[string]string) + customParams["line"] = line + customParams["line_id"] = fmt.Sprintf("%v-%v", path.Base(line), index) + customParams["_id_"] = fmt.Sprintf("%v", index) + customParams["_line_"] = execution.StripName(line) + + // make completely new Step + localStep := libs.Step{} + + for _, cmd := range step.Commands { + localStep.Commands = append(localStep.Commands, AltResolveVariable(cmd, customParams)) + } + for _, cmd := range step.RCommands { + localStep.RCommands = append(localStep.RCommands, AltResolveVariable(cmd, customParams)) + } + + if len(step.Ose) > 0 { + for _, ose := range step.Ose { + localStep.Ose = append(localStep.Ose, AltResolveVariable(ose, customParams)) + } + } + + for _, script := range step.RScripts { + localStep.RScripts = append(localStep.RScripts, AltResolveVariable(script, customParams)) + } + + for _, script := range step.Scripts { + localStep.Scripts = append(localStep.Scripts, AltResolveVariable(script, customParams)) + } + + for _, script := range step.PConditions { + localStep.PConditions = append(localStep.PConditions, AltResolveVariable(script, customParams)) + } + for _, script := range step.PScripts { + localStep.PScripts = append(localStep.PScripts, AltResolveVariable(script, customParams)) + } + + newGeneratedSteps = append(newGeneratedSteps, localStep) + } + + // skip concurrency part + if step.Parallel == 1 { + for _, newGeneratedStep := range newGeneratedSteps { + out, err = r.RunStep(newGeneratedStep) + if err != nil { + continue + } + } + } else { + ///////////// + // run multiple steps in concurrency mode + + utils.DebugF("Running the step in parallel: %v", step.Parallel) + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(step.Parallel, func(i interface{}) { + r.startStepJob(i) + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + for _, newGeneratedStep := range newGeneratedSteps { + wg.Add(1) + err = p.Invoke(newGeneratedStep) + if err != nil { + utils.ErrorF("Error in parallel: %v", err) + } + } + + wg.Wait() + } + return out, nil +} + +func (r *Runner) startStepJob(j interface{}) { + localStep := j.(libs.Step) + + err := r.CheckCondition(localStep.Conditions) + + if err != nil { + // run reverse commands + if len(localStep.RCommands) > 0 { + r.RunCommands(localStep.RCommands, localStep.Std) + } + if len(localStep.RScripts) > 0 { + r.RunScripts(localStep.RScripts) + } + } else { + if len(localStep.Commands) > 0 { + r.RunCommands(localStep.Commands, localStep.Std) + } + } + + if len(localStep.Ose) > 0 { + for _, ose := range localStep.Ose { + r.RunOse(ose) + } + } + + if len(localStep.Scripts) > 0 { + r.RunScripts(localStep.Scripts) + } + + // post scripts + if len(localStep.PConditions) > 0 || len(localStep.PScripts) > 0 { + err := r.CheckCondition(localStep.PConditions) + if err == nil { + if len(localStep.PScripts) > 0 { + r.RunScripts(localStep.PScripts) + } + } + } +} diff --git a/core/parse.go b/core/parse.go new file mode 100644 index 0000000..47125a3 --- /dev/null +++ b/core/parse.go @@ -0,0 +1,322 @@ +package core + +import ( + "bytes" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/Jeffail/gabs/v2" + "github.com/fatih/color" + "github.com/flosch/pongo2/v6" + "github.com/spf13/cast" + "golang.org/x/net/publicsuffix" + + "github.com/Shopify/yaml" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +// ResolveData resolve template from signature file +func ResolveData(format string, data map[string]string) string { + // for backward compatibility because new template using `{{variable}}` instead of `{{.variable}}` + if strings.Contains(format, "{{.") { + return OldResolveData(format, data) + } + + variable := make(map[string]interface{}) + for k, v := range data { + variable[k] = v + } + if tpl, err := pongo2.FromString(format); err == nil { + out, ok := tpl.Execute(variable) + if ok == nil { + return out + } + utils.ErrorF("Error when resolve template: %v", ok) + } + return format +} + +// OldResolveData resolve template from signature file +func OldResolveData(format string, data map[string]string) string { + t := template.Must(template.New("").Parse(format)) + + buf := &bytes.Buffer{} + err := t.Execute(buf, data) + if err != nil { + utils.ErrorF("Error render: %v -- %v", format, err) + return format + } + return buf.String() +} + +// ResolveSlice resolve template from signature file +func ResolveSlice(slice []string, data map[string]string) (resolveSlice []string) { + for _, s := range slice { + resolveSlice = append(resolveSlice, ResolveData(s, data)) + } + return resolveSlice +} + +// AltResolveVariable just like ResolveVariable but looking for [[.var]] +func AltResolveVariable(format string, data map[string]string) string { + t := template.Must(template.New("").Delims("[[", "]]").Parse(format)) + buf := &bytes.Buffer{} + err := t.Execute(buf, data) + if err != nil { + return format + } + return buf.String() +} + +// ParseFlow parse mode file +func ParseFlow(flowFile string) (libs.Flow, error) { + utils.DebugF("Parsing workflow at: %v", color.HiGreenString(flowFile)) + var flow libs.Flow + yamlFile, err := os.ReadFile(flowFile) + if err != nil { + utils.ErrorF("YAML parsing err: %v -- #%v ", flowFile, err) + return flow, err + } + err = yaml.Unmarshal(yamlFile, &flow) + if err != nil { + utils.ErrorF("Error unmarshal: %v -- %v", flowFile, err) + return flow, err + } + + if flow.Usage != "" && strings.Contains(flow.Usage, "{{.this_file}}") { + flow.Usage = strings.ReplaceAll(flow.Usage, "{{.this_file}}", flowFile) + } + return flow, nil +} + +// ParseModules parse module file +func ParseModules(moduleFile string) (libs.Module, error) { + utils.DebugF("Parsing module at: %v", color.HiCyanString(moduleFile)) + + var module libs.Module + + yamlFile, err := os.ReadFile(moduleFile) + if err != nil { + utils.ErrorF("YAML parsing err: %v -- #%v ", moduleFile, err) + return module, err + } + err = yaml.Unmarshal(yamlFile, &module) + if err != nil { + utils.ErrorF("Error unmarshal: %v -- %v", moduleFile, err) + return module, err + } + module.ModulePath = moduleFile + if module.Usage != "" && strings.Contains(module.Usage, "{{this_file}}") { + module.Usage = strings.ReplaceAll(module.Usage, "{{this_file}}", moduleFile) + } + return module, err +} + +// ParseInputFormat format input +func ParseInputFormat(raw string, options libs.Options) map[string]string { + target := make(map[string]string) + target["RawFormat"] = raw + + jsonParsed, err := gabs.ParseJSON([]byte(raw)) + if err != nil { + return target + } + + // parse Target first if found one + rawURL, ok := jsonParsed.ChildrenMap()["Target"] + if !ok { + panic("missing Target in special input") + } + + target = ParseInput(cast.ToString(rawURL.Data()), options) + + // override the whole thing + for k, v := range jsonParsed.ChildrenMap() { + target[k] = fmt.Sprintf("%v", v.Data()) + } + + return target +} + +// ParseInput parse input for routine +func ParseInput(raw string, options libs.Options) map[string]string { + ROptions := ParseTarget(raw) + if options.EnableFormatInput { + // avoid the loophole + options.EnableFormatInput = false + ROptions = ParseInputFormat(raw, options) + return ROptions + } + // some data stuff + dir, err := os.Getwd() + if err == nil { + ROptions["CWD"] = dir + } + + // default threads variables + ROptions["Threads"] = cast.ToString(options.Threads) + ROptions["threads"] = cast.ToString(options.Threads) + ROptions["thread"] = cast.ToString(options.Threads) + ROptions["baseThreads"] = cast.ToString(options.Threads) + + ROptions["Version"] = libs.VERSION + ROptions["Bucket"] = options.Cdn.Bucket + + ROptions["Today"] = time.Now().Format("2006-01-02") + ROptions["CurrentDay"] = time.Now().Format("2006-01-02T15:04:05") + ROptions["Date"] = time.Now().Format("2006-01-02T15:04:05") + ROptions["TimeStamp"] = utils.GetTS() + ROptions["TS"] = time.Now().Format("2006-01-02") + "T" + utils.GetTS() + + /* --- start to load default Env --- */ + // ~/osmedeus-base + ROptions["BaseFolder"] = utils.NormalizePath(strings.TrimLeft(options.Env.BaseFolder, "/")) + ROptions["Plugins"] = options.Env.BinariesFolder + ROptions["Binaries"] = options.Env.BinariesFolder + + ROptions["Backup"] = options.Env.BackupFolder + ROptions["Data"] = options.Env.DataFolder + ROptions["Workflow"] = options.Env.WorkFlowsFolder + ROptions["Scripts"] = options.Env.WorkFlowsFolder + ROptions["Cloud"] = options.Env.CloudConfigFolder + + ROptions["Workspaces"] = options.Env.WorkspacesFolder + if options.Scan.BaseWorkspace != "" { + ROptions["Workspaces"] = options.Scan.BaseWorkspace + } + ROptions["Storages"] = options.Env.StoragesFolder + /* --- end of load default Env --- */ + + ROptions["Workspace"] = utils.CleanPath(raw) + if options.Scan.CustomWorkspace != "" { + ROptions["Workspace"] = utils.CleanPath(options.Scan.CustomWorkspace) + } + ROptions["Output"] = path.Join(ROptions["Workspaces"], ROptions["Workspace"]) + + // params in workflow file + if len(options.Flow.Params) > 0 { + for _, param := range options.Flow.Params { + for k, v := range param { + v = ResolveData(v, ROptions) + if strings.HasPrefix(v, "~/") { + v = utils.NormalizePath(v) + } + ROptions[k] = v + } + } + } + + return ROptions +} + +// ParseParams parse more params from cli +func ParseParams(rawParams []string) map[string]string { + params := make(map[string]string) + for _, item := range rawParams { + if strings.Contains(item, "=") { + data := strings.Split(item, "=") + params[data[0]] = strings.Replace(item, data[0]+"=", "", -1) + } + } + return params +} + +// ParseTarget parsing target and some variable for template +func ParseTarget(raw string) map[string]string { + target := make(map[string]string) + if raw == "" { + return target + } + target["Target"] = raw + u, err := url.Parse(raw) + + // something wrong so parsing it again + if err != nil || u.Scheme == "" || strings.Contains(u.Scheme, ".") { + raw = fmt.Sprintf("https://%v", raw) + u, err = url.Parse(raw) + if err != nil { + return target + } + // fmt.Println("parse again") + } + var hostname string + var query string + port := u.Port() + // var domain string + domain := u.Hostname() + + query = u.RawQuery + if u.Port() == "" { + if strings.Contains(u.Scheme, "https") { + port = "443" + } else { + port = "80" + } + + hostname = u.Hostname() + } else { + // ignore common port in Host + if u.Port() == "443" || u.Port() == "80" { + hostname = u.Hostname() + } else { + hostname = u.Hostname() + ":" + u.Port() + } + } + + target["Scheme"] = u.Scheme + target["Path"] = u.Path + target["Domain"] = domain + + target["Org"] = domain + suffix, ok := publicsuffix.PublicSuffix(domain) + if ok { + target["Org"] = strings.Replace(domain, fmt.Sprintf(".%s", suffix), "", -1) + } else { + if strings.Contains(domain, ".") { + parts := strings.Split(domain, ".") + if len(parts) == 2 { + target["Org"] = parts[0] + } else { + target["Org"] = parts[len(parts)-2] + } + } + } + + target["Host"] = hostname + target["Port"] = port + target["RawQuery"] = query + + if (target["RawQuery"] != "") && (port == "80" || port == "443") { + target["URL"] = fmt.Sprintf("%v://%v%v?%v", target["Scheme"], target["Host"], target["Path"], target["RawQuery"]) + } else if port != "80" && port != "443" { + target["URL"] = fmt.Sprintf("%v://%v:%v%v?%v", target["Scheme"], target["Domain"], target["Port"], target["Path"], target["RawQuery"]) + } else { + target["URL"] = fmt.Sprintf("%v://%v%v", target["Scheme"], target["Host"], target["Path"]) + } + + uu, _ := url.Parse(raw) + target["BaseURL"] = fmt.Sprintf("%v://%v", uu.Scheme, uu.Host) + target["Extension"] = filepath.Ext(target["BaseURL"]) + + return target +} + +func IsRootDomain(raw string) bool { + suffix, ok := publicsuffix.PublicSuffix(raw) + if ok { + return false + } + + input := strings.ReplaceAll(raw, fmt.Sprintf(".%s", suffix), "") + if strings.Count(input, ".") == 1 && strings.Count(input, "/") == 0 { + return true + } + return false +} diff --git a/core/parse_test.go b/core/parse_test.go new file mode 100644 index 0000000..bd392dc --- /dev/null +++ b/core/parse_test.go @@ -0,0 +1,71 @@ +package core + +import ( + "fmt" + "github.com/flosch/pongo2/v6" + "runtime" + "testing" +) + +func TestParseTarget(t *testing.T) { + fmt.Println("----> INPUT:", "http://example.com") + result := ParseTarget("http://exmaple.com") + fmt.Println(result) + if len(result) == 0 { + t.Errorf("Error RunMasscan") + } + // case 2 + fmt.Println("----> INPUT:", "example.com") + result = ParseTarget("exmaple.com") + fmt.Println(result) + if len(result) == 0 { + t.Errorf("Error RunMasscan") + } + + // case 2 + fmt.Println("----> INPUT:", "http://exmaple.com/123?q=1") + result = ParseTarget("http://exmaple.com/123?q=1") + fmt.Println(result) + if len(result) == 0 { + t.Errorf("Error RunMasscan") + } + + // case 2 + fmt.Println("----> INPUT:", "1.2.3.4") + result = ParseTarget("1.2.3.4") + fmt.Println(result) + if len(result) == 0 { + t.Errorf("Error RunMasscan") + } + + // case 2 + fmt.Println("----> INPUT:", "1.2.3.4/24") + result = ParseTarget("1.2.3.4/24") + fmt.Println(result) + if len(result) == 0 { + t.Errorf("Error RunMasscan") + } +} + +func TestRenderTemplate(t *testing.T) { + formatString := "Hello {{name}}! \n" + formatString += "--> Express: {{ 2 * cpu }} \n" + formatString += "--> Bool: {{ Skip }} \n" + target := map[string]any{ + "name": "World", + "num": "2", + "cpu": runtime.NumCPU(), + "Skip": true, + } + fmt.Println(target) + + if tpl, err := pongo2.FromString(formatString); err == nil { + // Now you can render the template with the given + // pongo2.Context how often you want to. + out, err := tpl.Execute(target) + if err != nil { + panic(err) + } + fmt.Println(out) // Output: Hello Florian! + } +} diff --git a/core/queue.go b/core/queue.go new file mode 100644 index 0000000..52c8934 --- /dev/null +++ b/core/queue.go @@ -0,0 +1,147 @@ +package core + +import ( + "fmt" + "github.com/fatih/color" + "github.com/fsnotify/fsnotify" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" + "github.com/panjf2000/ants" + "github.com/thoas/go-funk" + "os" + "strings" + "sync" +) + +func QueueWatcher(options libs.Options) { + queueFile := options.Queue.QueueFile + + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(options.Concurrency, func(i interface{}) { + RunTheScan(i.(string), options) + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + data := utils.ReadingFileUnique(queueFile) + + /* Start the watcher */ + + // Create new watcher. + watcher, err := fsnotify.NewWatcher() + if err != nil { + panic(err) + } + defer watcher.Close() + utils.InforF("Starting to watch the queue file: %v", color.HiMagentaString(queueFile)) + + // Start listening for events. + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op.String() == "WRITE" { + utils.DebugF(color.HiMagentaString("modified file: %v -- %v", event.Name, event.Op)) + target := GetNewLine(queueFile) + wg.Add(1) + _ = p.Invoke(strings.TrimSpace(target)) + } + + if event.Op.String() == "REMOVE" { + utils.ErrorF("Queue file removed, exiting ...") + os.Exit(-1) + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + utils.ErrorF("error: %v", err) + } + } + }() + + err = watcher.Add(queueFile) + if err != nil { + panic(err) + } + + // Just to trigger the file events + utils.WriteToFile(queueFile, strings.Join(append(data, "\n"), "\n")) + + // Block main goroutine forever. + <-make(chan struct{}) +} + +func RunTheScan(target string, options libs.Options) error { + if strings.TrimSpace(target) == "" { + return fmt.Errorf("target is empty") + } + utils.InforF("Picking the target from the queue: %v", color.CyanString(target)) + + var inputFormat libs.InputFormat + if ok := jsoniter.UnmarshalFromString(target, &inputFormat); ok == nil { + utils.DebugF("Parsing the input in JSON format: %v", color.CyanString(target)) + cmd := CommandBuilder(inputFormat) + utils.InforF("Running the command: %v", color.CyanString(cmd)) + utils.RunOSCommand(cmd) + return nil + } + + runner, err := InitRunner(target, options) + if err != nil { + utils.ErrorF("Error init runner with: %s", target) + return err + } + runner.Start() + + return nil +} + +func GetNewLine(queueFile string) string { + data := utils.ReadingLines(queueFile) + if len(data) == 0 { + return "" + } + + target := data[0] + data = funk.DropString(data, 1) + utils.DebugF("Getting the target from the queue file: %v -- %v", queueFile, color.CyanString(target)) + utils.WriteToFile(queueFile, strings.Join(data, "\n")) + return target +} + +func CommandBuilder(inputFormat libs.InputFormat) (command string) { + if inputFormat.Command == "" { + inputFormat.Command = fmt.Sprintf("%v scan -t %v", libs.BINARY, inputFormat.Input) + if inputFormat.InputAsFile { + inputFormat.Command = fmt.Sprintf("%v scan -T %v", libs.BINARY, inputFormat.Input) + } + + if inputFormat.Flow == "" { + inputFormat.Command += " -f " + inputFormat.Flow + } + + // append the modules + if len(inputFormat.Modules) > 0 { + for _, item := range inputFormat.Modules { + inputFormat.Command += " -m " + item + } + } + + if len(inputFormat.Params) > 0 { + for _, item := range inputFormat.Params { + inputFormat.Command += " -p " + item + } + } + + inputFormat.Command += " " + inputFormat.Extra + } + + // formatting the command if there is any input in it + inputFormat.Command = strings.ReplaceAll(inputFormat.Command, "{{.input}}", inputFormat.Input) + return command +} diff --git a/core/reference.go b/core/reference.go new file mode 100644 index 0000000..9d8a7da --- /dev/null +++ b/core/reference.go @@ -0,0 +1,104 @@ +package core + +/* File to store all the script for better reference */ + +const ( + Cleaning = "Cleaning" + CleanAmass = "CleanAmass" + CleanRustScan = "CleanRustScan" + CleanGoBuster = "CleanGoBuster" + CleanMassdns = "CleanMassdns" + CleanSWebanalyze = "CleanSWebanalyze" + CleanJSONDnsx = "CleanJSONDnsx" + CleanWebanalyze = "CleanWebanalyze" + CleanArjun = "CleanArjun" + GenNucleiReport = "GenNucleiReport" + CleanJSONHttpx = "CleanJSONHttpx" + CleanFFUFJson = "CleanFFUFJson" +) + +const ( + // noti for slack + StartNoti = "StartNoti" + DoneNoti = "DoneNoti" + ReportNoti = "ReportNoti" + DiffNoti = "DiffNoti" + CustomNoti = "CustomNoti" + NotiFile = "NotiFile" + WebHookNoti = "WebHookNoti" + // noti for telegram + TeleMess = "TeleMess" + TeleMessWrap = "TeleMessWrap" + TeleMessByFile = "TeleMessByFile" + TeleSendFile = "TeleSendFile" +) + +const ( + ExecCmd = "ExecCmd" + ExecCmdB = "ExecCmdB" + ExecCmdWithOutput = "ExecCmdWithOutput" + ExecContain = "ExecContain" + Sleep = "Sleep" + Exit = "Exit" + CastToInt = "CastToInt" + StripSlash = "StripSlash" + Printf = "Printf" + Cat = "Cat" + SortU = "SortU" + SplitFile = "SplitFile" + Append = "Append" + Copy = "Copy" + CreateFolder = "CreateFolder" + DeleteFile = "DeleteFile" + DeleteFolder = "DeleteFolder" + SplitFileByPart = "SplitFileByPart" + FileLength = "FileLength" + IsFile = "IsFile" + EmptyDir = "EmptyDir" + EmptyFile = "EmptyFile" + ReadLines = "ReadLines" + Compress = "Compress" + Decompress = "Decompress" +) + +const ( + TotalSubdomain = "TotalSubdomain" + TotalDns = "TotalDns" + TotalScreenShot = "TotalScreenShot" + TotalTech = "TotalTech" + TotalVulnerability = "TotalVulnerability" + TotalArchive = "TotalArchive" + TotalLink = "TotalLink" + TotalDirb = "TotalDirb" + CreateReport = "CreateReport" +) + +const ( + RRSync = "RRSync" + Clone = "Clone" + FClone = "FClone" + PushResult = "PushResult" + PushFolder = "PushFolder" + PullFolder = "PullFolder" + DiffCompare = "DiffCompare" + GitDiff = "GitDiff" + LoopGitDiff = "LoopGitDiff" + // for gitlab API only + CreateRepo = "CreateRepo" + DeleteRepo = "DeleteRepo" + DeleteRepoByPid = "DeleteRepoByPid" + ListProjects = "ListProjects" +) + +const ( + UploadToS3 = "UploadToS3" + DownloadFromS3 = "DownloadFromS3" + DownloadFile = "DownloadFile" + GenMarkdownReport = "GenMarkdownReport" +) + +const ( + SetVar = "SetVar" + SetOSVar = "SetOSVar" + GetOSEnv = "GetOSEnv" +) diff --git a/core/report.go b/core/report.go new file mode 100644 index 0000000..d0293d6 --- /dev/null +++ b/core/report.go @@ -0,0 +1,215 @@ +package core + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/Jeffail/gabs/v2" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cast" +) + +func ListWorkspaces(options libs.Options) (content [][]string) { + workspaces, err := os.ReadDir(utils.NormalizePath(options.Env.WorkspacesFolder)) + if err != nil { + utils.ErrorF("Error reading workspaces folder: %s", err) + return content + } + + for _, ws := range workspaces { + if ws.IsDir() { + status := "unknown" + flowName := "unknown" + progress := "N/A" + wsFolder := path.Join(utils.NormalizePath(options.Env.WorkspacesFolder), ws.Name()) + + if utils.DirLength(wsFolder) == 0 { + continue + } + + if utils.FileExists(path.Join(wsFolder, "done")) { + status = "done" + } + + runtimeFile := path.Join(wsFolder, "runtime") + if utils.FileExists(runtimeFile) { + utils.DebugF("Reading information from: %v", runtimeFile) + runtimeContent := utils.GetFileContent(runtimeFile) + + if jsonParsed, ok := gabs.ParseJSON([]byte(runtimeContent)); ok == nil { + flowName = cast.ToString(jsonParsed.S("task_name").Data()) + doneStep := cast.ToString(jsonParsed.S("done_step").Data()) + totalSteps := cast.ToString(jsonParsed.S("total_steps").Data()) + isRunning := cast.ToString(jsonParsed.S("is_running").Data()) + + if isRunning == "true" { + status = "running" + } + + progress = color.HiCyanString(fmt.Sprintf(doneStep + "/" + totalSteps)) + } + } + + row := []string{ + path.Base(color.HiMagentaString(ws.Name())), status, flowName, progress, wsFolder, + } + content = append(content, row) + } + } + + table := tablewriter.NewWriter(os.Stderr) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"Workspace Name", "Status", "Routine", "Progress", "Workspace Path"}) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetColWidth(120) + table.AppendBulk(content) + table.Render() + + fmt.Println(color.HiGreenString("šŸ“ Total Workspaces: ") + color.HiMagentaString("%v", len(content))) + usage := color.HiWhiteString("šŸ’” How to view report:") + color.HiGreenString(" osmedeus report view -t %v", color.HiMagentaString("[targetName]")) + fmt.Println(usage) + + return content +} + +func ListSingleWorkspace(options libs.Options, target string) (content [][]string) { + workspaces, err := os.ReadDir(utils.NormalizePath(options.Env.WorkspacesFolder)) + if err != nil { + utils.ErrorF("Error reading workspaces folder: %s", err) + return content + } + + header := []string{"Module", "Report Name", "Report Path"} + for _, ws := range workspaces { + if !ws.IsDir() { + continue + } + // compare target name with workspace name + if target != path.Base(ws.Name()) { + continue + } + wsFolder := path.Join(utils.NormalizePath(options.Env.WorkspacesFolder), ws.Name()) + + runtimeFile := path.Join(wsFolder, "runtime") + // only listing file that in report part + if utils.FileExists(runtimeFile) && !options.Report.Raw { + utils.InforF("Reading runtime information from: %v", runtimeFile) + runtimeContent := utils.GetFileContent(runtimeFile) + + isImported := false + if !strings.Contains(runtimeContent, options.Env.WorkspacesFolder) { + isImported = true + } + + // replace the workspace folder if it doesn't exist + if !strings.Contains(runtimeContent, options.Env.WorkspacesFolder) { + homeFolder := "/root/.osmedeus/workspaces/" + if strings.Contains(runtimeContent, "/root/workspaces-osmedeus/") { + homeFolder = "/root/workspaces-osmedeus/" + } + runtimeContent = strings.ReplaceAll(runtimeContent, homeFolder, options.Env.WorkspacesFolder+"/") + } + + if strings.Contains(runtimeContent, "/root/.osmedeus/workspaces") { + runtimeContent = strings.ReplaceAll(runtimeContent, "/root/.osmedeus/workspaces", options.Env.WorkspacesFolder) + } + + row := []string{"==> Workspace Name", color.HiGreenString(ws.Name()), color.HiGreenString(wsFolder)} + content = append(content, row) + + if jsonParsed, ok := gabs.ParseJSON([]byte(runtimeContent)); ok == nil { + reports := jsonParsed.S("target", "reports").Children() + + for _, report := range reports { + moduleName := cast.ToString(report.S("module").Data()) + reportName := cast.ToString(report.S("report_name").Data()) + reportPath := cast.ToString(report.S("report_path").Data()) + + if !utils.FileExists(reportPath) { + // /root/.osmedeus/workspaces + continue + } + + row := []string{ + moduleName, processReport(options, reportName), processReport(options, reportPath), + } + + content = append(content, row) + } + + markDownSunmmary := cast.ToString(jsonParsed.S("markdown_summary").Data()) + if utils.FileExists(markDownSunmmary) { + row := []string{"==> Markdown Summary", path.Base(markDownSunmmary), color.HiGreenString(markDownSunmmary)} + content = append(content, row) + } + markDownReport := cast.ToString(jsonParsed.S("markdown_report").Data()) + if utils.FileExists(markDownReport) { + row := []string{"==> Markdown HTML Summary", path.Base(markDownReport), color.HiGreenString(markDownReport)} + content = append(content, row) + } + + sep := []string{"==> --------", color.HiGreenString("----------"), color.HiGreenString("-----------")} + content = append(content, sep) + + if len(content) <= 2 && isImported { + utils.WarnF("Workspace folder not found in runtime file, you might extract it from different machine that being run on different user") + utils.WarnF("šŸ’” If you still have problem, please view it as raw format: %s", color.HiGreenString("osmedeus view -t %s --raw", target)) + } + + } + continue + } + + // --raw flag: list all avaliable file + header = []string{"Report Name", "Report Path"} + if options.Report.Static { + header = []string{"Report Name", "Report URL"} + } + row := []string{"==> Workspace Name", color.HiGreenString(ws.Name())} + content = append(content, row) + + filepath.Walk(wsFolder, func(reportPath string, _ os.FileInfo, err error) error { + reportName := path.Base(reportPath) + row := []string{processReport(options, reportName), processReport(options, reportPath)} + content = append(content, row) + return nil + }) + + sep := []string{"<== --------", color.HiGreenString("-----------")} + content = append(content, sep) + + } + + table := tablewriter.NewWriter(os.Stderr) + table.SetAutoFormatHeaders(true) + table.SetHeader(header) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetColWidth(120) + table.AppendBulk(content) + table.SetHeaderLine(true) + table.Render() + + return content +} + +func processReport(options libs.Options, reportPath string) string { + if options.Report.Static { + base := fmt.Sprintf("https://%s:8000/%s/workspaces", options.Report.PublicIP, options.Server.StaticPrefix) + reportPath = strings.ReplaceAll(reportPath, options.Env.WorkspacesFolder, base) + } + + if strings.HasSuffix(reportPath, ".html") { + reportPath = color.HiCyanString(reportPath) + } + if strings.HasSuffix(reportPath, ".json") { + reportPath = color.HiBlueString(reportPath) + } + + return reportPath +} diff --git a/core/runner.go b/core/runner.go new file mode 100644 index 0000000..70262e1 --- /dev/null +++ b/core/runner.go @@ -0,0 +1,373 @@ +package core + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/Shopify/yaml" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/database" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/panjf2000/ants" + "github.com/robertkrimen/otto" + "github.com/thoas/go-funk" +) + +// Runner runner struct to start a job +type Runner struct { + Input string + Workspace string + + InputType string // domain, url, ip, cidr or domain-file, url-file, ip-file, cidr-file + RequiredInput string // this should match with InputType + IsInvalid bool + ForceParams bool + + RoutineType string // module or flow + RoutineName string // general + RoutinePath string + RunnerSource string // cli or api + RunnerType string // local or cloud + + Opt libs.Options + + // use for analytics + DoneStep int + TotalSteps int + RunningTime int + CurrentModule string + + DoneFile string + RuntimeFile string + WorkspaceFolder string + + RoutineModules []string + Reports []string + Routines []libs.Routine + + VM *otto.Otto + TargetObj database.Target + ScanObj database.Scan + + Target map[string]string + // this is same as targets but won't change during the execution time + Params map[string]string +} + +// InitRunner init runner +func InitRunner(input string, opt libs.Options) (Runner, error) { + var runner Runner + runner.Input = input + runner.Opt = opt + runner.PrepareRoutine() + runner.InitVM() + + runner.RunnerSource = "cli" + // @TODO check if running in cloud + runner.RunnerType = "local" + return runner, nil +} + +// PrepareWorkflow prepare workflow file +func (r *Runner) PrepareWorkflow() { + allFlows := ListAllFlowName(r.Opt) + + r.RoutineType = "flow" + var err error + flows := SelectFlow(r.Opt.Scan.Flow, r.Opt) + for _, flow := range flows { + if strings.TrimSpace(flow) == "" { + continue + } + r.Opt.Flow, err = ParseFlow(flow) + r.RoutinePath = flow + if err != nil { + continue + } + + if r.Opt.Flow.NoDB { + r.Opt.NoDB = true + } + + if r.Opt.Flow.Type == "general" { + r.Opt.Flow.Input = r.Opt.Scan.Input + } + // default folder to look for module file if not specify + r.Opt.Flow.DefaultType = r.Opt.Flow.Type + + r.Target["FlowPath"] = flow + r.RequiredInput = r.Opt.Flow.Validator + r.ForceParams = r.Opt.Flow.ForceParams + + // get more params from flow + if len(r.Opt.Flow.Params) > 0 { + for _, params := range r.Opt.Flow.Params { + for k, v := range params { + r.Target[k] = v + } + } + } + } + + // generate routines + for _, routine := range r.Opt.Flow.Routines { + // select module depend on the flow type + if routine.FlowFolder != "" { + r.Opt.Flow.Type = routine.FlowFolder + } else { + r.Opt.Flow.Type = r.Opt.Flow.DefaultType + } + + modules := SelectModules(routine.Modules, r.Opt) + routine.RoutineName = fmt.Sprintf("flow-%s", r.Opt.Flow.Name) + + for _, module := range modules { + parsedModule, err := ParseModules(module) + if err != nil || parsedModule.Name == "" { + continue + } + r.TotalSteps += len(parsedModule.Steps) + routine.ParsedModules = append(routine.ParsedModules, parsedModule) + } + r.Routines = append(r.Routines, routine) + } + + if len(r.Routines) == 0 { + if r.Opt.Scan.Flow != "cloud-distributed" { + utils.WarnF("Your workflow %v doesn't exist", color.HiRedString(r.Opt.Scan.Flow)) + utils.WarnF("Please select one of these flow: %v", color.HiMagentaString(strings.Join(allFlows, ", "))) + } + } +} + +func (r *Runner) PrepareModule() { + allModules := ListModuleName(r.Opt) + r.RoutineType = "module" + + var err error + for _, rawModule := range r.Opt.Scan.Modules { + var routine libs.Routine + + module := DirectSelectModule(r.Opt, rawModule) + r.RoutinePath = rawModule + r.Opt.Module, err = ParseModules(module) + if err != nil || r.Opt.Module.Name == "" { + utils.WarnF("Your module %v doesn't exist", color.HiRedString(r.Opt.Scan.Modules[0])) + utils.WarnF("Please select one of these module: %v", color.HiMagentaString(strings.Join(allModules, ", "))) + continue + } + if r.Opt.Module.NoDB { + r.Opt.NoDB = true + } + + r.Target["FlowPath"] = "direct-module" + routine.ParsedModules = append(routine.ParsedModules, r.Opt.Module) + routine.RoutineName = fmt.Sprintf("module-%s", r.Opt.Flow.Name) + r.Target["Module"] = module + + r.Routines = append(r.Routines, routine) + + r.TotalSteps += len(r.Opt.Module.Steps) + r.RequiredInput = r.Opt.Module.Validator + } +} + +func (r *Runner) PrepareRoutine() { + // prepare targets + r.Target = ParseInput(r.Input, r.Opt) + r.Workspace = r.Target["Workspace"] + + // take from -m flag + if len(r.Opt.Scan.Modules) > 0 { + r.RoutineName = strings.Join(r.Opt.Scan.Modules, "-") + r.PrepareModule() + return + } + + // take from -f flag + if r.Opt.Scan.Flow != "" { + r.RoutineName = r.Opt.Scan.Flow + r.PrepareWorkflow() + } +} + +// PrepareParams prepare global params +func (r *Runner) PrepareParams() { + r.Params = r.Target + + // looking for more params from each module + for _, routine := range r.Routines { + for _, module := range routine.ParsedModules { + // params from module file + if len(module.Params) > 0 { + for _, param := range module.Params { + for k, v := range param { + // skip params if override: false + _, exist := r.Params[k] + if r.ForceParams && exist { + utils.DebugF("Skip override param: %v --> %v", k, v) + continue + } + + v = ResolveData(v, r.Params) + if strings.HasPrefix(v, "~/") { + v = utils.NormalizePath(v) + } + r.Params[k] = v + } + } + } + + if len(r.Opt.Scan.ParamsFile) > 0 { + var params map[string]string + yamlFile, err := os.ReadFile(r.Opt.Scan.ParamsFile) + if err != nil { + utils.ErrorF("YAML parsing err: %v -- #%v ", r.Opt.Scan.ParamsFile, err) + return + } + err = yaml.Unmarshal(yamlFile, ¶ms) + if err != nil { + utils.ErrorF("Error unmarshal: %v -- %v", params, err) + return + } + if len(params) > 0 { + for k, v := range params { + v = ResolveData(v, r.Params) + r.Params[k] = v + } + } + } + + // more params from -p flag which will override everything + if len(r.Opt.Scan.Params) > 0 { + params := ParseParams(r.Opt.Scan.Params) + if len(params) > 0 { + for k, v := range params { + v = ResolveData(v, r.Params) + r.Params[k] = v + } + } + } + } + } + + r.ResolveRoutine() + +} + +// ResolveRoutine resolve the module name first +func (r *Runner) ResolveRoutine() { + var routines []libs.Routine + + for _, rawRoutine := range r.Routines { + + var routine libs.Routine + for _, module := range rawRoutine.ParsedModules { + module = ResolveReports(module, r.Params) + + r.Reports = append(r.Reports, module.Report.Final...) + module.PreRun = ResolveSlice(module.PreRun, r.Params) + + // steps + for i, step := range module.Steps { + module.Steps[i].Timeout = ResolveData(step.Timeout, r.Params) + module.Steps[i].Threads = ResolveData(step.Threads, r.Params) + module.Steps[i].Label = ResolveData(step.Label, r.Params) + module.Steps[i].Std = ResolveData(step.Std, r.Params) + module.Steps[i].Source = ResolveData(step.Source, r.Params) + + module.Steps[i].Conditions = ResolveSlice(step.Conditions, r.Params) + module.Steps[i].Required = ResolveSlice(step.Required, r.Params) + + module.Steps[i].Commands = ResolveSlice(step.Commands, r.Params) + module.Steps[i].Scripts = ResolveSlice(step.Scripts, r.Params) + + module.Steps[i].RCommands = ResolveSlice(step.RCommands, r.Params) + module.Steps[i].RScripts = ResolveSlice(step.RScripts, r.Params) + module.Steps[i].PConditions = ResolveSlice(step.PConditions, r.Params) + module.Steps[i].PScripts = ResolveSlice(step.PScripts, r.Params) + module.Steps[i].Ose = ResolveSlice(step.Ose, r.Params) + } + + module.PostRun = ResolveSlice(module.PostRun, r.Params) + routine.ParsedModules = append(routine.ParsedModules, module) + } + + routines = append(routines, routine) + } + r.Routines = routines +} + +func (r *Runner) Start() { + err := r.Validator() + if err != nil { + utils.ErrorF("Input does not match the require type: %v -- %v", r.RequiredInput, r.Input) + utils.InforF("Adding %v flag if you want to disable input validate", color.HiCyanString(`'--nv'`)) + return + } + utils.InforF("Running %s tactic with baseline threads hold as %s", color.YellowString(r.Opt.Tactics), color.HiMagentaString("%v", r.Opt.Threads)) + + r.Opt.Scan.ROptions = r.Target + // prepare some metadata files + utils.MakeDir(r.Target["Output"]) + r.DoneFile = r.Target["Output"] + "/done" + r.RuntimeFile = r.Target["Output"] + "/runtime" + r.WorkspaceFolder = r.Target["Output"] + os.Remove(r.DoneFile) + + utils.InforF("Running the routine %v on %v", color.HiYellowString(r.RoutineName), color.CyanString(r.Input)) + utils.InforF("Detailed runtime file can be found on %v", color.CyanString(r.RuntimeFile)) + execution.TeleSendMess(r.Opt, fmt.Sprintf("%s -- Start new scan: %s -- %s", r.Opt.Noti.ClientName, r.Opt.Scan.Flow, r.Target["Workspace"]), "#status", false) + + r.DBNewTarget() + r.DBNewScan() + r.LoadEngineScripts() + + r.PrepareParams() + + ///// + /* really start the scan here */ + r.StartRoutines() + ///// + + r.DBDoneScan() + utils.BlockF("Finished", fmt.Sprintf("The scan for %v was completed within %v", color.HiCyanString(r.Input), color.HiMagentaString("%vs", r.RunningTime))) + + if r.Opt.EnableBackup { + r.BackupWorkspace() + } +} + +// StartRoutines start the scan +func (r *Runner) StartRoutines() { + for _, routine := range r.Routines { + // start each section of modules + r.RunRoutine(routine.ParsedModules) + } +} + +func (r *Runner) RunRoutine(modules []libs.Module) { + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(r.Opt.Concurrency*10, func(m interface{}) { + module := m.(libs.Module) + r.RunModule(module) + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + for _, module := range modules { + if funk.ContainsString(r.Opt.Exclude, module.Name) { + utils.BadBlockF("Module-Excluded", fmt.Sprintf("Module %v has been excluded", color.CyanString(module.Name))) + continue + } + + p.Invoke(module) + wg.Add(1) + } + + wg.Wait() +} diff --git a/core/runtime.go b/core/runtime.go new file mode 100644 index 0000000..0c5db00 --- /dev/null +++ b/core/runtime.go @@ -0,0 +1,488 @@ +package core + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/utils" + "github.com/robertkrimen/otto" + "github.com/spf13/cast" +) + +// InitVM init scripting engine +func (r *Runner) InitVM() { + r.VM = otto.New() + r.LoadEngineScripts() +} + +func (r *Runner) ExecScript(script string) string { + utils.DebugF("[Run-Scripts] %v", script) + value, err := r.VM.Run(script) + if err == nil { + out, nerr := value.ToString() + if nerr == nil { + return out + } + } + + return "" +} + +// RunOse really start the runner +func (r *Runner) RunOse(scriptName string) { + scriptContent := scriptName + if !strings.Contains(scriptName, "\n") { + scriptFile := SelectScript(scriptName, r.Opt) + if utils.FileExists(scriptFile) { + scriptContent = utils.GetFileContent(scriptFile) + } + } + + if len(scriptContent) == 0 { + utils.ErrorF("Error running script: %s", scriptName) + return + } + + utils.DebugF("-- Start ose:\n\n%s", scriptName) + r.ExecScript(scriptContent) + utils.DebugF("-- Done ose: %s", scriptName) +} + +func (r *Runner) ConditionExecScript(script string) bool { + utils.DebugF("[Run-Scripts] %v", script) + value, err := r.VM.Run(script) + + if err == nil { + out, nerr := value.ToBoolean() + if nerr == nil { + return out + } + } + + return false +} + +func (r *Runner) LoadEngineScripts() { + r.LoadScripts() + r.LoadDBScripts() + // r.LoadImportScripts() + r.LoadExternalScripts() + r.LoadGitScripts() + r.LoadNotiScripts() +} + +func (r *Runner) LoadScripts() string { + var output string + vm := r.VM + + // set attribute + vm.Set("Target", r.Target) + + // SetVar('length', 6) + vm.Set(SetVar, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + varName := args[0].String() + value := args[1].String() + r.Target[varName] = value + return otto.Value{} + }) + + // Exit script used to exit the module + vm.Set(Exit, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + output = fmt.Sprintf("exit(%v)", args[0].String()) + utils.InforF("Exit Detected") + result, err := vm.ToValue(output) + if err == nil { + return result + } + return otto.Value{} + }) + + // ExecCmd execute command + vm.Set(ExecCmd, func(call otto.FunctionCall) otto.Value { + cmd := call.Argument(0).String() + _, err := utils.RunCommandWithErr(cmd) + var validate bool + if err != nil { + validate = true + } + result, err := vm.ToValue(validate) + if err != nil { + return otto.Value{} + } + return result + }) + + // Cat the file to stdout + vm.Set(Cat, func(call otto.FunctionCall) otto.Value { + filename := call.Argument(0).String() + utils.InforF("Showing the content of: %v", color.HiCyanString(filename)) + utils.Cat(filename) + result, err := vm.ToValue(true) + if err != nil { + return otto.Value{} + } + return result + }) + + // ExecCmdB execute in the background + vm.Set(ExecCmdB, func(call otto.FunctionCall) otto.Value { + cmd := call.Argument(0).String() + go func() { + utils.RunOSCommand(cmd) + }() + result, _ := vm.ToValue(true) + return result + }) + + // ExecCmd execute command + vm.Set(ExecCmdWithOutput, func(call otto.FunctionCall) otto.Value { + utils.RunCommandSteamOutput(call.Argument(0).String()) + result, err := vm.ToValue(true) + if err != nil { + return otto.Value{} + } + return result + }) + + // ExecCmd execute command + vm.Set(ExecContain, func(call otto.FunctionCall) otto.Value { + out := utils.RunCmdWithOutput(call.Argument(0).String()) + expected := call.Argument(2).String() + validate := strings.Contains(out, expected) + result, err := vm.ToValue(validate) + if err != nil { + return otto.Value{} + } + return result + }) + + // CastToInt convert string to int + vm.Set(CastToInt, func(call otto.FunctionCall) otto.Value { + toInt := cast.ToInt(call.Argument(0).String()) + result, err := vm.ToValue(toInt) + if err == nil { + return result + } + return otto.Value{} + }) + + vm.Set(FileLength, func(call otto.FunctionCall) otto.Value { + data := utils.FileLength(call.Argument(0).String()) + utils.DebugF("FileLength -- %v", data) + result, err := vm.ToValue(data) + if err == nil { + return result + } + return otto.Value{} + }) + + vm.Set(IsFile, func(call otto.FunctionCall) otto.Value { + data := utils.FileLength(call.Argument(0).String()) + var validate bool + if data > 1 { + validate = true + } + result, _ := vm.ToValue(validate) + return result + }) + + // StripSlash strip last '/' of URL + vm.Set(StripSlash, func(call otto.FunctionCall) otto.Value { + raw := call.Argument(0).String() + out := strings.Trim(raw, "/") + result, err := vm.ToValue(out) + if err != nil { + return otto.Value{} + } + return result + }) + + vm.Set(ReadLines, func(call otto.FunctionCall) otto.Value { + fileName := call.Argument(0).String() + data := utils.ReadingLines(fileName) + if len(data) > 0 { + result, err := vm.ToValue(data) + if err == nil { + return result + } + } + return otto.Value{} + }) + + // Printf simply print a string to console + vm.Set(Printf, func(call otto.FunctionCall) otto.Value { + fmt.Printf("%v\n", color.HiCyanString(call.Argument(0).String())) + returnValue, _ := otto.ToValue(true) + return returnValue + }) + + // split file to multiple + vm.Set(SplitFile, func(call otto.FunctionCall) otto.Value { + execution.SplitFile("size", call.ArgumentList) + return otto.Value{} + }) + + // split file to multiple + vm.Set(SplitFileByPart, func(call otto.FunctionCall) otto.Value { + execution.SplitFile("part", call.ArgumentList) + return otto.Value{} + }) + + vm.Set(Sleep, func(call otto.FunctionCall) otto.Value { + execution.Sleep(call.Argument(0).String()) + returnValue, _ := otto.ToValue(true) + return returnValue + }) + + vm.Set(SortU, func(call otto.FunctionCall) otto.Value { + execution.SortU(call.Argument(0).String()) + returnValue, _ := otto.ToValue(true) + return returnValue + }) + + vm.Set(Append, func(call otto.FunctionCall) otto.Value { + dest := call.Argument(0).String() + src := call.Argument(1).String() + execution.Append(dest, src) + returnValue, _ := otto.ToValue(true) + return returnValue + }) + + vm.Set(Decompress, func(call otto.FunctionCall) otto.Value { + dest := call.Argument(0).String() + src := call.Argument(1).String() + execution.Decompress(dest, src) + returnValue, _ := otto.ToValue(true) + return returnValue + }) + + vm.Set(Compress, func(call otto.FunctionCall) otto.Value { + dest := call.Argument(0).String() + src := call.Argument(1).String() + execution.Compress(dest, src) + returnValue, _ := otto.ToValue(true) + return returnValue + }) + + vm.Set(CreateFolder, func(call otto.FunctionCall) otto.Value { + utils.MakeDir(call.Argument(0).String()) + return otto.Value{} + }) + + vm.Set(DeleteFile, func(call otto.FunctionCall) otto.Value { + execution.DeleteFile(call.Argument(0).String()) + return otto.Value{} + }) + + vm.Set(DeleteFolder, func(call otto.FunctionCall) otto.Value { + execution.DeleteFolder(call.Argument(0).String()) + return otto.Value{} + }) + + vm.Set(Copy, func(call otto.FunctionCall) otto.Value { + src := call.Argument(0).String() + dest := call.Argument(1).String() + execution.Copy(src, dest) + return otto.Value{} + }) + + vm.Set(GetOSEnv, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + env := args[0].String() + defaultValue := env + if len(args) > 1 { + defaultValue = args[1].String() + } + utils.GetOSEnv(env, defaultValue) + return otto.Value{} + }) + + vm.Set(SetOSVar, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + varName := strings.ToUpper(args[0].String()) + defaultValue := varName + if len(args) > 1 { + defaultValue = args[1].String() + } + err := os.Setenv(varName, defaultValue) + if err != nil { + utils.ErrorF("Error setting environment variable: %v", err) + } + return otto.Value{} + }) + + vm.Set(EmptyDir, func(call otto.FunctionCall) otto.Value { + result, _ := vm.ToValue(utils.EmptyDir(call.Argument(0).String())) + return result + }) + + vm.Set(EmptyFile, func(call otto.FunctionCall) otto.Value { + result, err := vm.ToValue(utils.EmptyFile(call.Argument(0).String(), 0)) + if err != nil { + return otto.Value{} + } + if len(call.ArgumentList) > 1 { + num, _ := call.Argument(0).ToInteger() + result, err = vm.ToValue(utils.EmptyFile(call.Argument(0).String(), int(num))) + if err != nil { + return otto.Value{} + } + } + return result + }) + + vm.Set(RRSync, func(call otto.FunctionCall) otto.Value { + vpsIP := call.Argument(0).String() // root@ipaddress + src := call.Argument(1).String() // local path + dest := call.Argument(2).String() // remote path + cmd := fmt.Sprintf("rsync -e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i %s' -avzr --progress %s %s:%s", r.Opt.Cloud.SecretKey, src, vpsIP, dest) + r.RetryCommandWithExpectString(cmd, `bytes/sec`) + return otto.Value{} + }) + + r.VM = vm + + return output +} + +func (r *Runner) RetryCommandWithExpectString(cmd string, expectString string, timeoutRaw ...string) string { + timeout := "300s" + if len(timeoutRaw) > 0 { + timeout = timeoutRaw[0] + } + var out string + + utils.DebugF("Retry command: %s", cmd) + for i := 0; i < r.Opt.Cloud.Retry; i++ { + if timeout == "000" { + out, _ = utils.RunOSCommand(cmd) + } else { + out = utils.RunCmdWithOutput(cmd, timeout) + } + + if !strings.Contains(out, expectString) { + utils.DebugF(out) + time.Sleep(time.Duration(60*(i+1)) * time.Second) + continue + } + return out + } + return out +} + +func (r *Runner) LoadNotiScripts() string { + var output string + vm := r.VM + options := r.Opt + + // script for notification + vm.Set(StartNoti, func(call otto.FunctionCall) otto.Value { + execution.StatusNoti("start", options) + return otto.Value{} + }) + vm.Set(DoneNoti, func(call otto.FunctionCall) otto.Value { + execution.StatusNoti("done", options) + return otto.Value{} + }) + vm.Set(ReportNoti, func(call otto.FunctionCall) otto.Value { + execution.ReportNoti(call.ArgumentList, options) + return otto.Value{} + }) + vm.Set(DiffNoti, func(call otto.FunctionCall) otto.Value { + execution.DiffNoti(call.ArgumentList, options) + return otto.Value{} + }) + // CustomNoti("message here") + vm.Set(CustomNoti, func(call otto.FunctionCall) otto.Value { + execution.SendAttachment("custom", call.Argument(0).String(), options) + return otto.Value{} + }) + // NotiFile("src") + vm.Set(NotiFile, func(call otto.FunctionCall) otto.Value { + execution.SendFile(call.Argument(0).String(), options.Noti.SlackReportChannel, options) + return otto.Value{} + }) + // using webhook + vm.Set(WebHookNoti, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + messContent := args[0].String() + messType := "custom" + if len(args) > 1 { + messType = args[0].String() + messContent = args[1].String() + } + + execution.WebHookSendAttachment(options, messType, messContent) + return otto.Value{} + }) + + // Telegram functions + + vm.Set(TeleMess, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + messContent := args[0].String() + channelType := "general" + if len(args) > 1 { + channelType = args[0].String() + messContent = args[1].String() + } + execution.TeleSendMess(options, messContent, channelType, false) + return otto.Value{} + }) + // send message but with inside ``` + vm.Set(TeleMessWrap, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + messContent := args[0].String() + channelType := "general" + if len(args) > 1 { + channelType = args[0].String() + messContent = args[1].String() + } + execution.TeleSendMess(options, messContent, channelType, true) + return otto.Value{} + }) + + vm.Set(TeleMessByFile, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + fileName := args[0].String() + channelType := "general" + if len(args) > 1 { + channelType = args[0].String() + fileName = args[1].String() + } + if !utils.FileExists(fileName) { + utils.DebugF("File %s not found", fileName) + return otto.Value{} + } + + messContent := utils.GetFileContent(fileName) + if len(messContent) > 4000 { + execution.TeleSendFile(options, fileName, channelType) + } else { + execution.TeleSendMess(options, messContent, channelType, true) + } + + return otto.Value{} + }) + + vm.Set(TeleSendFile, func(call otto.FunctionCall) otto.Value { + args := call.ArgumentList + messContent := args[0].String() + channelType := "general" + if len(args) > 1 { + channelType = args[0].String() + messContent = args[1].String() + } + execution.TeleSendFile(options, messContent, channelType) + return otto.Value{} + }) + + return output + +} diff --git a/core/step.go b/core/step.go new file mode 100644 index 0000000..97fe556 --- /dev/null +++ b/core/step.go @@ -0,0 +1,167 @@ +package core + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/thoas/go-funk" +) + +func (r *Runner) RunModulesWithTimeout(timeoutRaw string, module libs.Module, options libs.Options) { + timeout := utils.CalcTimeout(timeoutRaw) + utils.InforF("Run module %v with %v seconds timeout", color.HiCyanString(module.Name), timeout) + + c := context.Background() + deadline := time.Now().Add(time.Duration(timeout) * time.Second) + c, cancel := context.WithDeadline(c, deadline) + defer cancel() + + go func() { + r.RunModule(module) + cancel() + }() + + select { + case <-c.Done(): + utils.DebugF("Module done") + return + case <-time.After(time.Duration(timeout) * time.Second): + utils.BadBlockF("timeout", fmt.Sprintf("Module got timeout after %v", color.HiMagentaString(timeoutRaw))) + } + return +} + +// CheckResume check resume report +func CheckResume(module libs.Module) bool { + for _, report := range module.Report.Final { + if !strings.Contains(report, ".osmedeus/storages") && !utils.FileExists(report) { + return false + } + } + return true +} + +// ResolveReports resolve real path of reports +func ResolveReports(module libs.Module, params map[string]string) libs.Module { + var final []string + var noti []string + var diff []string + for _, report := range module.Report.Final { + final = append(final, ResolveData(report, params)) + } + + for _, report := range module.Report.Noti { + noti = append(noti, ResolveData(report, params)) + } + + for _, report := range module.Report.Diff { + diff = append(diff, ResolveData(report, params)) + } + + module.Report.Final = final + module.Report.Noti = noti + module.Report.Diff = diff + return module +} + +// print all report +func printReports(module libs.Module) { + var files []string + files = append(files, module.Report.Final...) + files = append(files, module.Report.Noti...) + files = append(files, module.Report.Diff...) + + reports := funk.UniqString(files) + utils.BlockF("Report", color.HiCyanString("List of reports generated by the %v module", color.HiGreenString(module.Name))) + for _, report := range reports { + if !utils.FileExists(report) && utils.EmptyFile(report, 0) { + if !utils.FolderExists(report) && utils.EmptyDir(report) { + continue + } + } + utils.BlockF("report-file", report) + } +} + +// CheckRequired check if required file exist or not +func (r *Runner) CheckRequired(requires []string) error { + if len(requires) == 0 { + return nil + } + + utils.DebugF("Checking requirements: %v", requires) + for _, require := range requires { + + if strings.Contains(require, "(") && strings.Contains(require, ")") { + validate := r.ConditionExecScript(require) + if !validate { + utils.DebugF("Requirement not met: %v", require) + return fmt.Errorf("condition not met: %s", require) + } + continue + } + + require = utils.NormalizePath(require) + if !utils.FileExists(require) && utils.EmptyFile(require, 0) { + if !utils.FolderExists(require) && utils.DirLength(require) == 0 { + utils.DebugF("Missing %v", require) + return fmt.Errorf("missing requirement") + } + } + } + return nil +} + +// CheckCondition check if required file exist or not +func (r *Runner) CheckCondition(conditions []string) error { + if len(conditions) == 0 { + return nil + } + for _, require := range conditions { + if !r.ConditionExecScript(require) { + return fmt.Errorf("condition not met: %s", require) + } + } + return nil +} + +// RunCommands run list of commands in parallel +func (r *Runner) RunCommands(commands []string, std string) string { + var wg sync.WaitGroup + var output string + var err error + + for _, command := range commands { + wg.Add(1) + // don't run too much command at once + go func(command string) { + defer wg.Done() + var out string + if std != "" { + out, err = utils.RunOSCommand(command) + } else { + err = utils.RunCommandWithoutOutput(command) + } + + if err != nil { + utils.DebugF("error running command: %v -- %v", command, err) + } + + if out != "" { + output += out + } + }(command) + } + wg.Wait() + + if std != "" { + utils.WriteToFile(std, output) + } + return output +} diff --git a/core/tmux.go b/core/tmux.go new file mode 100644 index 0000000..47afe7b --- /dev/null +++ b/core/tmux.go @@ -0,0 +1,85 @@ +package core + +import ( + "fmt" + "strings" + + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +type Tmux struct { + ApplyAll bool + SelectedWindow string + Exclude string + Limit int + Windows []string +} + +func InitTmux(options libs.Options) (Tmux, error) { + cmd := "tmux ls" + var tmux Tmux + tmux.ApplyAll = options.Tmux.ApplyAll + tmux.SelectedWindow = options.Tmux.SelectedWindow + tmux.Exclude = options.Tmux.Exclude + tmux.Limit = options.Tmux.Limit + + raw := utils.RunCmdWithOutput(cmd) + if strings.Contains(raw, "command not found") || !strings.Contains(raw, "\n") { + return tmux, fmt.Errorf("tmux program not installed") + } + + stds := strings.Split(raw, "\n") + for _, line := range stds { + if strings.TrimSpace(line) == "" || !strings.Contains(line, " ") { + continue + } + + data := strings.Split(line, " ") + tmux.Windows = append(tmux.Windows, strings.TrimRight(data[0], ":")) + } + return tmux, nil +} + +func (t *Tmux) ListTmux() { + if len(t.Windows) == 0 { + fmt.Println("No tmux available") + return + } + fmt.Println(strings.Join(t.Windows, ", ")) +} + +func (t *Tmux) CatchSession() string { + var result string + if len(t.Windows) == 0 { + fmt.Println("No tmux available") + return result + } + for _, window := range t.Windows { + utils.DebugF("Get info of: %s", window) + if !t.ApplyAll { + if window != t.SelectedWindow { + continue + } + } + if strings.HasPrefix(window, t.Exclude) { + continue + } + utils.InforF("Get output of %s session", window) + cmd := fmt.Sprintf(`tmux capture-pane -pt "%s"`, window) + raw := utils.RunCmdWithOutput(cmd) + + data := strings.Split(raw, "\n") + if t.Limit == 0 { + fmt.Println(raw) + } else if t.Limit < len(data) && t.Limit > 0 { + fmt.Println(strings.Join(data[len(data)-t.Limit:len(data)-1], "\n")) + } else { + fmt.Println(raw) + } + + result += raw + } + + return result +} diff --git a/core/token.go b/core/token.go new file mode 100644 index 0000000..23213c9 --- /dev/null +++ b/core/token.go @@ -0,0 +1,288 @@ +package core + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/viper" +) + +func SetupOSEnv(options *libs.Options) { + utils.DebugF("Loading all environment variables from: %v", options.TokenConfigFile) + v = viper.New() + v.SetConfigName("osm-var") + v.SetConfigType("yaml") + v.AddConfigPath(path.Dir(options.TokenConfigFile)) + + // Read the configuration file + err := v.ReadInConfig() + if err != nil { + v.SetDefault("Storages", map[string]string{ + // path of secret key for push result + // ~/.osmedeus/secret/storages_key + "secret_key": utils.GetOSEnv("SECRET_KEY", "SECRET_KEY"), + // the repo format should be like this "git@gitlab.com:j3ssie/example.git", + "summary_storage": path.Join(options.Env.RootFolder, "storages/summary"), + "summary_repo": utils.GetOSEnv("SUMMARY_REPO", "SUMMARY_REPO"), + "subdomain_storage": path.Join(options.Env.RootFolder, "storages/subdomain"), + "subdomain_repo": utils.GetOSEnv("SUBDOMAIN_REPO", "SUBDOMAIN_REPO"), + "assets_storage": path.Join(options.Env.RootFolder, "storages/assets"), + "assets_repo": utils.GetOSEnv("ASSETS_REPO", "ASSETS_REPO"), + "ports_storage": path.Join(options.Env.RootFolder, "storages/ports"), + "ports_repo": utils.GetOSEnv("PORTS_REPO", "PORTS_REPO"), + "http_storage": path.Join(options.Env.RootFolder, "storages/http"), + "http_repo": utils.GetOSEnv("HTTP_REPO", "HTTP_REPO"), + "vuln_storage": path.Join(options.Env.RootFolder, "storages/vuln"), + "vuln_repo": utils.GetOSEnv("VULN_REPO", "VULN_REPO"), + "paths_storage": path.Join(options.Env.RootFolder, "storages/paths"), + "paths_repo": utils.GetOSEnv("PATHS_REPO", "PATHS_REPO"), + "mics_storage": path.Join(options.Env.RootFolder, "storages/mics"), + "mics_repo": utils.GetOSEnv("MICS_REPO", "MICS_REPO"), + }) + + // dedicated storages + v.SetDefault("Git", map[string]string{ + "base_url": utils.GetOSEnv("GITLAB_BASE_URL", "https://gitlab.com"), + "api": utils.GetOSEnv("GITLAB_API_TOKEN", "GITLAB_API_TOKEN"), + "username": utils.GetOSEnv("GITLAB_USER", "GITLAB_USER"), + "password": utils.GetOSEnv("GITLAB_PASS", "GITLAB_PASS"), + "group": utils.GetOSEnv("GITLAB_GROUP", "GITLAB_GROUP"), + "prefix_name": utils.GetOSEnv("GITLAB_PREFIX_NAME", "deosm"), + "default_tag": utils.GetOSEnv("GITLAB_DEFAULT_TAG", "osmd"), + "default_user": utils.GetOSEnv("GITLAB_DEFAULT_USER", "j3ssie"), + "default_uid": utils.GetOSEnv("GITLAB_DEFAULT_UID", "3537075"), + "destorage": path.Join(options.Env.RootFolder, "destorage"), + }) + + v.SetDefault("Notification", map[string]string{ + "client_name": utils.GetOSEnv("CLIENT_NAME", "CLIENT_NAME"), + "slack_status_channel": utils.GetOSEnv("SLACK_STATUS_CHANNEL", "SLACK_STATUS_CHANNEL"), + "slack_report_channel": utils.GetOSEnv("SLACK_REPORT_CHANNEL", "SLACK_REPORT_CHANNEL"), + "slack_diff_channel": utils.GetOSEnv("SLACK_DIFF_CHANNEL", "SLACK_DIFF_CHANNEL"), + "slack_webhook": utils.GetOSEnv("SLACK_WEBHOOK", "SLACK_WEBHOOK"), + "telegram_channel": utils.GetOSEnv("TELEGRAM_CHANNEL", "TELEGRAM_CHANNEL"), + "telegram_status_channel": utils.GetOSEnv("TELEGRAM_STATUS_CHANNEL", "TELEGRAM_STATUS_CHANNEL"), + "telegram_report_channel": utils.GetOSEnv("TELEGRAM_REPORT_CHANNEL", "TELEGRAM_REPORT_CHANNEL"), + "telegram_sensitive_channel": utils.GetOSEnv("TELEGRAM_SENSITIVE_CHANNEL", "TELEGRAM_SENSITIVE_CHANNEL"), + "telegram_dirb_channel": utils.GetOSEnv("TELEGRAM_DIRB_CHANNEL", "TELEGRAM_DIRB_CHANNEL"), + "telegram_mics_channel": utils.GetOSEnv("TELEGRAM_MICS_CHANNEL", "TELEGRAM_MICS_CHANNEL"), + }) + + v.SetDefault("Cdn", map[string]string{ + "cdn_s3_bucket": utils.GetOSEnv("CDN_S3_BUCKET", "CDN_S3_BUCKET"), + "cdn_aws_access_key": utils.GetOSEnv("CDN_AWS_ACCESS_KEY", "CDN_AWS_ACCESS_KEY"), + "cdn_aws_secret_key": utils.GetOSEnv("CDN_AWS_SECRET_KEY", "CDN_AWS_SECRET_KEY"), + "cdn_aws_region": utils.GetOSEnv("CDN_AWS_REGION", "ap-southeast-1"), + }) + + // default tokens but you're feel free to add more and it will be automatically loaded to ENV + v.SetDefault("Tokens", map[string]string{ + "slack": utils.GetOSEnv("SLACK_API_TOKEN", "SLACK_API_TOKEN"), + "telegram": utils.GetOSEnv("TELEGRAM_API_TOKEN", "TELEGRAM_API_TOKEN"), + "gitlab": utils.GetOSEnv("GITLAB_API_TOKEN", "GITLAB_API_TOKEN"), + "github": utils.GetOSEnv("GITHUB_API_KEY", "GITHUB_API_KEY"), + }) + + if ok := v.WriteConfigAs(options.TokenConfigFile); ok != nil { + utils.ErrorF("Error writing config file: %s", ok) + } + utils.InforF("Created a new token configuration file at %s", color.HiCyanString(options.ConfigFile)) + + } + + if err := v.ReadInConfig(); err != nil { + utils.ErrorF("Error reading config file, %s", err) + } + // Read tokens as a list of maps + tokens := v.GetStringMapString("tokens") + if len(tokens) > 0 { + utils.DebugF("Adding %v tokens to the environment variables", color.HiMagentaString("%v", len(tokens))) + // Iterate through each token + for name, value := range tokens { + // automatic convert to upper case + name = strings.ToUpper(name) + + // skip if the value is equal to the name + if name == value { + continue + } + + utils.DebugF("Setting environment variable: %v -- %v", name, value) + + err := os.Setenv(name, value) + if err != nil { + utils.ErrorF("Error setting environment variable: %v -- %v", name, err) + } + } + } + // get all the config that need to be set manually + GetStorages(options) + GetStorages(options) + GetNotification(options) + GetGit(options) + GetCdn(options) +} + +func GetStorages(options *libs.Options) { + if !options.PremiumPackage || utils.GetOSEnv("ENABLE_GIT_STORAGES", "") != "TRUE" { + return + } + + storages := v.GetStringMapString("Storages") + + // get variables from config.yaml + storagesOptions := make(map[string]string) + for k, dest := range storages { + storages[k] = utils.NormalizePath(dest) + storagesOptions[k] = utils.NormalizePath(dest) + } + secretKey := storages["secret_key"] + if secretKey == "" || secretKey == "SECRET_KEY" { + return + } + + storagesOptions["secret_key"] = secretKey + // load default existing key if it exists + defaultKey := path.Join(options.Env.BaseFolder, "secret/storages_key") + if !utils.FileExists(secretKey) && utils.FileExists(defaultKey) { + utils.InforF("Loaded default secret for storages from: %v", color.HiCyanString(defaultKey)) + if _, err := utils.RunCommandWithErr(fmt.Sprintf("cp %s %s && chmod 600 %s", defaultKey, secretKey, secretKey)); err != nil { + utils.ErrorF("error copying default secret key: %v", defaultKey) + } + } + + if !utils.FileExists(secretKey) { + utils.InforF("No SSH key for storages found. Generate a new one at: %v", color.HiCyanString(secretKey)) + if _, err := utils.RunCommandWithErr(fmt.Sprintf(`ssh-keygen -t ed25519 -f %s -q -N ''`, secretKey)); err != nil { + color.Red("[-] error generated SSH Key for storages at: %v", secretKey) + return + } + utils.InforF("Please add the public key at %v to your gitlab profile", color.HiCyanString(secretKey+".pub")) + } + + if !utils.FileExists("~/.gitconfig") { + utils.WarnF("Looks like you didn't set up the git user at %v yet", color.HiCyanString("~/.gitconfig")) + utils.WarnF("šŸ’” Init git info with this command: %s", color.HiCyanString(`git config --global user.name "your-username" && git config --global user.email "your-username@users.noreply.gitlab.com"`)) + } + + if options.CustomGit { + // in case custom repo is set + for _, env := range os.Environ() { + // the ENV should be OSM_SUMMARY_STORAGE, OSM_SUMMARY_REPO + if strings.HasSuffix(env, "OSM_") { + data := strings.Split(env, "=") + key := strings.ToLower(data[0]) + value := strings.Replace(env, data[0]+"=", "", -1) + + if strings.HasSuffix(key, "summary_storage") { + storagesOptions["summary_storage"] = value + } + if strings.HasSuffix(key, "summary_repo") { + storagesOptions["summary_repo"] = value + } + + if strings.HasSuffix(key, "assets_storage") { + storagesOptions["assets_storage"] = value + } + if strings.HasSuffix(key, "assets_repo") { + storagesOptions["assets_repo"] = value + } + + if strings.HasSuffix(key, "ports_storage") { + storagesOptions["ports_storage"] = value + } + if strings.HasSuffix(key, "ports_repo") { + storagesOptions["ports_repo"] = value + } + } + } + } + + storagesOptions[storages["summary_storage"]] = storages["summary_repo"] + storagesOptions[storages["subdomain_storage"]] = storages["subdomain_repo"] + storagesOptions[storages["http_storage"]] = storages["http_repo"] + storagesOptions[storages["assets_storage"]] = storages["assets_repo"] + storagesOptions[storages["mics_storage"]] = storages["mics_repo"] + storagesOptions[storages["ports_storage"]] = storages["ports_repo"] + storagesOptions[storages["paths_storage"]] = storages["paths_repo"] + storagesOptions[storages["vuln_storage"]] = storages["vuln_repo"] + options.Storages = storagesOptions + + // disable git feature or no secret key found + if options.NoGit { + options.Storages["secret_key"] = "" + } + if options.Storages["secret_key"] == "" { + options.NoGit = true + } + + if options.NoGit { + return + } + execution.CloneRepo(storages["summary_repo"], storages["summary_storage"], *options) + execution.CloneRepo(storages["http_repo"], storages["http_storage"], *options) + execution.CloneRepo(storages["assets_repo"], storages["assets_storage"], *options) + execution.CloneRepo(storages["subdomain_repo"], storages["subdomain_storage"], *options) + execution.CloneRepo(storages["ports_repo"], storages["ports_storage"], *options) + execution.CloneRepo(storages["mics_repo"], storages["mics_storage"], *options) + execution.CloneRepo(storages["paths_repo"], storages["paths_storage"], *options) + execution.CloneRepo(storages["vuln_repo"], storages["vuln_storage"], *options) +} + +// GetNotification get storge repos +func GetNotification(options *libs.Options) { + noti := v.GetStringMapString("Notification") + tokens := v.GetStringMapString("Tokens") + + // tokens + options.Noti.SlackToken = tokens["slack"] + options.Noti.TelegramToken = tokens["telegram"] + + // this mean you're not setup the notification yet + if len(options.Noti.TelegramToken) < 20 { + options.NoNoti = true + } + + options.Noti.ClientName = noti["client_name"] + + options.Noti.SlackStatusChannel = noti["slack_status_channel"] + options.Noti.SlackReportChannel = noti["slack_report_channel"] + options.Noti.SlackDiffChannel = noti["slack_diff_channel"] + options.Noti.SlackWebHook = noti["slack_webhook"] + options.Noti.TelegramChannel = noti["telegram_channel"] + options.Noti.TelegramSensitiveChannel = noti["telegram_sensitive_channel"] + options.Noti.TelegramReportChannel = noti["telegram_report_channel"] + options.Noti.TelegramStatusChannel = noti["telegram_status_channel"] + options.Noti.TelegramDirbChannel = noti["telegram_dirb_channel"] + options.Noti.TelegramMicsChannel = noti["telegram_mics_channel"] +} + +// GetCdn get options for client +func GetCdn(options *libs.Options) { + cdn := v.GetStringMapString("Cdn") + options.Cdn.Bucket = cdn["cdn_s3_bucket"] + options.Cdn.AccessKeyId = cdn["cdn_aws_access_key"] + options.Cdn.SecretKey = cdn["cdn_aws_secret_key"] + options.Cdn.Region = cdn["cdn_aws_region"] +} + +// GetGit get options for client +func GetGit(options *libs.Options) { + git := v.GetStringMapString("Git") + options.Git.BaseURL = git["base_url"] + options.Git.DeStorage = git["destorage"] + options.Git.Token = git["api"] + options.Git.Username = git["username"] + options.Git.Password = git["password"] + options.Git.Group = git["group"] + options.Git.DefaultPrefix = git["prefix_name"] + options.Git.DefaultTag = git["default_tag"] + options.Git.DefaultUser = git["default_user"] + options.Git.DefaultUID = utils.StrToInt(git["default_uid"]) +} diff --git a/core/update.go b/core/update.go new file mode 100644 index 0000000..6854438 --- /dev/null +++ b/core/update.go @@ -0,0 +1,336 @@ +package core + +import ( + "fmt" + "os" + "os/exec" + "path" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/fatih/color" + "github.com/hashicorp/go-version" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/provider" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" + "github.com/mitchellh/go-homedir" +) + +/* Mostly calling OS commands for double-check the PATH too */ + +func CheckUpdate(options *libs.Options) bool { + // t.Format("02-Jan-2006") + + var shouldUpdate bool + // ~/.osmedeus/update/metadata.json + options.Update.UpdateConfig = path.Join(utils.NormalizePath(options.Env.RootFolder), "update") + utils.MakeDir(options.Update.UpdateConfig) + + if options.Update.MetaDataURL == "" { + options.Update.MetaDataURL = fmt.Sprintf("%s/public.json", libs.METADATA) + if options.PremiumPackage { + options.Update.MetaDataURL = fmt.Sprintf("%s/premium.json", libs.METADATA) + } + } + + utils.InforF("Checking metadata information from: %v", options.Update.MetaDataURL) + metadataFile := path.Join(options.Update.UpdateConfig, "metadata.json") + + var oldMetaData libs.UpdateMetaData + oldMetaData.CoreVersion = libs.VERSION + oldMetaData.WorkflowVersion = "v0.0.1" + + if utils.FileExists(metadataFile) { + oldMetaDataContent := utils.GetFileContent(metadataFile) + if err := jsoniter.UnmarshalFromString(oldMetaDataContent, &oldMetaData); err != nil { + utils.ErrorF("error to parse metadata: %v", metadataFile) + return false + } + } + + var newMetaData libs.UpdateMetaData + res := utils.SendGET("", options.Update.MetaDataURL) + if res.StatusCode == 200 { + if err := jsoniter.UnmarshalFromString(res.Body, &newMetaData); err != nil { + utils.ErrorF("error to parse metadata: %v", options.Update.MetaDataURL) + return false + } + + utils.InforF("Writing metadata to: %v", color.HiCyanString(metadataFile)) + if data, err := jsoniter.MarshalToString(&newMetaData); err == nil { + utils.WriteToFile(metadataFile, data) + } + } else { + utils.ErrorF("error fetching metadata from: %v", options.Update.MetaDataURL) + return false + } + + utils.DebugF(res.Body) + + v1, err := version.NewVersion(oldMetaData.CoreVersion) + if err != nil { + utils.ErrorF("error parsing version: %v -- %v", oldMetaData.CoreVersion, err) + return false + } + + // get from metadata URL + v2, err := version.NewVersion(newMetaData.CoreVersion) + if err != nil { + utils.ErrorF("error parsing version: %v -- %v", newMetaData.CoreVersion, err) + + return false + } + + // Comparison example. There is also GreaterThan, Equal, and just + // a simple Compare that returns an int allowing easy >=, <=, etc. + if v1.LessThan(v2) { + fmt.Printf("Your current %s %s are outdated. Latest is %s\n", libs.BINARY, color.HiMagentaString("%v", v1), color.HiGreenString("%v", v2)) + shouldUpdate = true + } else { + fmt.Printf("You're using %s core latest version %s updated at %s\n", libs.BINARY, color.HiMagentaString("%v", v1), color.HiGreenString("%v", newMetaData.UpdatedAt)) + } + + // check workflow version if core is updated + if !shouldUpdate { + wfv1, err := version.NewVersion(oldMetaData.WorkflowVersion) + if err != nil { + utils.ErrorF("error parsing version: %v -- %v", oldMetaData.WorkflowVersion, err) + } + wfv2, err := version.NewVersion(newMetaData.WorkflowVersion) + if err != nil { + utils.ErrorF("error parsing version: %v -- %v", newMetaData.WorkflowVersion, err) + } + if wfv1.LessThan(wfv2) { + fmt.Printf("Your current %s workflow %s are outdated. Latest is %s\n", libs.BINARY, color.HiMagentaString("%v", v1), color.HiGreenString("%v", v2)) + shouldUpdate = true + } + } + + if shouldUpdate { + home, _ := homedir.Dir() + fmt.Printf("šŸ“– Run %s again to update Check out this page for more detail: %s\n", color.HiGreenString("the same install script"), color.HiGreenString("https://docs.osmedeus.org/installation/")) + fmt.Printf("šŸ’” If you want a fresh install please run the command: %s\n", color.HiBlueString("rm -rf %s/osmedeus-base %s/.osmedeus", home, home)) + } + + return shouldUpdate +} + +func GetUpdateURL(options libs.Options) string { + if !options.PremiumPackage { + utils.InforF("Getting update url of public community package") + return libs.INSTALL + } + utils.InforF("šŸ’Ž Getting update url of premium package") + + providerConfigs, err := provider.ParseProvider(options.CloudConfigFile) + if err != nil { + utils.ErrorF("error to parse provider config: %v", err) + return "" + } + + return providerConfigs.Builder.BuildRepo +} + +func RunUpdate(options libs.Options) error { + if options.Update.UpdateURL == "" { + return fmt.Errorf("no update URL") + } + + if options.Update.CleanOldData { + utils.InforF("Cleaning old data: %s", color.HiCyanString(options.Env.BaseFolder)) + os.RemoveAll(options.Env.BaseFolder) + } + + if options.PremiumPackage { + utils.InforF("Running update from premium package install script") + } else { + utils.InforF("Running update from: %v", color.HiCyanString(options.Update.UpdateURL)) + } + + options.Update.UpdateScript = fmt.Sprintf("/tmp/%s-update.sh", libs.BINARY) + cmd := fmt.Sprintf("rm -rf %s && wget -qO %s %s ", options.Update.UpdateScript, options.Update.UpdateScript, options.Update.UpdateURL) + _, err := utils.RunCommandWithErr(cmd) + if err != nil { + utils.ErrorF("error to run update script: %v", err) + return err + } + + if !utils.FileExists(options.Update.UpdateScript) { + utils.ErrorF("update script doesn't exist: %v", options.Update.UpdateScript) + return fmt.Errorf("update script doesn't exist: %v", options.Update.UpdateScript) + } + + if _, err := utils.RunCommandSteamOutput(fmt.Sprintf(`bash %s`, options.Update.UpdateScript)); err != nil { + utils.ErrorF("error to run update script: %v", err) + return err + } + return nil +} + +func GenerateMetaData(options libs.Options) { + utils.InforF("Generating metadata to: %v", color.HiCyanString(options.Update.GenerateMeta)) + t := time.Now() + t.Format("2006-01-02T15:04:05") + var updateData = libs.UpdateMetaData{ + CoreVersion: libs.VERSION, + WorkflowVersion: libs.VERSION, + UpdatedAt: t.Format("2006-01-02T15:04"), + } + + if data, ok := jsoniter.MarshalToString(updateData); ok == nil { + utils.InforF("Generate meta data: %v", data) + utils.WriteToFile(options.Update.GenerateMeta, data) + } +} + +func GitUpdate(opt libs.Options) error { + cmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git clone --depth=1 %v %v", opt.Storages["secret_key"], opt.Update.UpdateURL, opt.Update.UpdateFolder) + _, err := utils.RunCommandWithErr(cmd) + return err +} + +func HTTPUpdate(opt libs.Options) error { + cmd := fmt.Sprintf("wget -q %s -O %s", opt.Update.UpdateURL, opt.Update.UpdateFolder) + _, err := utils.RunCommandWithErr(cmd) + return err +} + +func DownloadUpdate(opt libs.Options) error { + os.RemoveAll(opt.Update.UpdateFolder) + utils.InforF("Downloading the update folder via %v: %v", opt.Update.UpdateType, opt.Update.UpdateURL) + var err error + + backOff := backoff.NewExponentialBackOff() + backOff.MaxElapsedTime = 1200 * time.Second + backOff.Multiplier = 2.0 + backOff.InitialInterval = 30 * time.Second + + operation := func() error { + switch strings.ToLower(opt.Update.UpdateType) { + case "git": + err = GitUpdate(opt) + case "s3", "http": + err = HTTPUpdate(opt) + default: + err = GitUpdate(opt) + } + if err != nil { + utils.ErrorF("error downloading update content: %s -- %s", opt.Update.UpdateType, opt.Update.UpdateURL) + } + return err + } + err = backoff.Retry(operation, backOff) + if err != nil { + utils.ErrorF("error downloading update content: %s -- %s", opt.Update.UpdateType, opt.Update.UpdateURL) + return err + } + return nil +} + +func Update(opt libs.Options) { + os.RemoveAll(opt.Update.UpdateFolder) + utils.MakeDir(opt.Update.UpdateFolder) + + updateScript := fmt.Sprintf("%s/update.sh", opt.Update.UpdateFolder) + cmd := fmt.Sprintf("wget -q %s -O %s/install.sh", opt.Update.UpdateURL, updateScript) + if _, err := utils.RunCommandWithErr(cmd); err != nil { + utils.ErrorF("error downloading the update script: %v", opt.Update.UpdateURL) + return + } + + cmd = fmt.Sprintf("bash %s", updateScript) + if _, err := utils.RunCommandWithErr(cmd); err != nil { + utils.ErrorF("error running the update script: %v", updateScript) + return + } +} + +func UpdateVuln(opt libs.Options) { + utils.InforF("Updating Vulnerability Database only") + + // update nuclei templates + utils.DebugF("Updating Nuclei Templates") + nucleiTemplate := utils.NormalizePath("~/nuclei-templates") + + if utils.DirLength(nucleiTemplate) > 0 { + os.RemoveAll(nucleiTemplate) + } + utils.RunOSCommand(fmt.Sprintf("git clone --depth=1 https://github.com/projectdiscovery/nuclei-templates.git %s", nucleiTemplate)) +} + +func UpdateBase(opt libs.Options) { + err := DownloadUpdate(opt) + if err != nil { + return + } + + // change the folder since we will update it + if opt.Update.IsUpdateBin { + utils.InforF("Updating External binaries") + binPath := path.Join(opt.Update.UpdateFolder, "binaries") + utils.Move(binPath, opt.Env.BinariesFolder) + opt.Update.UpdateFolder = path.Join(opt.Update.UpdateFolder, fmt.Sprintf("%s-base", libs.BINARY)) + } + + // update Env + utils.InforF("Updating Environments Data") + utils.Move(path.Join(opt.Update.UpdateFolder, "data"), opt.Env.DataFolder) + utils.Move(path.Join(opt.Update.UpdateFolder, "workflow"), opt.Env.WorkFlowsFolder) + utils.Move(path.Join(opt.Update.UpdateFolder, "ose"), opt.Env.OseFolder) + utils.Move(path.Join(opt.Update.UpdateFolder, "ui"), opt.Env.UIFolder) + + utils.Move(path.Join(opt.Update.UpdateFolder, "clouds"), opt.Env.CloudConfigFolder) + os.Chmod(opt.Cloud.SecretKey, 0600) + + // update osmedeus core binary + corePath, err := exec.LookPath(libs.BINARY) + utils.InforF("Updating %v binary at %v", color.HiCyanString(libs.BINARY), color.HiCyanString(corePath)) + if err == nil { + os.RemoveAll(corePath) + newBin := fmt.Sprintf("%s/dist/%s", strings.TrimRight(opt.Update.UpdateFolder, "/"), libs.BINARY) + unZipCmd := fmt.Sprintf("unzip %s/dist/%s-linux.zip -d %s/dist/", strings.TrimRight(opt.Update.UpdateFolder, "/"), libs.BINARY, strings.TrimRight(opt.Update.UpdateFolder, "/")) + utils.RunOSCommand(unZipCmd) + + // update binaries in gopath + goPath := utils.GetOSEnv("GOPATH", "GOPATH") + if goPath != "GOPATH" { + goPath = path.Join(goPath, fmt.Sprintf("bin/%s", libs.BINARY)) + os.RemoveAll(goPath) + utils.RunOSCommand(fmt.Sprintf("cp %s %s", newBin, goPath)) + + // go path but in plugins folder + goPath = path.Join(opt.Env.BinariesFolder, "go", libs.BINARY) + os.RemoveAll(goPath) + utils.RunOSCommand(fmt.Sprintf("cp %s %s", newBin, goPath)) + } + utils.Move(newBin, corePath) + } + + // update vulnerability signatures + utils.InforF("Updating Jaeles Signatures") + jaelesSign := path.Join(opt.Update.UpdateFolder, "pro-signatures") + if utils.DirLength(jaelesSign) > 0 { + utils.RunOSCommand(fmt.Sprintf("jaeles config reload --signDir %s", jaelesSign)) + utils.Move(jaelesSign, "~/pro-signatures") + } else { + os.RemoveAll(utils.NormalizePath("~/pro-signatures")) + utils.RunOSCommand(fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %s' git clone --depth=1 git@gitlab.com:j3ssie/pro-signatures ~/pro-signatures", opt.Storages["secret_key"])) + utils.RunOSCommand(fmt.Sprintf("rm -rf ~/custom-nuclei-template && GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %s' git clone --depth=1 git@gitlab.com:j3ssie/custom-nuclei-template.git ~/custom-nuclei-template", opt.Storages["secret_key"])) + utils.RunOSCommand("jaeles config reload --signDir ~/pro-signatures") + } + + // update nuclei templates + utils.InforF("Updating Nuclei Templates") + nucleiTemplate := path.Join(opt.Update.UpdateFolder, "nuclei-templates") + if utils.DirLength(nucleiTemplate) > 0 { + utils.Move(nucleiTemplate, utils.NormalizePath("~/nuclei-templates")) + } else { + utils.RunOSCommand(fmt.Sprintf("git clone --depth=1 https://github.com/projectdiscovery/nuclei-templates.git ~/nuclei-templates")) + } + + // clean up + utils.InforF("Clean up update folder") + os.RemoveAll(opt.Update.UpdateFolder) +} diff --git a/core/validate.go b/core/validate.go new file mode 100644 index 0000000..e16e801 --- /dev/null +++ b/core/validate.go @@ -0,0 +1,144 @@ +package core + +import ( + "fmt" + "path" + "strings" + + "github.com/fatih/color" + "github.com/go-playground/validator/v10" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +func (r *Runner) Validator() error { + if r.RequiredInput == "" || r.Opt.DisableValidateInput { + return nil + } + + r.RequiredInput = strings.ToLower(strings.TrimSpace(r.RequiredInput)) + var inputAsFile bool + // cidr, cidr-file + if strings.HasSuffix(r.RequiredInput, "-file") || r.RequiredInput == "file" { + inputAsFile = true + } + v := validator.New() + + // if input as a file + if utils.FileExists(r.Input) && inputAsFile { + r.InputType = "file" + inputs := utils.ReadingLines(r.Input) + + for index, input := range inputs { + if strings.TrimSpace(input) == "" { + continue + } + // no more validation if it's file 'validator: file' + if r.RequiredInput == "file" { + continue + } + + inputType, err := validate(v, input) + // fmt.Println("r.RequiredInput, inputType", r.RequiredInput, inputType) + if err == nil { + // really validate thing + if !strings.HasPrefix(r.RequiredInput, inputType) { + utils.DebugF("validate: %v -- %v", input, inputType) + errString := fmt.Sprintf("line %v in %v file not match the require input: %v -- %v", index, r.Input, input, inputType) + utils.ErrorF(errString) + return fmt.Errorf(errString) + } + } + } + return nil + + } + + var err error + r.InputType, err = validate(v, r.Input) + if err != nil { + utils.ErrorF("unrecognized input: %v", r.Input) + return err + } + utils.InforF("Start validating input: %v -- %v", color.HiCyanString(r.Input), color.HiCyanString(r.InputType)) + + if !strings.HasPrefix(r.RequiredInput, r.InputType) { + return fmt.Errorf("input does not match the require validation: inputType:%v -- requireType:%v", r.InputType, r.RequiredInput) + } + + if inputAsFile { + utils.MakeDir(libs.TEMP) + suffix := utils.RandomString(4) + if r.Opt.Scan.SuffixName != "" { + suffix = r.Opt.Scan.SuffixName + "-" + utils.RandomString(4) + } + dest := path.Join(libs.TEMP, fmt.Sprintf("%v-%v", utils.StripPath(r.Input), suffix)) + if r.Opt.Scan.CustomWorkspace != "" { + dest = path.Join(libs.TEMP, fmt.Sprintf("%v-%v", utils.StripPath(r.Opt.Scan.CustomWorkspace), suffix)) + } + utils.WriteToFile(dest, r.Input) + utils.InforF("Convert input to a file: %v", dest) + r.Input = dest + r.Target = ParseInput(r.Input, r.Opt) + } + + utils.DebugF("validator: input:%v -- type: %v -- require:%v", r.Input, r.InputType, r.RequiredInput) + return nil +} + +func validate(v *validator.Validate, raw string) (string, error) { + var err error + var inputType string + + if utils.FileExists(raw) { + inputType = "file" + } + + err = v.Var(raw, "required,url") + if err == nil { + inputType = "url" + } + + err = v.Var(raw, "required,ipv4") + if err == nil { + inputType = "ip" + } + + err = v.Var(raw, "required,fqdn") + if err == nil { + inputType = "domain" + } + + err = v.Var(raw, "required,hostname") + if err == nil { + inputType = "domain" + } + + err = v.Var(raw, "required,cidr") + if err == nil { + inputType = "cidr" + } + + err = v.Var(raw, "required,uri") + if err == nil { + inputType = "url" + } + + err = v.Var(raw, "required,uri") + if err == nil { + inputType = "url" + if strings.HasPrefix(raw, "https://github.com") || strings.HasPrefix(raw, "https://gitlab.com") { + inputType = "git-url" + } + } + + if strings.HasPrefix(raw, "git@") { + inputType = "git-url" + } + + if inputType == "" { + return "", fmt.Errorf("unrecognized input") + } + + return inputType, nil +} diff --git a/core/validate_test.go b/core/validate_test.go new file mode 100644 index 0000000..b11364d --- /dev/null +++ b/core/validate_test.go @@ -0,0 +1,53 @@ +package core + +import ( + "fmt" + "github.com/j3ssie/osmedeus/libs" + "testing" +) + +func TestValidator(t *testing.T) { + input := "target.com" + opt := libs.Options{} + runner, _ := InitRunner(input, opt) + runner.RequiredInput = "domain" + runner.Validator() + + runner.Input = "apple.com" + runner.Validator() + + fmt.Printf("runner.InputType --> %v:%v -- %s\n", runner.RequiredInput, runner.InputType, runner.Input) + + runner.Input = "1.2.3.4" + runner.Validator() + + runner.Input = "http://127.0.0.1/q" + runner.Validator() + runner.Input = "sub.domain.com" + runner.Validator() + + runner.Input = "1.2.3.4/24" + runner.Validator() + + runner.Input = "https://github.com/j3ssie/osmedeus" + runner.Validator() + fmt.Printf("==> runner.InputType --> %v:%v -- %s\n\n", runner.RequiredInput, runner.InputType, runner.Input) + + runner.Input = "git@github.com:j3ssie/osmedeus.git" + runner.Validator() + fmt.Printf("==> runner.InputType --> %v:%v -- %s\n\n", runner.RequiredInput, runner.InputType, runner.Input) + + // + ////raw := "tcp://git@gitlab.com:j3ssie/osmd-assets" + //raw := "git@gitlab.com/j3ssie/osmd-assets" + //v := validator.New() + //err := v.Var(raw, "required,uri") + //fmt.Println(err) + // + //err = v.Var(raw, "required,datauri") + //fmt.Println(err) + + if runner.InputType == "" { + t.Errorf("Error Validator") + } +} diff --git a/database/connect.go b/database/connect.go new file mode 100644 index 0000000..2697d4f --- /dev/null +++ b/database/connect.go @@ -0,0 +1,63 @@ +package database + +// load driver + +// // DB gorm connector +// var DB *gorm.DB + +// // InitDB connect to db +// func InitDB(options libs.Options) (*gorm.DB, error) { +// newLogger := logger.New( +// log.New(ioutil.Discard, "\r\n", log.LstdFlags), // io writer +// logger.Config{ +// SlowThreshold: time.Second, // Slow SQL threshold +// LogLevel: logger.Info, // Log level +// IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger +// Colorful: false, // Disable color +// }, +// ) + +// config := gorm.Config{ +// SkipDefaultTransaction: false, +// NamingStrategy: nil, +// FullSaveAssociations: false, +// Logger: newLogger, +// NowFunc: nil, +// DryRun: false, +// PrepareStmt: false, +// DisableAutomaticPing: false, +// DisableForeignKeyConstraintWhenMigrating: false, +// DisableNestedTransaction: false, +// AllowGlobalUpdate: false, +// QueryFields: false, +// CreateBatchSize: 0, +// ClauseBuilders: nil, +// ConnPool: nil, +// Dialector: nil, +// Plugins: nil, +// } + +// var err error +// if options.Server.DBType == "mysql" { +// utils.InforF("Connect Database at %v:%v", options.Server.DBHost, options.Server.DBPort) +// utils.InforF("Use Database: %v", options.Server.DBName) +// DB, err = gorm.Open(mysql.Open(options.Server.DBConnection), &config) +// } else { +// DB, err = gorm.Open(sqlite.Open(options.Server.DBPath), &config) +// } + +// if err != nil { +// fmt.Printf("Error Database connect at %v:%v -- %v\n", options.Server.DBHost, options.Server.DBPort, err) +// return nil, err +// } + +// // scanning data +// // DB.AutoMigrate(&User{}) +// //DB.AutoMigrate(&Org{}) +// DB.AutoMigrate(&Target{}) +// DB.AutoMigrate(&Scan{}) +// DB.AutoMigrate(&Report{}) +// DB.AutoMigrate(&CloudInstance{}) +// // asset data +// return DB, nil +// } diff --git a/database/models.go b/database/models.go new file mode 100644 index 0000000..9a892fe --- /dev/null +++ b/database/models.go @@ -0,0 +1,99 @@ +package database + +import ( + "time" +) + +type Model struct { + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + +// Schedule store task to do every single time +type Schedule struct { + Model + TaskName string `gorm:"type:varchar(255)" json:"task_name"` + RefreshSeconds string `gorm:"type:varchar(255)" json:"refresh_seconds"` + Command string `gorm:"type:longtext" json:"command"` + Status string `gorm:"type:varchar(255)" json:"status"` +} + +/////////// + +type Scan struct { + Model + + // input part + InputName string `gorm:"type:varchar(255);not null" json:"input_name"` + InputType string `gorm:"type:varchar(255);default:'general'" json:"input_type"` + + TaskName string `gorm:"type:varchar(255)" json:"task_name"` + TaskPath string `gorm:"type:varchar(255)" json:"task_path"` + TaskType string `gorm:"type:varchar(255);default:'flow'" json:"task_type"` + + MarkDownSunmmary string `gorm:"type:varchar(255)" json:"markdown_summary"` + MarkDownReport string `gorm:"type:varchar(255)" json:"markdown_report"` + + RunningTime int `json:"running_time"` // as seconds + CurrentModule string `gorm:"type:varchar(255)" json:"current_module"` + DoneStep int `json:"done_step"` + TotalSteps int `json:"total_steps"` + + // mics part + LogFile string `json:"log_file"` + ProcessID int `json:"process_id"` + + // progress checking + IsRunning bool `json:"is_running"` + IsDone bool `json:"is_done"` + IsNew bool `json:"is_new"` + IsError bool `json:"is_error"` + IsStarted bool `json:"is_started"` + + // if the task is running by cloud provider + IsPrepared bool `json:"is_prepared"` + IsCloud bool `json:"is_cloud"` + CloudInfo string `json:"cloud_info"` + + Target Target `json:"target"` +} + +// runtime object +type Target struct { + InputName string `gorm:"type:varchar(255);unique;not null" json:"input_name"` + // @NOTE: below field shouldn't be show in UI + // Workspace == InputName but strip out '/' + Workspace string `gorm:"type:varchar(255);unique;not null" json:"workspace"` + InputType string `gorm:"type:varchar(255);default:'N/A'" json:"input_type"` + + // total number for stat + TotalAssets int `json:"total_assets"` + TotalDns int `json:"total_dns"` + TotalTech int `json:"total_tech"` + TotalScreenShot int `json:"total_screenshot"` + TotalVulnerability int `json:"total_vulnerability"` + TotalDirb int `json:"total_dirb"` + TotalLink int `json:"total_link"` + TotalArchive int `json:"total_archive"` + TotalIPRange int `json:"total_ip_range"` + TotalCloud int `json:"total_cloud"` + TotalCred int `json:"total_cred"` + + // flag information + IsNew bool `json:"is_new"` + IsWildCard bool `json:"is_wildcard"` + + Reports []Report `json:"reports"` +} + +// Report store reports file record +type Report struct { + ReportName string `gorm:"type:varchar(255)" json:"report_name"` + ReportPath string `gorm:"type:longtext" json:"report_path"` + + Module string `gorm:"type:varchar(255)" json:"module"` + ModulePath string `gorm:"type:longtext" json:"module_path"` + + WorkspaceName string `gorm:"type:varchar(255)" json:"workspace_name"` + ReportType string `gorm:"type:varchar(255);default:'text'" json:"report_type"` +} diff --git a/database/select.go b/database/select.go new file mode 100644 index 0000000..c462eb6 --- /dev/null +++ b/database/select.go @@ -0,0 +1,100 @@ +package database + +import ( + "os" + "path" + "path/filepath" + "strings" + + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" +) + +func GetAllWorkspaces(opt libs.Options) (directories []string) { + // Open the specified directory + dir, err := os.Open(opt.Env.WorkspacesFolder) + if err != nil { + return directories + } + defer dir.Close() + + // Read all entries in the directory + entries, err := dir.Readdir(-1) + if err != nil { + return directories + } + + for _, entry := range entries { + wsName := entry.Name() + directories = append(directories, wsName) + } + + return directories +} + +func GetAllScan(opt libs.Options) (scans []Scan) { + wss := GetAllWorkspaces(opt) + for _, wsName := range wss { + runtimeFile := filepath.Join(opt.Env.WorkspacesFolder, wsName, "runtime") + if !utils.FileExists(runtimeFile) { + continue + } + + // parse the content + runtimeContent := utils.GetFileContent(runtimeFile) + wsData := Scan{} + if err := jsoniter.UnmarshalFromString(runtimeContent, &wsData); err == nil { + + // replace the filepath with static prefix + wsData.MarkDownReport = strings.ReplaceAll(wsData.MarkDownReport, opt.Env.WorkspacesFolder, path.Join("/", opt.Server.StaticPrefix, "workspaces")) + wsData.MarkDownSunmmary = strings.ReplaceAll(wsData.MarkDownSunmmary, opt.Env.WorkspacesFolder, path.Join("/", opt.Server.StaticPrefix, "workspaces")) + scans = append(scans, wsData) + } + + } + return scans +} + +func GetSingleScan(wsName string, opt libs.Options) (scan Scan) { + runtimeFile := filepath.Join(opt.Env.WorkspacesFolder, wsName, "runtime") + if !utils.FileExists(runtimeFile) { + return scan + } + + // parse the content + runtimeContent := utils.GetFileContent(runtimeFile) + if err := jsoniter.UnmarshalFromString(runtimeContent, &scan); err == nil { + // replace the filepath with static prefix + scan.MarkDownReport = strings.ReplaceAll(scan.MarkDownReport, opt.Env.WorkspacesFolder, path.Join("/", opt.Server.StaticPrefix, "workspaces")) + scan.MarkDownSunmmary = strings.ReplaceAll(scan.MarkDownSunmmary, opt.Env.WorkspacesFolder, path.Join("/", opt.Server.StaticPrefix, "workspaces")) + return scan + } + return scan +} + +func GetScanProgress(opt libs.Options) (scans []Scan) { + rawScans := GetAllScan(opt) + + for _, scan := range rawScans { + scan.Target = Target{} + scans = append(scans, scan) + } + + return scans +} + +// func GetWorkspaceDetail(wsName string, opt libs.Options) (workspace Scan) { +// runtimeFile := filepath.Join(opt.Env.WorkspacesFolder, wsName, "runtime") +// if !utils.FileExists(runtimeFile) { +// return workspace +// } + +// // parse the content +// runtimeContent := utils.GetFileContent(runtimeFile) +// if err := jsoniter.UnmarshalFromString(runtimeContent, &workspace); err == nil { +// return workspace +// } + +// return workspace +// } diff --git a/distribute/clean.go b/distribute/clean.go new file mode 100644 index 0000000..0bfaa3f --- /dev/null +++ b/distribute/clean.go @@ -0,0 +1,54 @@ +package distribute + +import ( + "os" + "sync" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/jinzhu/copier" + "github.com/panjf2000/ants" +) + +func ClearAllInstances(opt libs.Options) { + if opt.Cloud.Retry == 0 { + opt.Cloud.Retry = 8 + } + + // select all cloud instance + instances := GetAllInstances(opt) + if len(instances) == 0 { + utils.WarnF("no active cloud instance running") + return + } + + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(opt.Concurrency*5, func(i interface{}) { + instance := i.(CloudRunner) + + var options libs.Options + copier.Copy(&options, &opt) + instance.Opt = opt + instance.Provider.IsBackgroundCheck = true + instance.Provider.InitClient() + instance.Prepare() + utils.InforF("Deleting the instance: %v -- %v", instance.Provider.ProviderName, instance.PublicIP) + if err := instance.Provider.DeleteInstance(instance.InstanceID); err == nil { + utils.InforF("Instance deleted %s -- %s", color.HiYellowString(instance.PublicIP), color.HiYellowString(instance.InstanceID)) + instanceFile := instance.Opt.Env.InstancesFolder + "/" + instance.InstanceName + "-" + instance.PublicIP + ".json" + os.Remove(instanceFile) + } + + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + utils.InforF("Evaluating the status of %v cloud instances", color.HiMagentaString("%v", len(instances))) + for _, instance := range instances { + wg.Add(1) + _ = p.Invoke(instance) + } + wg.Wait() + +} diff --git a/distribute/cloud_runner.go b/distribute/cloud_runner.go new file mode 100644 index 0000000..da2ebcc --- /dev/null +++ b/distribute/cloud_runner.go @@ -0,0 +1,201 @@ +package distribute + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/provider" + "github.com/j3ssie/osmedeus/utils" + "github.com/panjf2000/ants" +) + +type CloudRunner struct { + Opt libs.Options `json:"-"` + Provider provider.Provider + + // for storing in local DB + TaskName string `json:"task_name"` + TaskType string `json:"task_type"` + Input string `json:"input"` + RawCommand string `json:"raw_command"` + InstanceFile string `json:"instance_file"` + + // core entry point + PublicIP string `json:"public_ip"` + DestInstance string `json:"dest_instance"` + SshPublicKey string `json:"ssh_public_key"` + SshPrivateKey string + SSHUser string + BasePath string + + InstanceID string `json:"instance_id"` + InstanceName string `json:"instance_name"` + IsError bool `json:"is_error"` + + Target map[string]string `json:"-"` + Runner core.Runner `json:"-"` +} + +// InitCloud init cloud runner obj +func InitCloud(options libs.Options, targets []string) { + // init clouds object queue + cloudConfigs := GetClouds(options) + if options.Cloud.CheckingLimit { + return + } + + // really start doing something + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(options.Concurrency, func(i interface{}) { + target := i.(string) + if strings.TrimSpace(target) == "" { + wg.Done() + return + } + + for { + var selectedCloud CloudRunner + var isDone bool + utils.DebugF("Select available accounts") + for _, cloudInfo := range cloudConfigs { + // don't run it again for next cloud account + if isDone { + break + } + if cloudInfo.Provider.Available { + selectedCloud = cloudInfo + } + + target = PrepareTarget(target, options) + if !options.Cloud.OnlyCreateDroplet { + utils.InforF("Initiating the scanning process of %v", color.HiMagentaString(utils.CleanPath(target))) + } + + // really start to run scan here + err := selectedCloud.Scan(target) + if err != nil { + utils.ErrorF("error start scan %s", color.HiCyanString(target)) + if options.Cloud.NoDelete { + continue + } + if ok := selectedCloud.Provider.DeleteInstance(selectedCloud.InstanceID); ok == nil { + selectedCloud.DeleteInstanceConfig() + } + continue + } + + if !options.Cloud.OnlyCreateDroplet { + utils.InforF("Completed scanning %v", color.HiCyanString(utils.CleanPath(target))) + utils.InforF("--------------------------------") + } + isDone = true + } + break + } + wg.Done() + + }, ants.WithPreAlloc(true)) + defer p.Release() + + for _, target := range targets { + wg.Add(1) + _ = p.Invoke(target) + } + wg.Wait() + +} + +// GetClouds prepare clouds object in config file +func GetClouds(options libs.Options) []CloudRunner { + var cloudInfos []CloudRunner + + // parse config from cloud/provider.yaml file + providerConfigs, err := provider.ParseProvider(options.CloudConfigFile) + if err != nil { + return cloudInfos + } + + options.Cloud.BuildRepo = providerConfigs.Builder.BuildRepo + options.Cloud.SecretKey = utils.NormalizePath(providerConfigs.Builder.SecretKey) + options.Cloud.PublicKey = utils.NormalizePath(providerConfigs.Builder.PublicKey) + + // we only get provider info from config file but replace token with --token + if options.Cloud.IgnoreConfigFile || options.Cloud.Token != "" { + var providerConfig provider.ConfigProvider + if len(providerConfigs.Clouds) > 0 { + utils.InforF("Ignore config file from: %v", options.CloudConfigFile) + providerConfig = providerConfigs.Clouds[0] + } + providerConfig.Token = options.Cloud.Token + cloudInfo := SetupProvider(options, providerConfig) + cloudInfos = append(cloudInfos, cloudInfo) + return cloudInfos + } + + for _, providerConfig := range providerConfigs.Clouds { + cloudInfo := SetupProvider(options, providerConfig) + if len(cloudInfo.Provider.Token) > 6 { + cloudInfo.Provider.RedactedToken = cloudInfo.Provider.Token[:5] + "***" + cloudInfo.Provider.Token[len(cloudInfo.Provider.Token)-5:] + } + cloudInfos = append(cloudInfos, cloudInfo) + } + + utils.InforF("Number of cloud providers ready in queue: %v", color.HiMagentaString("%v", len(cloudInfos))) + return cloudInfos +} + +// SetupProvider setup new provider +func SetupProvider(opt libs.Options, providerConfig provider.ConfigProvider) CloudRunner { + var cloudRunner CloudRunner + cloudRunner.Opt = opt + cloudRunner.Prepare() + + providerCloud, err := provider.InitProviderWithConfig(opt, providerConfig) + if err != nil { + return cloudRunner + } + cloudRunner.Provider = providerCloud + + cloudRunner.SSHUser = cloudRunner.Provider.SSHUser + if cloudRunner.SSHUser == "" { + cloudRunner.SSHUser = "root" + } + + if cloudRunner.SSHUser != "root" { + cloudRunner.BasePath = fmt.Sprintf("/home/%s", cloudRunner.SSHUser) + } + + if opt.Cloud.IgnoreSetup { + return cloudRunner + } + + // check if snapshot is okay or not + if !cloudRunner.Provider.SnapshotFound || opt.Cloud.ReBuildBaseImage { + err = cloudRunner.Provider.BuildImage() + if err != nil { + utils.ErrorF("error build snapshot at %v", cloudRunner.Provider.ProviderConfig.BuildFile) + return cloudRunner + } + } + + return cloudRunner +} + +// Prepare some variables +func (c *CloudRunner) Prepare() { + c.SshPrivateKey = c.Opt.Cloud.SecretKey + c.SshPublicKey = c.Opt.Cloud.PublicKey + c.SSHUser = "root" + c.BasePath = "/root" + + // make sure the permission of private key is right + os.Chmod(utils.NormalizePath(c.SshPrivateKey), 0600) + + // parse blank target to get env + c.Target = core.ParseInput("example.com", c.Opt) +} diff --git a/distribute/command.go b/distribute/command.go new file mode 100644 index 0000000..e2f3ecf --- /dev/null +++ b/distribute/command.go @@ -0,0 +1,175 @@ +package distribute + +import ( + "strings" + "time" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/utils" +) + +func (c *CloudRunner) CloudMoreParams() { + utils.DebugF("Parsed more params") + + // take from -m flag + if c.Opt.Cloud.Module != "" { + module := core.DirectSelectModule(c.Opt, c.Opt.Cloud.Module) + parsedModule, err := core.ParseModules(module) + if err != nil || parsedModule.Name == "" { + return + } + + for _, param := range parsedModule.Params { + for k, v := range param { + v = core.ResolveData(v, c.Target) + if strings.HasPrefix(v, "~/") { + v = utils.NormalizePath(v) + } + c.Target[k] = v + } + } + + return + } + + flows := core.SelectFlow(c.Opt.Cloud.Flow, c.Opt) + if len(flows) == 0 { + return + } + + parsedFlow, err := core.ParseFlow(flows[0]) + if err != nil { + return + } + + for _, param := range parsedFlow.Params { + for k, v := range param { + v = core.ResolveData(v, c.Target) + if strings.HasPrefix(v, "~/") { + v = utils.NormalizePath(v) + } + c.Target[k] = v + } + } + +} + +func (c *CloudRunner) CreateUIReport() { + if c.Opt.NoDB { + return + } + + // take from -m flag + if c.Opt.Cloud.Module != "" { + module := core.DirectSelectModule(c.Opt, c.Opt.Cloud.Module) + parsedModule, err := core.ParseModules(module) + if err != nil || parsedModule.Name == "" { + return + } + + // create record on UI + c.Opt.Module = core.ResolveReports(parsedModule, c.Target) + // @TODO: add the report file to the runtime JSON file + // database.DBNewReports(parsedModule, &c.Runner.TargetObj) + return + } + + flows := core.SelectFlow(c.Opt.Cloud.Flow, c.Opt) + if len(flows) == 0 { + return + } + + parsedFlow, err := core.ParseFlow(flows[0]) + if err != nil { + return + } + + for _, rawModules := range parsedFlow.Routines { + // select module depend on it's flow + if rawModules.FlowFolder != "" { + parsedFlow.Type = rawModules.FlowFolder + } else { + parsedFlow.Type = parsedFlow.DefaultType + } + + modules := core.SelectModules(rawModules.Modules, c.Opt) + + for _, module := range modules { + parsedModule, err := core.ParseModules(module) + if err != nil || parsedModule.Desc == "" { + continue + } + // create record on UI + c.Opt.Module = core.ResolveReports(parsedModule, c.Target) + // @TODO: add the report runtime JSON file + // database.DBNewReports(c.Opt.Module, &c.Runner.TargetObj) + } + } +} + +func (c *CloudRunner) RetryCommandWithExpectString(cmd string, expectString string, timeoutRaw ...string) string { + timeout := "300s" + if len(timeoutRaw) > 0 { + timeout = timeoutRaw[0] + } + var out string + + utils.DebugF("Retry command: %s", cmd) + for i := 0; i < c.Opt.Cloud.Retry; i++ { + if timeout == "000" { + out, _ = utils.RunOSCommand(cmd) + } else { + out = utils.RunCmdWithOutput(cmd, timeout) + } + + if !strings.Contains(out, expectString) { + utils.DebugF(out) + time.Sleep(time.Duration(60*(i+1)) * time.Second) + continue + } + return out + } + return out +} + +func (c *CloudRunner) RetryCommandWithExcludeString(cmd string, excludeString string, timeoutRaw ...string) string { + var out string + timeout := "300s" + if len(timeoutRaw) > 0 { + timeout = timeoutRaw[0] + } + + utils.DebugF("Retry command: %s", cmd) + for i := 0; i < c.Opt.Cloud.Retry; i++ { + if timeout == "000" { + out, _ = utils.RunOSCommand(cmd) + } else { + out = utils.RunCmdWithOutput(cmd, timeout) + } + + if strings.Contains(out, excludeString) { + utils.DebugF(out) + time.Sleep(time.Duration(60*(i+1)) * time.Second) + continue + } + return out + } + return out +} + +func (c *CloudRunner) RetryCommand(cmd string, timeoutRaw ...string) { + timeout := "300s" + if len(timeoutRaw) > 0 { + timeout = timeoutRaw[0] + } + utils.DebugF("Retry command: %s", cmd) + for i := 0; i < c.Opt.Cloud.Retry; i++ { + out, err := utils.RunCommandWithErr(cmd, timeout) + if err != nil { + utils.DebugF(out) + time.Sleep(time.Duration(60*(i+1)) * time.Second) + continue + } + break + } +} diff --git a/distribute/create.go b/distribute/create.go new file mode 100644 index 0000000..c88cd0d --- /dev/null +++ b/distribute/create.go @@ -0,0 +1,153 @@ +package distribute + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/jinzhu/copier" + jsoniter "github.com/json-iterator/go" +) + +func (c *CloudRunner) CreateInstance(target string) error { + c.Opt.Cloud.Input = target + c.Target = core.ParseInput(target, c.Opt) + + if c.Opt.EnableFormatInput { + c.Opt.Cloud.Input = c.Target["Target"] + } + if c.Opt.Cloud.Workspace == "" { + c.Opt.Cloud.Workspace = utils.CleanPath(c.Target["Target"]) + } + + if c.Opt.Cloud.Workspace != "" { + c.Target["Workspace"] = c.Opt.Cloud.Workspace + // disable changing workspace name in huntersuite to keep track the scanID + if c.Opt.Cloud.EnableChunk { + if strings.Contains(target, "-chunk-") { + index := strings.Split(target, "-chunk-")[1] + c.Target["WorkspaceChunk"] = path.Base(c.Opt.Cloud.Workspace) + "-" + index + utils.DebugF("Changing workspace name: %v", c.Target["WorkspaceChunk"]) + } + } + utils.DebugF(`c.Target["Workspace"] -- %v`, c.Target["Workspace"]) + } + + // run-flow-example.com + InstancePrefix := fmt.Sprintf("run") + if c.Opt.Cloud.Flow != "" { + InstancePrefix = fmt.Sprintf("run-%s", utils.CleanPath(c.Opt.Cloud.Flow)) + } + if c.Opt.Cloud.Module != "" { + InstancePrefix = fmt.Sprintf("run-%s", utils.CleanPath(c.Opt.Cloud.Module)) + } + + // run-flow-example.com-1 + c.InstanceName = fmt.Sprintf("%s-%s", InstancePrefix, strings.TrimSpace(c.Target["Workspace"])) + if c.Opt.Cloud.EnableChunk { + c.InstanceName = fmt.Sprintf("%s-%s", InstancePrefix, strings.TrimSpace(c.Target["WorkspaceChunk"])) + } + + // make sure the droplet name is unique + c.InstanceName = c.InstanceName + "-" + utils.RandomString(4) + // clean up the instance name first + if strings.Contains(c.InstanceName, "_") { + c.InstanceName = strings.ReplaceAll(c.InstanceName, "_", "-") + } + + // force changing instance name from cli + if c.Opt.Cloud.InstanceName != "" { + c.InstanceName = c.Opt.Cloud.InstanceName + } + + // quick check for instance name + switch c.Provider.ProviderName { + case "ln", "line", "linode": + if len(c.InstanceName) > 32 { + c.InstanceName = strings.Trim(strings.Trim(strings.Trim(c.InstanceName[:20], "-"), "."), "_") + "-" + utils.RandomString(9) + } + } + + /* Really start to run command to create instance here */ + err := c.Provider.CreateInstance(c.InstanceName) + + // fix the naming in linode might need another check for length + if err != nil { + // check if account reach limit first + if strings.Contains(err.Error(), "Account Limit reached") { + utils.ErrorF("Account %v reach limit instance", c.Provider.RedactedToken) + return fmt.Errorf("error creating instance") + } + + if strings.Contains(err.Error(), "valid hostname characters are allowed") || strings.Contains(err.Error(), "[400] [label]") { + c.InstanceName = fmt.Sprintf("runr-%s-%s", utils.GetTS(), utils.RandomString(8)) + } + err = c.Provider.CreateInstance(c.InstanceName) + if err != nil { + return fmt.Errorf("error creating instance") + } + } + + if err != nil { + return fmt.Errorf("error creating instance: %v", c.InstanceName) + } + + c.PublicIP = c.Provider.CreatedInstance.IPAddress + + c.InstanceID = c.Provider.CreatedInstance.InstanceID + c.DestInstance = fmt.Sprintf("%s@%s", c.SSHUser, c.PublicIP) + utils.InforF("Instance created: %s -- %s -- %v", color.HiCyanString(c.InstanceName), color.HiCyanString(c.InstanceID), color.HiCyanString(c.PublicIP)) + c.Target["CIP"] = c.PublicIP + c.Target["RemoteIP"] = c.PublicIP + + // create the JSON file for the instance + c.WriteInstanceConfig() + return nil + +} + +func (c *CloudRunner) DeleteInstanceConfig() { + utils.DebugF("Deleting instance config %v", color.HiCyanString(c.InstanceFile)) + os.Remove(c.InstanceFile) +} + +func (c *CloudRunner) WriteInstanceConfig() error { + instanceFile := c.Opt.Env.InstancesFolder + "/" + c.InstanceName + "-" + c.PublicIP + ".json" + c.InstanceFile = instanceFile + utils.DebugF("Writing instance config to %v", color.HiCyanString(instanceFile)) + + var instanceObj CloudRunner + copier.Copy(&instanceObj, &c) + instanceObj.Runner = core.Runner{} + instanceObj.Opt = libs.Options{} + + if data, err := jsoniter.MarshalToString(instanceObj); err == nil { + utils.WriteToFile(instanceFile, data) + } + + return nil +} + +func (c *CloudRunner) ErrorWriteInstanceConfig() error { + instanceFile := c.Opt.Env.InstancesFolder + "/" + c.InstanceName + "-" + c.PublicIP + ".json" + c.InstanceFile = instanceFile + utils.DebugF("Writing instance config to %v", color.HiCyanString(instanceFile)) + + var instanceObj CloudRunner + copier.Copy(&instanceObj, &c) + + instanceObj.IsError = true + instanceObj.Runner = core.Runner{} + instanceObj.Opt = libs.Options{} + + if data, err := jsoniter.MarshalToString(instanceObj); err == nil { + utils.WriteToFile(instanceFile, data) + } + + return nil +} diff --git a/distribute/db_cloud.go b/distribute/db_cloud.go new file mode 100644 index 0000000..43afa24 --- /dev/null +++ b/distribute/db_cloud.go @@ -0,0 +1,35 @@ +package distribute + +import ( + "path" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/database" + "github.com/j3ssie/osmedeus/utils" + jsoniter "github.com/json-iterator/go" +) + +func (c *CloudRunner) DBNewTarget() { + targetFolder := path.Join(c.Opt.Env.WorkspacesFolder, c.Target["Workspace"]) + utils.MakeDir(targetFolder) + + c.Runner.ScanObj = database.Scan{ + IsCloud: true, + InputName: c.Target["Workspace"], + TaskType: "distributed", + } + + c.Runner.ScanObj.CreatedAt = time.Now() + c.DBRuntimeUpdate() +} + +func (c *CloudRunner) DBRuntimeUpdate() { + runtimeFile := path.Join(c.Opt.Env.WorkspacesFolder, c.Target["Workspace"], "runtime") + c.Runner.ScanObj.UpdatedAt = time.Now() + + if runtimeData, err := jsoniter.MarshalToString(c.Runner.ScanObj); err == nil { + utils.InforF("Updating runtime file: %s", color.HiCyanString(runtimeFile)) + utils.WriteToFile(runtimeFile, runtimeData) + } +} diff --git a/distribute/health.go b/distribute/health.go new file mode 100644 index 0000000..6994de6 --- /dev/null +++ b/distribute/health.go @@ -0,0 +1,159 @@ +package distribute + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/jinzhu/copier" + jsoniter "github.com/json-iterator/go" + "github.com/panjf2000/ants" +) + +func GetAllInstances(opt libs.Options) (instances []CloudRunner) { + utils.InforF("Getting all instances from %s", opt.Env.InstancesFolder) + + // Read all entries in the directory + entries, err := os.ReadDir(opt.Env.InstancesFolder) + if err != nil { + utils.ErrorF("error reading instances folder: %v", err) + return instances + } + + for _, entry := range entries { + instanceFile := filepath.Join(opt.Env.InstancesFolder, entry.Name()) + + if !utils.FileExists(instanceFile) { + continue + } + + instance := CloudRunner{} + runtimeContent := utils.GetFileContent(instanceFile) + err := jsoniter.UnmarshalFromString(runtimeContent, &instance) + if err != nil { + utils.ErrorF("error marshal data: %v", err) + } + instances = append(instances, instance) + } + + return instances +} + +func CheckingCloudInstance(opt libs.Options) { + if opt.Cloud.Retry == 0 { + opt.Cloud.Retry = 8 + } + + // select all cloud instance + instances := GetAllInstances(opt) + if len(instances) == 0 { + utils.WarnF("no active cloud instance running") + return + } + + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(opt.Concurrency*5, func(i interface{}) { + // really start to scan + instance := i.(CloudRunner) + + var options libs.Options + copier.Copy(&options, &opt) + instance.Opt = opt + instance.Provider.IsBackgroundCheck = true + instance.Provider.InitClient() + instance.Prepare() + instance.HealthCheck() + wg.Done() + }, ants.WithPreAlloc(true)) + defer p.Release() + + utils.InforF("Evaluating the status of %v cloud instances", color.HiMagentaString("%v", len(instances))) + for _, instance := range instances { + wg.Add(1) + _ = p.Invoke(instance) + } + wg.Wait() + +} + +func (c *CloudRunner) HealthCheck() bool { + utils.InforF("[%v] Checking the instance: %v -- %v", color.HiYellowString(c.Provider.ProviderName), color.HiCyanString(c.PublicIP), color.HiCyanString(path.Base(c.InstanceName))) + c.Opt.Cloud.Input = c.Input + + counter := 0 + for i := 0; i < c.Opt.Cloud.Retry; i++ { + // sync in result first + c.SyncResult() + if !c.IsRunning() || c.IsPanic() { + counter += 1 + utils.DebugF("retry[%v]: An error has been detected at the cloud instance: %v -- %v", counter, c.Provider.ProviderName, c.PublicIP) + } + + if counter == c.Opt.Cloud.Retry-1 { + utils.ErrorF("An error has been detected at the cloud instance: %v -- %v", c.Provider.ProviderName, c.PublicIP) + err := c.Provider.DeleteInstance(c.InstanceID) + if err == nil { + utils.InforF("Instance deleted %s -- %s", color.HiYellowString(c.PublicIP), color.HiYellowString(c.InstanceID)) + instanceFile := c.Opt.Env.InstancesFolder + "/" + c.InstanceName + "-" + c.PublicIP + ".json" + os.Remove(instanceFile) + } + return false + } + time.Sleep(10 * time.Second) + } + + utils.InforF("[%v] Instance is still running well: %v -- %v", color.HiYellowString(c.Provider.ProviderName), color.HiCyanString(c.PublicIP), color.HiCyanString(path.Base(c.InstanceName))) + return true +} + +// IsRunning checking if cloud instance is running or not +func (c *CloudRunner) IsRunning() bool { + utils.DebugF("Checking running process at: %v", c.PublicIP) + cmd := fmt.Sprintf("%s utils ps --json", libs.BINARY) + + // ignore checking process if you're running custom command '--no-ps' + if c.Opt.Cloud.IgnoreProcess { + return true + } + + out, err := c.SSHExec(cmd) + if err == nil && strings.Contains(out, "pid") { + return true + } + + // retry checking process + for i := 0; i < c.Opt.Cloud.Retry; i += 2 { + out, err := c.SSHExec(cmd) + if err == nil && strings.Contains(out, "pid") { + return true + } + } + + utils.DebugF(out) + utils.ErrorF("no process running at %v", c.PublicIP) + return false +} + +// IsPanic checking if cloud instance has any panic error +func (c *CloudRunner) IsPanic() bool { + utils.DebugF("Checking panic error at: %v", c.PublicIP) + cmd := fmt.Sprintf("%s utils tmux logs -A -l 30", libs.BINARY) + out, err := c.SSHExec(cmd) + + if err == nil { + if strings.Contains(out, "out of memory") || strings.Contains(out, "runtime.(*") || strings.Contains(out, "[panic]") { + utils.DebugF(out) + utils.ErrorF("Fatal panic detected at: %s", c.PublicIP) + return true + } + } + + return false +} diff --git a/distribute/mics.go b/distribute/mics.go new file mode 100644 index 0000000..3222aaa --- /dev/null +++ b/distribute/mics.go @@ -0,0 +1,115 @@ +package distribute + +import ( + "fmt" + "path" + "strings" + + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cast" +) + +// CommandBuilder build core command from API +func CommandBuilder(options libs.Options) string { + binary := libs.BINARY + " --from-remote" + + if options.Debug { + binary += " --debug" + } + + if options.Update.EnableUpdate { + binary += " --update" + } + + if options.Threads > 0 { + binary += " --threads-hold " + cast.ToString(options.Threads) + } + + if options.ScanID != "" { + binary += fmt.Sprintf(" --sid %v ", options.ScanID) + } + + taskData := options.Cloud + var command string + var workspace, concurrency, timeout, params, workflow, plugin, extra string + if options.Cloud.Extra != "" { + extra = " " + options.Cloud.Extra + " " + } + + if options.Cloud.Flow != "" { + options.Cloud.Flow = utils.NormalizePath(options.Cloud.Flow) + workflow = fmt.Sprintf(" -f '%v' ", options.Cloud.Flow) + } + + if len(taskData.Params) > 0 { + for _, param := range taskData.Params { + params += fmt.Sprintf(" -p '%v'", param) + } + } + + if options.Cloud.Threads > 1 { + concurrency = fmt.Sprintf(" -c %d ", options.Cloud.Threads) + } + + // get workspace + if taskData.Workspace == "" { + taskData.Workspace = utils.CleanPath(taskData.Input) + } + if taskData.Workspace != "" { + workspace = fmt.Sprintf(" -w '%v'", strings.TrimSpace(taskData.Workspace)) + } + + if taskData.Module != "" { + taskData.Module = utils.NormalizePath(taskData.Module) + plugin = fmt.Sprintf(" -m '%v' ", taskData.Module) + } + + // override everything + if taskData.RawCommand != "" { + return taskData.RawCommand + } + + // use target as a file + if options.Cloud.TargetAsFile && utils.FileExists(taskData.Input) { + taskData.InputsFile = taskData.Input + } + + // mean general scan + if taskData.Module == "" { + command = fmt.Sprintf("%v scan %v -t %v %v%v%v%v%v", binary, workflow, taskData.Input, concurrency, timeout, workspace, params, extra) + if options.Cloud.TargetAsFile { + command = fmt.Sprintf("%v scan %v -T %v %v%v%v%v%v", binary, workflow, taskData.InputsFile, concurrency, timeout, workspace, params, extra) + } + command = strings.TrimSpace(command) + + return command + } + + command = fmt.Sprintf("%v scan %v -t %v %v%v%v%v%v", binary, plugin, taskData.Input, concurrency, timeout, workspace, params, extra) + if options.Cloud.TargetAsFile { + command = fmt.Sprintf("%v scan %v -T %v %v%v%v%v%v", binary, plugin, taskData.InputsFile, concurrency, timeout, workspace, params, extra) + } + command = strings.TrimSpace(command) + + return command +} + +// PrepareTarget change the target file destination +func PrepareTarget(target string, options libs.Options) string { + if options.Cloud.EnableChunk { + return target + } + + if !utils.FileExists(target) && !utils.FolderExists(target) { + utils.DebugF("target is not a file: %s", target) + return target + } + + utils.MakeDir(options.Cloud.TempTarget) + dest := path.Join(options.Cloud.TempTarget, path.Base(target)) + + utils.Copy(target, dest) + utils.InforF("Change target %s --> %s", target, dest) + return dest +} diff --git a/distribute/routine.go b/distribute/routine.go new file mode 100644 index 0000000..589b7e8 --- /dev/null +++ b/distribute/routine.go @@ -0,0 +1,94 @@ +package distribute + +import ( + "fmt" + "time" + + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +func (c *CloudRunner) Scan(target string) error { + err := c.CreateInstance(target) + if err != nil { + utils.ErrorF("Error to create instance") + return err + } + + if c.Opt.Cloud.OnlyCreateDroplet { + utils.DebugF("Only create instance, skip the scan") + return nil + } + + // parse some parameters first + c.PrepareInput() + + // pre run before starting the scan + c.PreRunLocal() + + if c.Opt.Cloud.EnableSyncWorkflow { + err = c.CopyWorkflow() + if err != nil { + utils.ErrorF("Error to copy workflow to instance") + return err + } + } + + // copy target to droplet first + err = c.CopyTarget() + if err != nil { + utils.ErrorF("Error to copy input to instance") + return err + } + + // pre run commands + c.PreRunRemote() + + err = c.StartScan() + if err != nil { + utils.ErrorF("Error to run command on instance") + return err + } + + // check if done file created in instance or not + c.CheckingDone() + + if !c.Opt.Cloud.DisableLocalSync { + err = c.SyncResult() + // sync result to one more time if it's not done yet + if c.Opt.Cloud.NoDelete { + time.Sleep(100 * time.Second) + err = c.SyncResult() + } + + if err != nil { + utils.ErrorF("Error to sync result to instance") + return err + } + } + + // post run after scan done + c.PostRunLocal() + + if c.Opt.Cloud.CopyWorkspaceToGit { + utils.InforF("Coping workspace to git storages") + baseCmd := fmt.Sprintf("%s scan --nn -f sync -t {{.Workspace}}", libs.BINARY) + cmd := core.ResolveData(baseCmd, c.Target) + utils.RunCommandWithErr(cmd) + } + + if c.Opt.Cloud.NoDelete { + utils.DebugF("Skipping delete instance") + return nil + } + + err = c.Provider.DeleteInstance(c.InstanceID) + if err != nil { + utils.ErrorF("Error to delete instance") + return err + } + c.DeleteInstanceConfig() + + return nil +} diff --git a/distribute/scan.go b/distribute/scan.go new file mode 100644 index 0000000..7a38b88 --- /dev/null +++ b/distribute/scan.go @@ -0,0 +1,267 @@ +package distribute + +import ( + "fmt" + "path" + "strings" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/provider" + "github.com/j3ssie/osmedeus/utils" +) + +func (c *CloudRunner) PrepareInput() { + c.Opt.Scan.ROptions = c.Target + c.Opt.Scan.Flow = "cloud-distributed" + //database.NewScan(c.Opt, "cli") + c.Input = c.Target["Target"] + runner, err := core.InitRunner(c.Input, c.Opt) + if err == nil { + c.Runner = runner + c.Runner.RunnerType = "cloud" + } + + // for creating local DB record + if c.Opt.Cloud.Flow != "" { + c.TaskName = c.Opt.Cloud.Flow + c.TaskType = "flow" + } else { + c.TaskName = c.Opt.Cloud.Flow + c.TaskType = "module" + } + c.CloudMoreParams() + // more params from -p flag + if len(c.Opt.Cloud.Params) > 0 { + params := core.ParseParams(c.Opt.Cloud.Params) + if len(params) > 0 { + for k, v := range params { + v = core.ResolveData(v, c.Target) + c.Target[k] = v + } + } + } + +} + +func (c *CloudRunner) StartScan() error { + c.DBNewTarget() + // c.DBNewScanLocal() + // c.DBNewCloudInstance() + + err := c.RunScan() + if err != nil { + return fmt.Errorf("error to start the scan") + } + + // utils.DebugF("Create UI report for %s: %s", c.DestInstance, color.HiCyanString(c.Opt.Cloud.RawCommand)) + // c.Runner.DBDoneScan() + return nil +} + +func (c *CloudRunner) RunScan() error { + // -f mean run in a background + // ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i root@IP -f + // -t mean run and wait + // ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i root@IP -t + if c.Opt.Cloud.RawCommand != "" { + c.Opt.Cloud.RawCommand = core.ResolveData(c.Opt.Cloud.RawCommand, c.Target) + } else { + c.Opt.Cloud.RawCommand = CommandBuilder(c.Opt) + } + c.Opt.Cloud.RawCommand = core.ResolveData(c.Opt.Cloud.RawCommand, c.Target) + c.RawCommand = c.Opt.Cloud.RawCommand + + // init tmux session + out, err := c.SSHExec("tmux new-session -d -t main") + + // boot the instance again if it still didn't up + if err != nil && strings.Contains(err.Error(), "time out") { + time.Sleep(60 * time.Second) + c.Provider.Action(provider.BootInstance, c.InstanceID) + out, err = c.SSHExec("tmux new-session -d -t main") + } + + // really run main command + tcmd := fmt.Sprintf(`"%s"`, c.Opt.Cloud.RawCommand) + _, err = c.SSHExec(fmt.Sprintf(`tmux send-keys %s ENTER`, tcmd)) + + // still error then it must be something wrong + if err != nil { + utils.ErrorF("An error occurred with %v", color.HiYellowString(c.DestInstance)) + utils.ErrorF("error log: %v", out) + return fmt.Errorf("error running command on %v", color.HiYellowString(c.DestInstance)) + } + utils.InforF("Start to run the scan %v with command %v", color.HiYellowString(c.DestInstance), color.HiCyanString(c.Opt.Cloud.RawCommand)) + + // wait a bit for process really start + time.Sleep(60 * time.Second) + if !c.IsRunning() { + return fmt.Errorf("Failed to initiate the scan on %v", color.HiYellowString(c.DestInstance)) + } + + c.WriteInstanceConfig() + return nil +} + +func (c *CloudRunner) CheckingDone() error { + if c.Opt.Cloud.NoDelete { + time.Sleep(60 * time.Second) + return nil + } + utils.InforF("Checking scan process at: %s", color.HiBlueString(c.PublicIP)) + + // dest := fmt.Sprintf("%s/.%s/workspaces/%s/done", c.BasePath, libs.BINARY, c.Target["Workspace"]) + // @NOTE: this is new workspaces folder + dest := fmt.Sprintf("%s/workspaces-%s/%s/done", c.BasePath, libs.BINARY, c.Target["Workspace"]) + + cmd := fmt.Sprintf("file %s", dest) + out, _ := c.SSHExec(cmd) + + if strings.Contains(out, "ASCII text") || strings.Contains(out, "JSON data") { + return nil + } + + waitTime := utils.CalcTimeout(c.Opt.Cloud.ClearTime) + counter := 1 + for { + time.Sleep(time.Duration(waitTime) * time.Second) + out, _ = c.SSHExec(cmd) + if strings.Contains(out, "ASCII text") || strings.Contains(out, "JSON data") { + utils.InforF("The scan is done at: %s", color.HiBlueString(c.PublicIP)) + return nil + } + + if !c.IsRunning() { + return fmt.Errorf("no process running at %v", c.PublicIP) + } + + // check if we have panic or not + if c.IsPanic() { + return fmt.Errorf("panic detected at %v", c.PublicIP) + } + + if counter%50 == 0 { + c.SyncResult() + } + counter++ + } +} + +// below code is experimental part + +func (c *CloudRunner) SyncResult() error { + target := c.Opt.Cloud.Input + if !c.Provider.IsBackgroundCheck { + utils.InforF("Sync back the data of taget %v from %v", color.HiCyanString(target), color.HiYellowString(c.DestInstance)) + } + + if c.Opt.Cloud.LocalSyncFolder == "" { + c.Opt.Cloud.LocalSyncFolder = fmt.Sprintf("%s/workspaces-%s/", c.BasePath, libs.BINARY) + } + + // on vps machine + src := c.Opt.Cloud.LocalSyncFolder + + // on local + dest := path.Join(c.Opt.Env.WorkspacesFolder, c.Opt.Cloud.BaseWorkspace) + utils.MakeDir(dest) + + cmd := fmt.Sprintf("rsync -e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i %s' -avzr --progress %s:%s %s", c.SshPrivateKey, c.DestInstance, src, dest) + + c.RetryCommandWithExpectString(cmd, `bytes/sec`) + if !utils.FolderExists(dest) { + utils.ErrorF("error sync result back from: %v to %v", c.DestInstance, dest) + } + + return nil +} + +func (c *CloudRunner) CopyTarget() error { + target := c.Opt.Cloud.Input + utils.DebugF("Sync input of %s to %s", target, c.DestInstance) + + dest := c.Target["Target"] + if !utils.FileExists(dest) && !utils.FolderExists(dest) { + utils.DebugF("target is not a file: %s", dest) + return nil + } + + if c.Opt.Cloud.EnableChunk { + dest = c.Opt.Cloud.ChunkInputs + } + + c.SSHExec(fmt.Sprintf("mkdir -p %s", path.Dir(dest))) + cmd := fmt.Sprintf("rsync -e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i %s' -avzr --progress %s %s:%s", c.SshPrivateKey, dest, c.DestInstance, dest) + c.RetryCommandWithExpectString(cmd, `bytes/sec`) + return nil +} + +func (c *CloudRunner) CopyWorkflow() error { + utils.DebugF("Sync workflow of %s to %s", c.Opt.Env.WorkFlowsFolder, c.DestInstance) + destWorkflow := fmt.Sprintf("%v/osmedeus-base/", c.BasePath) + if c.Opt.Cloud.RemoteWorkflowFolder != "" { + destWorkflow = c.Opt.Cloud.RemoteWorkflowFolder + } + + // c.SSHExec(fmt.Sprintf("rm -rf %s && mkdir -p %s", destWorkflow, destWorkflow)) + cmd := fmt.Sprintf("rsync -e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i %s' -avzr --progress %s %s:%s", c.SshPrivateKey, c.Opt.Env.WorkFlowsFolder, c.DestInstance, destWorkflow) + c.RetryCommandWithExpectString(cmd, `bytes/sec`) + return nil +} + +func (c *CloudRunner) PreRunRemote() error { + if len(c.Opt.Cloud.RemotePreRun) <= 0 { + return nil + } + utils.InforF("Run remote command on: %s", c.PublicIP) + + // really start to run pre commands + for _, rcmd := range c.Opt.Cloud.RemotePreRun { + cmd := core.ResolveData(rcmd, c.Target) + utils.InforF("Run pre command on %s: %s", c.PublicIP, cmd) + c.SSHExec(cmd) + } + return nil +} + +func (c *CloudRunner) PreRunLocal() error { + if len(c.Opt.Cloud.LocalPreRun) <= 0 { + return nil + } + c.Opt.Scan.ROptions = c.Target + utils.InforF("Start %v", color.HiCyanString("PreRunLocal")) + + // really start to run pre commands + for _, script := range c.Opt.Cloud.LocalPreRun { + script = core.ResolveData(script, c.Target) + c.Runner.RunScript(script) + } + return nil +} + +func (c *CloudRunner) PostRunLocal() error { + c.Opt.Scan.ROptions = c.Target + + if len(c.Opt.Cloud.LocalSteps) > 0 { + // for running local steps + utils.DebugF("Running local steps") + for _, step := range c.Opt.Cloud.LocalSteps { + c.Runner.RunStep(step) + } + } + + if len(c.Opt.Cloud.LocalPostRun) <= 0 { + return nil + } + + utils.InforF("Start %v", color.HiCyanString("PostRunLocal")) + // really start to run pre commands + for _, script := range c.Opt.Cloud.LocalPostRun { + script = core.ResolveData(script, c.Target) + c.Runner.RunScript(script) + } + return nil +} diff --git a/distribute/ssh.go b/distribute/ssh.go new file mode 100644 index 0000000..f7e2c44 --- /dev/null +++ b/distribute/ssh.go @@ -0,0 +1,430 @@ +package distribute + +import ( + "bytes" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/utils" + + "golang.org/x/crypto/ssh" +) + +func (c *CloudRunner) SSHExec(command string) (string, error) { + client, err := c.InitSSHClient() + if err != nil { + utils.ErrorF("Failed to initialize SSH connection to %v", color.HiYellowString(c.PublicIP)) + return "", err + } + defer client.Close() + + utils.DebugF("Running command on %s: %s", color.HiBlueString(c.PublicIP), color.HiGreenString(command)) + out, err := client.Cmd(command).Output() + if err != nil { + utils.ErrorF("err run command: %v", err) + return "", err + } + + return string(out), nil +} + +func (c *CloudRunner) InitSSHClient() (*Client, error) { + host := fmt.Sprintf("%s:%s", c.PublicIP, "22") + dest := color.HiCyanString("%s@%s", c.SSHUser, host) + utils.DebugF("Connecting to %v with key %v", dest, c.SshPrivateKey) + client, err := DialWithKeyString(host, c.SSHUser, c.Opt.Cloud.SecretKeyContent) + if err != nil { + utils.ErrorF("err connect to %v -- %v", host, err) + for i := 0; i < c.Opt.Cloud.Retry; i++ { + client, err = DialWithKeyString(host, c.SSHUser, c.Opt.Cloud.SecretKeyContent) + if err != nil { + time.Sleep(time.Duration(30*(i+1)) * time.Second) + continue + } + break + } + if err != nil { + return nil, err + } + } + return client, nil +} + +/* Start of SSH Lib */ +// literally copy from this: https://github.com/helloyi/go-sshclient + +type remoteScriptType byte +type remoteShellType byte + +const ( + cmdLine remoteScriptType = iota + rawScript + scriptFile + + interactiveShell remoteShellType = iota + nonInteractiveShell +) + +type Client struct { + client *ssh.Client +} + +// DialWithPasswd starts a client connection to the given SSH server with passwd authmethod. +func DialWithPasswd(addr, user, passwd string) (*Client, error) { + config := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.Password(passwd), + }, + HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }), + } + + return Dial("tcp", addr, config) +} + +// DialWithKeyString starts a client connection to the given SSH server with key authmethod. +func DialWithKeyString(addr, user, keyContent string) (*Client, error) { + signer, err := ssh.ParsePrivateKey([]byte(strings.TrimSpace(keyContent))) + if err != nil { + return nil, err + } + + config := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }), + } + + return Dial("tcp", addr, config) +} + +// DialWithKey starts a client connection to the given SSH server with key authmethod. +func DialWithKey(addr, user, keyfile string) (*Client, error) { + key, err := os.ReadFile(keyfile) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, err + } + + config := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }), + } + + return Dial("tcp", addr, config) +} + +// DialWithKeyWithPassphrase same as DialWithKey but with a passphrase to decrypt the private key +func DialWithKeyWithPassphrase(addr, user, keyfile string, passphrase string) (*Client, error) { + key, err := os.ReadFile(keyfile) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKeyWithPassphrase(key, []byte(passphrase)) + if err != nil { + return nil, err + } + + config := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }), + } + + return Dial("tcp", addr, config) +} + +// Dial starts a client connection to the given SSH server. +// This is wrap the ssh.Dial +func Dial(network, addr string, config *ssh.ClientConfig) (*Client, error) { + client, err := ssh.Dial(network, addr, config) + if err != nil { + return nil, err + } + return &Client{ + client: client, + }, nil +} + +func (c *Client) Close() error { + return c.client.Close() +} + +// Cmd create a command on client +func (c *Client) Cmd(cmd string) *remoteScript { + return &remoteScript{ + _type: cmdLine, + client: c.client, + script: bytes.NewBufferString(cmd + "\n"), + } +} + +// Script +func (c *Client) Script(script string) *remoteScript { + return &remoteScript{ + _type: rawScript, + client: c.client, + script: bytes.NewBufferString(script + "\n"), + } +} + +// ScriptFile +func (c *Client) ScriptFile(fname string) *remoteScript { + return &remoteScript{ + _type: scriptFile, + client: c.client, + scriptFile: fname, + } +} + +type remoteScript struct { + client *ssh.Client + _type remoteScriptType + script *bytes.Buffer + scriptFile string + err error + + stdout io.Writer + stderr io.Writer +} + +// Run +func (rs *remoteScript) Run() error { + if rs.err != nil { + fmt.Println(rs.err) + return rs.err + } + + if rs._type == cmdLine { + return rs.runCmds() + } else if rs._type == rawScript { + return rs.runScript() + } else if rs._type == scriptFile { + return rs.runScriptFile() + } else { + return errors.New("Not supported remoteScript type") + } +} + +func (rs *remoteScript) Output() ([]byte, error) { + if rs.stdout != nil { + return nil, errors.New("Stdout already set") + } + var out bytes.Buffer + rs.stdout = &out + err := rs.Run() + return out.Bytes(), err +} + +func (rs *remoteScript) SmartOutput() ([]byte, error) { + if rs.stdout != nil { + return nil, errors.New("Stdout already set") + } + if rs.stderr != nil { + return nil, errors.New("Stderr already set") + } + + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) + rs.stdout = &stdout + rs.stderr = &stderr + err := rs.Run() + if err != nil { + return stderr.Bytes(), err + } + return stdout.Bytes(), err +} + +func (rs *remoteScript) Cmd(cmd string) *remoteScript { + _, err := rs.script.WriteString(cmd + "\n") + if err != nil { + rs.err = err + } + return rs +} + +func (rs *remoteScript) SetStdio(stdout, stderr io.Writer) *remoteScript { + rs.stdout = stdout + rs.stderr = stderr + return rs +} + +func (rs *remoteScript) runCmd(cmd string) error { + session, err := rs.client.NewSession() + if err != nil { + return err + } + defer session.Close() + + session.Stdout = rs.stdout + session.Stderr = rs.stderr + + if err := session.Run(cmd); err != nil { + return err + } + return nil +} + +func (rs *remoteScript) runCmds() error { + for { + statment, err := rs.script.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return err + } + + if err := rs.runCmd(statment); err != nil { + return err + } + } + + return nil +} + +func (rs *remoteScript) runScript() error { + session, err := rs.client.NewSession() + if err != nil { + return err + } + + session.Stdin = rs.script + session.Stdout = rs.stdout + session.Stderr = rs.stderr + + if err := session.Shell(); err != nil { + return err + } + if err := session.Wait(); err != nil { + return err + } + + return nil +} + +func (rs *remoteScript) runScriptFile() error { + var buffer bytes.Buffer + file, err := os.Open(rs.scriptFile) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(&buffer, file) + if err != nil { + return err + } + + rs.script = &buffer + return rs.runScript() +} + +type remoteShell struct { + client *ssh.Client + requestPty bool + terminalConfig *TerminalConfig + + stdin io.Reader + stdout io.Writer + stderr io.Writer +} + +type TerminalConfig struct { + Term string + Height int + Weight int + Modes ssh.TerminalModes +} + +// Terminal create a interactive shell on client. +func (c *Client) Terminal(config *TerminalConfig) *remoteShell { + return &remoteShell{ + client: c.client, + terminalConfig: config, + requestPty: true, + } +} + +// Shell create a noninteractive shell on client. +func (c *Client) Shell() *remoteShell { + return &remoteShell{ + client: c.client, + requestPty: false, + } +} + +func (rs *remoteShell) SetStdio(stdin io.Reader, stdout, stderr io.Writer) *remoteShell { + rs.stdin = stdin + rs.stdout = stdout + rs.stderr = stderr + return rs +} + +// Start start a remote shell on client +func (rs *remoteShell) Start() error { + session, err := rs.client.NewSession() + if err != nil { + return err + } + defer session.Close() + + if rs.stdin == nil { + session.Stdin = os.Stdin + } else { + session.Stdin = rs.stdin + } + if rs.stdout == nil { + session.Stdout = os.Stdout + } else { + session.Stdout = rs.stdout + } + if rs.stderr == nil { + session.Stderr = os.Stderr + } else { + session.Stderr = rs.stderr + } + + if rs.requestPty { + tc := rs.terminalConfig + if tc == nil { + tc = &TerminalConfig{ + Term: "xterm", + Height: 40, + Weight: 80, + } + } + if err := session.RequestPty(tc.Term, tc.Height, tc.Weight, tc.Modes); err != nil { + return err + } + } + + if err := session.Shell(); err != nil { + return err + } + + if err := session.Wait(); err != nil { + return err + } + + return nil +} diff --git a/distribute/wizard.go b/distribute/wizard.go new file mode 100644 index 0000000..74e2b1b --- /dev/null +++ b/distribute/wizard.go @@ -0,0 +1,240 @@ +package distribute + +import ( + "bufio" + "fmt" + "os" + "strings" + "syscall" + + "github.com/Shopify/yaml" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/provider" + "github.com/j3ssie/osmedeus/utils" + "github.com/olekukonko/tablewriter" + "golang.org/x/term" +) + +func InitCloudSetup(opt libs.Options) { + var supportedProvider = []string{"aws", "digitalocean", "linode"} + fmt.Println("šŸ”® Start cloud setup wizard šŸ”®") + fmt.Println("Currently only these providers are supported: ", color.HiYellowString("%v", supportedProvider)) + + var configProviders provider.ConfigProviders + configProviders.Builder.BuildRepo = opt.Cloud.BuildRepo + if configProviders.Builder.BuildRepo == "" { + configProviders.Builder.BuildRepo = StringPrompt("šŸŒ€ Enter premium install script URL (e.g: https://long-url-here/x/premium.sh)?", "") + } + configProviders.Builder.PublicKey = opt.Cloud.PublicKey + configProviders.Builder.SecretKey = opt.Cloud.SecretKey + + // get the existing one + if opt.Cloud.AddNewProvider { + fmt.Println("šŸŖ„ Add new provider config to the existing config") + providerConfigs, err := provider.ParseProvider(opt.CloudConfigFile) + if err == nil { + configProviders.Clouds = providerConfigs.Clouds + } + } + + for { + configProvider := generateProvider() + configProviders.Clouds = append(configProviders.Clouds, configProvider) + if stop := StringPrompt("šŸŒ€ Do you want to add more provider (y/N)?", "n"); stop != "y" { + break + } + } + + data, err := yaml.Marshal(&configProviders) + if err != nil { + return + } + + //utils.DebugF(string(data)) + if override := StringPrompt("šŸ§™ Do you want to override the old config at "+color.HiGreenString(opt.CloudConfigFile)+" (Y/n)?", "y"); override != "n" { + _, err := utils.WriteToFile(opt.CloudConfigFile, string(data)) + if err != nil { + utils.WarnF("error to write provider config: %v", opt.CloudConfigFile) + } + } + + if isValidate := StringPrompt("ā„ļø Do you want to validate your provider config (Y/n)?", "y"); isValidate == "n" { + return + } + fmt.Printf("šŸ’” You also can manually rebuild the snapshot with the command: %s\n", color.HiCyanString("%s provider build --rebuild", libs.BINARY)) + + var cloudRunners []CloudRunner + for _, configProvider := range configProviders.Clouds { + cloudRunner, err := ValidateProvider(opt, configProvider) + if err != nil { + utils.ErrorF("error validate config: %v -- %v", configProvider.Name, configProvider.RedactedToken) + continue + } + cloudRunners = append(cloudRunners, cloudRunner) + } + + content := [][]string{} + for _, cloudRunner := range cloudRunners { + row := []string{ + cloudRunner.Provider.ProviderName, + cloudRunner.Provider.RedactedToken, + cloudRunner.Provider.SSHKeyID, + cloudRunner.Provider.SnapshotID, + } + content = append(content, row) + } + table := tablewriter.NewWriter(os.Stderr) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"Provider", "Token", "SSH Key ID", "Osmedeus Snapshot ID"}) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.AppendBulk(content) // Add Bulk Data + table.Render() + +} + +func generateProvider() provider.ConfigProvider { + configProvider := provider.ConfigProvider{ + Token: "", + Provider: "digitalocean", + DefaultImage: "debian-10-x64", + Size: "s-2vcpu-4gb", + Region: "sfo3", + Username: "root", + Limit: 0, + } + + //# provider: "digitalocean" + //# name: "do-osmp" + //# default_image: "debian-10-x64" + //# size: "s-2vcpu-4gb" + //# region: "sfo3" + + //# provider: "linode" + //# name: "linode-osmp" + //# default_image: "linode/debian10" + //# size: "g6-standard-1" + //# region: "us-east" + + configProvider.Provider = StringPrompt("šŸŒ€ What is your cloud provider?", configProvider.Provider) + + switch configProvider.Provider { + case "do", "digitalocean": + configProvider.Provider = "digitalocean" + fmt.Printf("==> provider selected: %s\n", color.HiBlueString("digitalocean")) + case "ln", "line", "linode": + configProvider = provider.ConfigProvider{ + Token: "", + Provider: "linode", + DefaultImage: "linode/debian10", + Size: "g6-standard-1", + Region: "us-east", + Username: "root", + + Limit: 0, + } + fmt.Printf("==> provider selected: %s\n", color.HiBlueString("linode")) + case "aw", "asw", "aws": + configProvider = provider.ConfigProvider{ + AccessKeyId: "", + SecretKey: "", + Provider: "aws", + DefaultImage: "ami-0ee39036464b9a87e", + Size: "t2.medium", + Region: "ap-southeast-1", + Username: "admin", + Limit: 0, + } + fmt.Printf("==> provider selected: %s\n", color.HiBlueString("aws")) + + default: + configProvider.Provider = "digitalocean" + fmt.Printf("==> provider selected: %s\n", color.HiBlueString("digitalocean")) + } + + if configProvider.Provider == "aws" { + configProvider.AccessKeyId = credentials("AWS AccessKeyId") + fmt.Println() + configProvider.SecretKey = credentials("AWS SecretKey") + fmt.Println() + } else { + configProvider.Token = credentials("API Token") + } + + configProvider.Name = fmt.Sprintf("%s-%s", configProvider.Provider, utils.RandomString(6)) + + if configProvider.Provider == "aws" { + fmt.Printf("šŸ’” Choose your image carefully because it related to region.\n") + fmt.Printf("šŸ’” Refer for this link for more information: " + color.HiCyanString("https://wiki.debian.org/Cloud/AmazonEC2Image/Buster")) + } + configProvider.DefaultImage = StringPrompt("\nšŸŒ€ Choose "+color.HiGreenString("base image")+" for building Osmedeus Image?", configProvider.DefaultImage) + configProvider.Size = StringPrompt("šŸŒ€ Choose "+color.HiGreenString("instance type")+" for running the scan?", configProvider.Size) + configProvider.Region = StringPrompt("šŸŒ€ Choose "+color.HiGreenString("instance region")+" for running the scan?", configProvider.Region) + + return configProvider +} + +func credentials(name string) string { + fmt.Printf("šŸ”‘ Enter your %s? ", color.HiGreenString(name)) + var token string + for { + byteToken, err := term.ReadPassword(int(syscall.Stdin)) + if err == nil && len(byteToken) > 6 { + token = strings.TrimSpace(string(byteToken)) + break + } + utils.WarnF("Looks like your token is invalid. Please try again: %v", token) + } + + redactedToken := token[:5] + "***" + token[len(token)-5:] + fmt.Printf("Your data has been saved: %v", color.HiBlueString(redactedToken)) + return token +} + +// StringPrompt asks for a string value using the label +func StringPrompt(label string, alt string) string { + var s string + r := bufio.NewReader(os.Stdin) + for { + fmt.Fprintf(os.Stderr, fmt.Sprintf("%v (default: %s): ", label, color.HiCyanString(alt))) + s, _ = r.ReadString('\n') + s = strings.TrimSpace(strings.ToLower(s)) + if s == "" { + if alt != "" { + return alt + } + utils.WarnF("Blank input doesn't allow, please specify one") + } + + if s != "" { + break + } + } + return strings.TrimSpace(s) +} + +// ValidateProvider setup new provider +func ValidateProvider(opt libs.Options, providerConfig provider.ConfigProvider) (CloudRunner, error) { + var cloudRunner CloudRunner + cloudRunner.Opt = opt + cloudRunner.Prepare() + + providerCloud, err := provider.InitProviderWithConfig(opt, providerConfig) + if err != nil { + return cloudRunner, err + } + cloudRunner.Provider = providerCloud + + // check if snapshot is okay or not + if !cloudRunner.Provider.SnapshotFound { + utils.InforF("No Snapshot found: %v", cloudRunner.Provider.SnapshotName) + err = cloudRunner.Provider.BuildImage() + if err != nil { + utils.ErrorF("error build snapshot at %v", cloudRunner.Provider.ProviderConfig.BuildFile) + return cloudRunner, err + } + } + + return cloudRunner, nil +} diff --git a/execution/clean.go b/execution/clean.go new file mode 100644 index 0000000..063f10d --- /dev/null +++ b/execution/clean.go @@ -0,0 +1,527 @@ +package execution + +import ( + "bufio" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/flosch/pongo2/v6" + "github.com/spf13/cast" + + "github.com/thoas/go-funk" + + "github.com/Jeffail/gabs/v2" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +// Cleaning the execution directory +func Cleaning(folder string, reports []string) { + utils.DebugF("Cleaning result: %v", folder) + // list all the file + items, err := filepath.Glob(fmt.Sprintf("%v/*", folder)) + if err != nil { + return + } + + for _, item := range items { + item = utils.NormalizePath(item) + utils.DebugF("Check Cleaning: %v", item) + if funk.Contains(reports, item) { + utils.DebugF("Skip cleaning file: %v", item) + continue + } + + fi, err := os.Stat(item) + if err != nil { + continue + } + switch mode := fi.Mode(); { + case mode.IsDir(): + DeleteFolder(item) + continue + case mode.IsRegular(): + DeleteFile(item) + } + } +} + +// CleanGoBuster clean output for gobuster +func CleanGoBuster(src string, output string) { + data := utils.GetFileContent(src) + if data == "" { + return + } + result := strings.Replace(data, "Found: ", "", -1) + utils.WriteToFile(output, result) +} + +// CleanMassdns clean result of massdns to get IP address +func CleanMassdns(filename string, output string) { + file, err := os.Open(utils.NormalizePath(filename)) + if err != nil { + return + } + defer file.Close() + + outputFile, err := os.OpenFile(utils.NormalizePath(output), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return + } + defer outputFile.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + if strings.Contains(line, " A ") { + data := strings.Split(line, " A ") + host := strings.Trim(data[0], ".") + ip := strings.Trim(data[1], ".") + outputFile.WriteString(fmt.Sprintf("%v,%v\n", host, ip)) + } else if strings.Contains(line, " CNAME ") { + data := strings.Split(line, " CNAME ") + host := strings.Trim(data[0], ".") + ip := strings.Trim(data[1], ".") + outputFile.WriteString(fmt.Sprintf("%v,%v\n", host, ip)) + } + } +} + +// CleanAmass get IP range and ASN from Amass result +func CleanAmass(filename string, output string) { + content := utils.ReadingLines(filename) + if len(content) <= 0 { + return + } + + var result []string + for _, line := range content { + jsonParsed, err := gabs.ParseJSON([]byte(line)) + if err != nil { + continue + } + asn := jsonParsed.Path("asn").String() + cidr := jsonParsed.Path("cidr").String() + desc := jsonParsed.Path("desc").String() + if cidr != "" && cidr != "null" { + result = append(result, fmt.Sprintf("AS%v,%v,%v", asn, cidr, desc)) + } + } + + if len(result) > 0 { + utils.WriteToFile(output, strings.Join(result, "\n")) + } + +} + +// CleanSWebanalyze get to get formatted report +func CleanSWebanalyze(filename string, output string) { + content := utils.ReadingLines(filename) + if len(content) <= 0 { + return + } + + result := make(map[string][]string) + var finalResult []string + for _, line := range content { + if strings.TrimSpace(line) == "" { + continue + } + if !strings.Contains(line, "tech~") || !strings.Contains(line, "|") { + continue + } + data := strings.Split(line, "|") + domain, _ := utils.GetDomain(strings.TrimLeft(data[1], "~")) + tech := data[2] + if strings.HasSuffix(tech, "/") { + tech = strings.Trim(tech, "/") + } + result[domain] = append(result[domain], tech) + } + + // join summary result + for k, v := range result { + v = funk.UniqString(v) + sort.Strings(v) + techs := strings.Join(v, ",") + data := fmt.Sprintf("domain|%v;;techs|%v", k, strings.Trim(techs, ",")) + finalResult = append(finalResult, data) + } + + if len(result) > 0 { + utils.WriteToFile(output, strings.Join(finalResult, "\n")) + } +} + +// CleanWebanalyze get to get formatted report +func CleanWebanalyze(filename string, output string, sum string) { + content := utils.ReadingLines(filename) + if len(content) <= 0 { + return + } + + result := make(map[string][]string) + var finalResult []string + var techSum []string + for _, line := range content { + jsonParsed, err := gabs.ParseJSON([]byte(line)) + if err != nil { + continue + } + + URL := strings.Trim(jsonParsed.S("hostname").String(), `"`) + u, err := url.Parse(URL) + if err != nil { + continue + } + matches := jsonParsed.S("matches").Children() + for _, child := range matches { + techs := "" + app := strings.Trim(child.S("app_name").String(), `"`) + version := strings.Trim(child.S("version").String(), `"`) + if version != "" { + techs += fmt.Sprintf("%v/%v,", app, version) + } else { + techs += fmt.Sprintf("%v,", app) + } + + // ignore blank techs + if techs == "" || strings.Trim(techs, ",") == "" || strings.Trim(techs, ",") == " " { + continue + } + techSum = append(techSum, strings.Trim(app, ",")) + techs = strings.TrimSpace(strings.Trim(techs, ",")) + result[u.Hostname()] = append(result[u.Hostname()], techs) + } + } + + // join summary result + for k, v := range result { + techs := strings.Join(funk.UniqString(v), ",") + data := fmt.Sprintf("domain|%v;;techs|%v", k, strings.Trim(techs, ",")) + finalResult = append(finalResult, data) + } + + if len(techSum) > 0 { + utils.WriteToFile(sum, strings.Join(funk.UniqString(techSum), "\n")) + } + if len(result) > 0 { + utils.WriteToFile(output, strings.Join(finalResult, "\n")) + } +} + +// CleanArjun clean output of Arjun +func CleanArjun(src string, dest string) { + src = utils.NormalizePath(src) + if !utils.FolderExists(src) { + return + } + + if !strings.HasSuffix(src, "/") { + src += "/" + } + src = path.Join(src, "*") + outputs, err := filepath.Glob(src) + if err != nil { + return + } + + var data []string + for _, output := range outputs { + content := utils.GetFileContent(output) + jsonParsed, err := gabs.ParseJSON([]byte(content)) + if err != nil { + continue + } + prefix := "[GET]" + if strings.HasPrefix(output, "post") { + prefix = "[POST]" + } + + for URL, child := range jsonParsed.ChildrenMap() { + if len(child.Children()) <= 0 { + continue + } + for _, query := range child.Children() { + line := fmt.Sprintf("%v %v?%v=FUZZ", prefix, URL, query.Data()) + data = append(data, line) + } + } + } + + if len(data) > 0 { + utils.WriteToFile(dest, strings.Join(data, "\n")) + } +} + +// CleanJSONDnsx get to get formatted report +func CleanJSONDnsx(filename string, dest string) { + content := utils.ReadingLines(filename) + if len(content) <= 0 { + utils.WarnF("File not found: %s", filename) + return + } + + var results []string + for _, line := range content { + jsonParsed, err := gabs.ParseJSON([]byte(line)) + if err != nil { + continue + } + + domain, ok := jsonParsed.S("host").Data().(string) + if !ok { + continue + } + + a := jsonParsed.S("a") + if a != nil { + for _, record := range a.Children() { + data := fmt.Sprintf("%s A %s", domain, cast.ToString(record.Data())) + results = append(results, data) + } + } + + cname := jsonParsed.S("cname") + if cname != nil { + for _, record := range cname.Children() { + data := fmt.Sprintf("%s CNAME %s", domain, cast.ToString(record.Data())) + results = append(results, data) + } + } + + mx := jsonParsed.S("mx") + if mx != nil { + for _, record := range mx.Children() { + data := fmt.Sprintf("%s MX %s", domain, cast.ToString(record.Data())) + results = append(results, data) + } + } + + ns := jsonParsed.S("ns") + if ns != nil { + for _, record := range ns.Children() { + data := fmt.Sprintf("%s NS %s", domain, cast.ToString(record.Data())) + results = append(results, data) + } + } + + } + + if len(results) > 0 { + utils.WriteToFile(dest, strings.Join(results, "\n")) + } +} + +// CleanRustScan make rustscan data to flat format ip:port +func CleanRustScan(src string, dest string) { + src = utils.NormalizePath(src) + dest = utils.NormalizePath(dest) + content := utils.ReadingLines(src) + if len(content) <= 0 { + utils.WarnF("File not found: %s", src) + return + } + + var results []string + for _, line := range content { + // 103.247.207.76 -> [80,80,443,443] + if !strings.Contains(line, " -> ") { + continue + } + + ip := strings.Split(line, " -> ")[0] + rPorts := strings.Split(line, " -> ")[1] + rPorts = rPorts[1 : len(rPorts)-1] + + if !strings.Contains(rPorts, ",") { + results = append(results, fmt.Sprintf("%s:%s", ip, rPorts)) + } + + ports := strings.Split(rPorts, ",") + ports = funk.UniqString(ports) + for _, port := range ports { + results = append(results, fmt.Sprintf("%s:%s", ip, port)) + } + } + utils.WriteToFile(dest, strings.Join(results, "\n")) +} + +type Vulnerability struct { + SignID string + SignPath string + URL string + Risk string + Confidence string + Request string + ModalID string + + ReportPath string + ReportFile string + + Status string + Length string + Words string + Time string +} + +func GenNucleiReport(opt libs.Options, src string, dest string, templateFile string) { + if templateFile == "" { + templateFile = path.Join(opt.Env.DataFolder, "nuclei-report.html") + } + + if !utils.FileExists(src) { + utils.WarnF("file not found: %v", src) + return + } + content := utils.ReadingLines(src) + var vulns []Vulnerability + + for index, line := range content { + if strings.TrimSpace(line) == "" { + continue + } + jsonParsed, err := gabs.ParseJSON([]byte(line)) + if err != nil { + utils.WarnF("Error parse JSON Data") + continue + } + + modalID, err := utils.GetDomain(cast.ToString(jsonParsed.S("host").Data())) + if err != nil { + continue + } + modalID = fmt.Sprintf("%s-%d", strings.ReplaceAll(modalID, ".", "-"), index) + + vulns = append(vulns, Vulnerability{ + Request: cast.ToString(jsonParsed.S("request").Data()), + URL: cast.ToString(jsonParsed.S("matched-at").Data()), + SignID: cast.ToString(jsonParsed.S("template-id").Data()), + Risk: cast.ToString(jsonParsed.S("info", "severity").Data()), + ModalID: modalID, + Confidence: "Tentative", + }) + } + + utils.DebugF("Reading vuln %v from: %v ", len(vulns), src) + if len(vulns) == 0 { + utils.WarnF("No Vulnerability found %v", src) + return + } + + // read template file + tmpl := utils.GetFileContent(templateFile) + if strings.TrimSpace(tmpl) == "" { + utils.WarnF("empty template data: %v", templateFile) + return + } + + variable := make(map[string]interface{}) + variable["Title"] = "Nuclei Summary Report" + variable["Vulnerabilities"] = vulns + variable["CurrentDay"] = utils.GetCurrentDay() + variable["Version"] = libs.VERSION + variable["Src"] = filepath.Base(src) + + tpl, err := pongo2.FromString(tmpl) + if err != nil { + utils.WarnF("error render data: %v", err) + return + } + out, ok := tpl.Execute(variable) + if ok == nil { + utils.DebugF("Writing Nuclei HTML report to: %v", dest) + utils.WriteToFile(dest, out) + } +} + +// CleanJSONHttpx get to get formatted report +func CleanJSONHttpx(filename string, dest string) { + content := utils.ReadingLines(filename) + if len(content) <= 0 { + utils.WarnF("File not found: %s", filename) + return + } + + var results []string + for _, line := range content { + jsonParsed, err := gabs.ParseJSON([]byte(line)) + if err != nil { + continue + } + + URL := cast.ToString(jsonParsed.S("url").Data()) + //domain, _ := utils.GetDomain(URL) + bodyHash := cast.ToString(jsonParsed.S("body-sha256").Data()) + headerHash := cast.ToString(jsonParsed.S("header-sha256").Data()) + hash := utils.GenHash(fmt.Sprintf("%s-%s", headerHash, bodyHash)) + + title := "No-Title" + rawTitle := jsonParsed.S("title") + if rawTitle != nil { + title = cast.ToString(rawTitle.Data()) + } + + rawTechs := jsonParsed.S("tech") + techs := "No-Tech" + if rawTechs != nil { + techs = strings.Join(cast.ToStringSlice(rawTechs.Data()), ";;") + + } + + data := fmt.Sprintf("%s,%s,%s,%s", URL, hash, title, techs) + results = append(results, data) + + } + + if len(results) > 0 { + utils.WriteToFile(dest, strings.Join(results, "\n")) + } +} + +// CleanFFUFJson get to get formatted report +func CleanFFUFJson(filename string, dest string) { + content := utils.ReadingLines(filename) + if len(content) <= 0 { + utils.WarnF("File not found: %s", filename) + return + } + + var results []string + for _, line := range content { + jsonParsed, err := gabs.ParseJSON([]byte(line)) + if err != nil { + continue + } + + resultsJson := jsonParsed.S("results") + if resultsJson == nil { + continue + } + + for _, item := range resultsJson.Children() { + //.url,.status,.length,.words,.lines,.redirectlocation + endpoint := cast.ToString(item.S("url").Data()) + status := cast.ToString(item.S("status").Data()) + length := cast.ToString(item.S("length").Data()) + words := cast.ToString(item.S("words").Data()) + lines := cast.ToString(item.S("lines").Data()) + redirectLocation := cast.ToString(item.S("redirectlocation").Data()) + + data := fmt.Sprintf("%s,%s,%s,%s,%s,%s", endpoint, status, length, words, lines, redirectLocation) + results = append(results, data) + } + } + + if len(results) > 0 { + utils.WriteToFile(dest, strings.Join(results, "\n")) + } +} diff --git a/execution/clean_test.go b/execution/clean_test.go new file mode 100644 index 0000000..8342b9a --- /dev/null +++ b/execution/clean_test.go @@ -0,0 +1,19 @@ +package execution + +// +//func TestCleanWebanalyze(t *testing.T) { +// var options libs.Options +// options.Debug = true +// CleanWebanalyze("/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/fingerprint/duckduckgo.com-technology.json", "/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/fingerprint/duckduckgo.com-technologies.txt") +// +// if !utils.FileExists("/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/fingerprint/duckduckgo.com-technologies.txt") { +// t.WarnF("Error CleanWebanalyze") +// } +//} + +//func TestCleanSWebanalyze(t *testing.T) { +// CleanSWebanalyze("/tmp/mmm", "/tmp/technologies.txt") +// if !utils.FileExists("/tmp/technologies.txt") { +// t.WarnF("Error CleanSWebanalyze") +// } +//} diff --git a/execution/git.go b/execution/git.go new file mode 100644 index 0000000..03f7206 --- /dev/null +++ b/execution/git.go @@ -0,0 +1,183 @@ +package execution + +import ( + "errors" + "fmt" + "os" + "path" + "strings" + + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +/* + +Base Git command: +GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i /path/to/private/key' git xxx + +*/ + +/* +GitDiff('path-to-diff-file', 'path-to-output') +GitDiff('path-to-diff-folder', 'path-to-output') +GitDiff('path-to-diff-file', 'path-to-output', 'number-of-commit-to-diff') +*/ + +// GitDiff run git diff command +func GitDiff(dest string, output string, history string, options libs.Options) { + if options.NoGit || options.Storages["secret_key"] == "" { + return + } + if !utils.FileExists(dest) { + utils.WarnF("File not found: %v", dest) + return + } + utils.DebugF("Git Diff: %v", dest) + diffCmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git diff -U0 HEAD~%v --output=%v %v", options.Storages["secret_key"], history, output, dest) + Execution(diffCmd, options) +} + +// LoopGitDiff like GitDiff but take input as a file +func LoopGitDiff(src string, output string, options libs.Options) { + lines := utils.ReadingFileUnique(src) + if len(lines) == 0 { + return + } + for _, line := range lines { + GitDiff(line, output, "1", options) + } +} + +// DiffCompare run git diff command +func DiffCompare(src string, dest string, output string, options libs.Options) { + if options.NoGit || options.Storages["secret_key"] == "" { + return + } + // if !utils.FileExists(src) || !utils.FileExists(dest) { + // return + // } + diffCmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git diff --no-index --output=%v %v %v", options.Storages["secret_key"], output, src, dest) + Execution(diffCmd, options) +} + +// PullResult pull latest data from result repo +func PullResult(storageFolder string, options libs.Options) { + if options.NoGit || options.Storages["secret_key"] == "" { + return + } + if !utils.FolderExists(storageFolder) { + return + } + utils.DebugF("git pull on: %v", storageFolder) + pullCmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git -C %v pull -f", options.Storages["secret_key"], storageFolder) + Execution(pullCmd, options) +} + +// PushResult push result to git repo +func PushResult(storageFolder string, commitMess string, options libs.Options) { + if options.NoGit || options.Storages["secret_key"] == "" { + return + } + + if !utils.FolderExists(storageFolder) { + return + } + utils.DebugF("git push on: %v", storageFolder) + cmds := []string{ + fmt.Sprintf(`GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git -C %v add -A`, options.Storages["secret_key"], storageFolder), + fmt.Sprintf(`GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git -C %v commit -m "%v"`, options.Storages["secret_key"], storageFolder, commitMess), + fmt.Sprintf(`GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git -C %v push -f`, options.Storages["secret_key"], storageFolder), + fmt.Sprintf(`GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git -C %v push -f`, options.Storages["secret_key"], storageFolder), + } + // really run git command + for _, cmd := range cmds { + Execution(cmd, options) + } +} + +// GitClone update latest UI and Plugins from default repo +func GitClone(url string, dest string, forced bool, options libs.Options) { + if options.NoGit || options.Storages["secret_key"] == "" { + utils.WarnF("Storage Disable") + return + } + + if url == "" || dest == "" { + utils.WarnF("Invalid repo or no destination") + return + } + + // check if folder is exist or not + dest = utils.NormalizePath(dest) + if forced { + utils.DebugF("Remove: %v", dest) + os.RemoveAll(dest) + } + + // if folder exist and have .git folder in it, do git pull instead + if utils.FolderExists(dest) { + if utils.FileExists(path.Join(dest, "/.git/HEAD")) { + pullCmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git -C %v pull -f", options.Storages["secret_key"], dest) + Execution(pullCmd, options) + return + } + } + + // cloning new + utils.DebugF("Cloning: %v", url) + cloneCmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git clone --depth=1 %v %v", options.Storages["secret_key"], url, dest) + Execution(cloneCmd, options) +} + +// CloneRepo clone the repo +func CloneRepo(url string, dest string, options libs.Options) error { + if !ValidGitURL(url) { + return errors.New("invalid repo name") + } + if options.NoGit || options.Storages["secret_key"] == "" { + return errors.New("storage Disable") + } + + if url == "" || dest == "" { + return errors.New("storage didn't setup correctly") + } + // check if folder is exist or not + dest = utils.NormalizePath(dest) + utils.DebugF("Cloning: %v", url) + + if utils.FolderExists(dest) { + if utils.FileExists(path.Join(dest, "/.git/HEAD")) { + return nil + } + utils.DebugF("Remove: %v", dest) + os.RemoveAll(dest) + } + + // cloning new + cloneCmd := fmt.Sprintf("GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i %v' git clone --depth=1 %v %v", options.Storages["secret_key"], url, dest) + Execution(cloneCmd, options) + if utils.FolderExists(dest) { + if utils.FileExists(path.Join(dest, "/.git/HEAD")) { + return nil + } + return nil + } + return errors.New("fail to clone Repo") +} + +// ValidGitURL simple validate git repo +func ValidGitURL(raw string) bool { + if strings.TrimSpace(raw) == "" { + return false + } + if !strings.HasPrefix(raw, "git@") { + return false + } + + if !strings.Contains(raw, "github.com") && !strings.Contains(raw, "gitlab.com") { + return false + } + + return true +} diff --git a/execution/git_test.go b/execution/git_test.go new file mode 100644 index 0000000..6cdbda1 --- /dev/null +++ b/execution/git_test.go @@ -0,0 +1,25 @@ +package execution + +import ( + "fmt" + "testing" + + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +func TestDiffCompare(t *testing.T) { + var options libs.Options + options.Debug = true + DiffCompare("/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/subdomain/final-duckduckgo.com.txt", "/Users/j3ssie/.osmedeus/storages/summary/duckduckgo.com/subdomain-duckduckgo.com.txt", "/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/subdomain/diff-duckduckgo.com-2019-12-28_3:20:9.txt", options) + // DiffCompare(options) + // fmt.Println(result) + + data := utils.GetFileContent("/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/subdomain/diff-duckduckgo.com-2019-12-28_3:20:9.txt") + fmt.Println(data) + + if !utils.FileExists("/Users/j3ssie/.osmedeus/workspaces/duckduckgo.com/subdomain/diff-duckduckgo.com-2019-12-28_3:20:9.txt") { + t.Logf("Error DiffCompare") + } + +} diff --git a/execution/gitlab.go b/execution/gitlab.go new file mode 100644 index 0000000..00ba0aa --- /dev/null +++ b/execution/gitlab.go @@ -0,0 +1,206 @@ +package execution + +import ( + "errors" + "fmt" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "log" + "strings" + + "github.com/xanzy/go-gitlab" +) + +// GitlabAuth do authentication with gitlab +func GitlabAuth(options libs.Options) (*gitlab.Client, error) { + if options.NoGit { + return nil, nil + } + // prefer using username + if options.Git.Username != "GITLAB_USER" { + utils.DebugF("Do authen with user: %s", options.Git.Username) + git, err := gitlab.NewBasicAuthClient( + options.Git.Username, + options.Git.Password, + gitlab.WithBaseURL(options.Git.BaseURL), + ) + if err != nil { + utils.WarnF("Error authen in Gitlab %v: %v", options.Git.BaseURL, err) + return nil, err + } + return git, nil + } + + if options.Git.Token == "" { + return nil, errors.New("no credentials provided") + } + utils.DebugF("Do authen with token: %s", options.Git.Token) + git, err := gitlab.NewClient( + options.Git.Token, + gitlab.WithBaseURL(options.Git.BaseURL), + ) + if err != nil { + utils.WarnF("Error authen in Gitlab %v: %v", options.Git.BaseURL, err) + return nil, err + } + return git, nil +} + +// CreateGitlabRepo create gitlab repo +func CreateGitlabRepo(repo string, tag string, options libs.Options) string { + if options.NoGit { + return "" + } + git, err := GitlabAuth(options) + if err != nil { + return "" + } + + utils.InforF("Create repo: %v", repo) + tags := []string{options.Git.DefaultTag} + if tag != "" { + if strings.Contains(tag, ",") { + tags = append(tags, strings.Split(tag, ",")...) + } else { + tags = append(tags, tag) + } + } + + opt := gitlab.CreateProjectOptions{ + Name: gitlab.String(repo), + Path: nil, + DefaultBranch: gitlab.String("master"), + Visibility: gitlab.Visibility(gitlab.PrivateVisibility), + TagList: &tags, + } + gRepo, _, err := git.Projects.CreateProject(&opt, nil) + if err != nil { + utils.WarnF("Error to create %v: %v", repo, err) + return "" + } + + optMember := gitlab.AddProjectMemberOptions{ + UserID: gitlab.Int(options.Git.DefaultUID), + AccessLevel: gitlab.AccessLevel(gitlab.MaintainerPermissions), + } + git.ProjectMembers.AddProjectMember(gRepo.ID, &optMember) + utils.InforF("Created repo at: %s with pid: %v", gRepo.SSHURLToRepo, gRepo.ID) + return gRepo.SSHURLToRepo +} + +// DeleteRepo delete repo by id or name +func DeleteRepo(repo string, pid int, options libs.Options) { + if options.NoGit { + return + } + git, err := GitlabAuth(options) + if err != nil { + return + } + // select pid first + if pid != 0 { + utils.InforF("Delete repo with id: %v", pid) + git.Projects.DeleteProject(pid) + return + } + + user, _, err := git.Users.CurrentUser() + projects, _, err := git.Projects.ListUserProjects(user.ID, nil) + for _, project := range projects { + if project.Name == repo { + utils.InforF("Delete repo: %v", repo) + git.Projects.DeleteProject(project.ID) + return + } + } + utils.WarnF("Project not found: %v", repo) +} + +// ListProjects delete repo by id or name +func ListProjects(gitUser int, options libs.Options) { + if options.NoGit { + return + } + git, err := GitlabAuth(options) + if err != nil { + utils.WarnF("Err get do authen user: %v", err) + return + } + uid := gitUser + var username string + if gitUser == 0 { + user, _, err := git.Users.CurrentUser() + if err != nil { + utils.WarnF("Err get current user: %v", err) + return + } + uid = user.ID + username = user.Username + } else { + //user, _, err := git.Users.GetUser(uid, git.Users.) + //if err != nil { + // utils.WarnF("Err get current user: %v", err) + // return + //} + //username = user.Username + } + utils.InforF("Listing projects of uid: %v", uid) + projects, _, err := git.Projects.ListUserProjects(uid, nil) + if err != nil { + utils.WarnF("Error listing projects: %v", err) + return + } + for _, project := range projects { + fmt.Printf("%30s -- %10d\n", fmt.Sprintf("%s/%s", username, project.Name), project.ID) + } +} + +// This example shows how to create a client with username and password. +func GitAuthSample() { + // git, err := gitlab.NewBasicAuthClient( + // "user", + // "password", + // gitlab.WithBaseURL("https://gitlab.com"), + // ) + git, err := gitlab.NewClient( + "token-here-s", + gitlab.WithBaseURL("https://gitlab.com"), + ) + + if err != nil { + log.Fatal(err) + } + + // List all projects + user, _, err := git.Users.CurrentUser() + //spew.Dump(user) + + // Create + projects, _, err := git.Projects.ListUserProjects(user.ID, nil) + ////git.Projects. + //group, _, _ := git.Groups.ListAllGroupMembers(9182310, nil) + //spew.Dump(group) + //tags := []string{"osm", "test"} + ////git.Projects.ListUserProjects() + //opt := gitlab.CreateProjectOptions{ + // Name: gitlab.String("test-project-tag-osm"), + // Path: nil, + // DefaultBranch: gitlab.String("master"), + // //GroupWithProjectTemplatesID: gitlab.Int(9182310), + // Visibility: gitlab.Visibility(gitlab.PrivateVisibility), + // TagList: &tags, + //} + //git.Projects.CreateProject(&opt, nil) + + if err != nil { + log.Fatal(err) + } + + //git.Projects.DeleteProject(pid) + + log.Printf("Found %d projects", len(projects)) + for _, project := range projects { + fmt.Printf("%30s -- %10d\n", fmt.Sprintf("%s/%s", user.Username, project.Name), project.ID) + } + +} diff --git a/execution/gitlab_test.go b/execution/gitlab_test.go new file mode 100644 index 0000000..24050cf --- /dev/null +++ b/execution/gitlab_test.go @@ -0,0 +1,9 @@ +package execution + +import ( + "testing" +) + +func TestGitAuth(t *testing.T) { + GitAuthSample() +} diff --git a/execution/noti.go b/execution/noti.go new file mode 100644 index 0000000..4cf6c03 --- /dev/null +++ b/execution/noti.go @@ -0,0 +1,352 @@ +package execution + +import ( + "encoding/json" + "errors" + "fmt" + "math/rand" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/spf13/cast" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/robertkrimen/otto" + "github.com/slack-go/slack" +) + +// StatusNoti send to when module is done with report file +func StatusNoti(notiType string, options libs.Options) { + SendAttachment(notiType, "", options) +} + +// ReportNoti send to notification when module is done with report file +func ReportNoti(arguments []otto.Value, options libs.Options) { + if len(arguments) >= 1 { + for _, argument := range arguments { + SendFile(argument.String(), options.Noti.SlackReportChannel, options) + } + return + } + // if doesn't provide report file send all file in noti section + for _, file := range options.Module.Report.Noti { + SendFile(file, options.Noti.SlackReportChannel, options) + } +} + +// DiffNoti send to notification based on diff content +func DiffNoti(arguments []otto.Value, options libs.Options) { + if len(arguments) >= 1 { + for _, argument := range arguments { + filename := argument.String() + if !utils.FileExists(filename) { + continue + } + data := getNewContent(utils.ReadingLines(filename)) + if strings.TrimSpace(data) == "" { + continue + } + // messageContent := fmt.Sprintf("%v \n ```%v```", filename, data) + // SendAttachment("diff", messageContent, options) + SendFile(filename, options.Noti.SlackDiffChannel, options) + } + return + } + // if doesn't provide report file send all file in noti section + for _, filename := range options.Module.Report.Diff { + if !utils.FileExists(filename) { + continue + } + data := getNewContent(utils.ReadingLines(filename)) + if strings.TrimSpace(data) == "" { + continue + } + // messageContent := fmt.Sprintf("%v \n ```%v```", filename, data) + // SendAttachment("diff", messageContent, options) + SendFile(filename, options.Noti.SlackDiffChannel, options) + } +} + +func getNewContent(data []string) string { + var result string + for _, line := range data { + if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "++") { + result += fmt.Sprintf("%v\n", line) + } + } + return result +} + +// SendAttachment send attach message to specific channel +func SendAttachment(messType string, messContent string, options libs.Options) error { + if options.Noti.SlackToken == "" { + return errors.New("Slack config improperly") + } + + // choose method + var channel, color, mess string + switch messType { + case "start": + channel = options.Noti.SlackStatusChannel + color = "#005b9f" + mess = fmt.Sprintf("%v Start to run *%v* on *%v*", GetEmoji(), options.Module.Name, options.Scan.ROptions["Workspace"]) + break + case "done": + channel = options.Noti.SlackStatusChannel + color = "#32cb00" + mess = fmt.Sprintf("%v Done run *%v* on *%v*", GetEmoji(), options.Module.Name, options.Scan.ROptions["Workspace"]) + break + case "diff": + channel = options.Noti.SlackDiffChannel + color = "#5E35B1" + mess = fmt.Sprintf("%v Diff content on %v: \n %v", GetEmoji(), options.Scan.ROptions["Workspace"], messContent) + break + case "custom": + channel = options.Noti.SlackStatusChannel + color = "#1ABC9C" + mess = messContent + break + } + + if channel == "" || mess == "" { + return errors.New("Slack channel config improperly") + } + utils.DebugF("Sending %v message to %v", messType, channel) + + api := slack.New(options.Noti.SlackToken) + // message config + attachment := slack.Attachment{ + Color: color, + Text: mess, + // sender name + Footer: options.Noti.ClientName, + FooterIcon: GetIcon(), + Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + } + + _, _, err := api.PostMessage(channel, slack.MsgOptionAttachments(attachment)) + if err != nil { + return err + } + return nil +} + +// SlackWebHook send message with webhook +func SlackWebHook(webhookURL string, content string) error { + content = fmt.Sprintf("```%s```", content) + attachment := slack.Attachment{ + Color: "#1ABC9C", + Text: content, + // sender name + //Footer: options.Noti.ClientName, + FooterIcon: GetIcon(), + Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + } + + msg := slack.WebhookMessage{ + Attachments: []slack.Attachment{attachment}, + } + err := slack.PostWebhook(webhookURL, &msg) + return err +} + +// WebHookSendAttachment send attach message to specific channel +func WebHookSendAttachment(options libs.Options, messType string, messContent string) error { + if options.NoNoti { + return fmt.Errorf("noti disabled") + } + if options.Noti.SlackWebHook == "" { + return errors.New("slack webhook config improperly") + } + + // choose method + var color, mess string + switch messType { + case "start": + color = "#005b9f" + mess = fmt.Sprintf("%v Start to run *%v* on *%v*", GetEmoji(), options.Module.Name, options.Scan.ROptions["Workspace"]) + break + case "done": + color = "#32cb00" + mess = fmt.Sprintf("%v Done run *%v* on *%v*", GetEmoji(), options.Module.Name, options.Scan.ROptions["Workspace"]) + break + case "diff": + color = "#5E35B1" + mess = fmt.Sprintf("%v Diff content on %v: \n %v", GetEmoji(), options.Scan.ROptions["Workspace"], messContent) + break + case "custom": + color = "#1ABC9C" + mess = messContent + break + default: + color = "#1ABC9C" + mess = messContent + } + + // message config + attachment := slack.Attachment{ + Color: color, + Text: mess, + // sender name + Footer: options.Noti.ClientName, + FooterIcon: GetIcon(), + Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + } + + msg := slack.WebhookMessage{ + Attachments: []slack.Attachment{attachment}, + } + err := slack.PostWebhook(options.Noti.SlackWebHook, &msg) + if err != nil { + return err + } + return nil +} + +// SendFile send file to specific channel +func SendFile(filename string, channel string, options libs.Options) error { + if options.Noti.SlackToken == "" || options.Noti.SlackReportChannel == "" { + return fmt.Errorf("Slack config improperly") + } + + if !utils.FileExists(filename) { + return fmt.Errorf("report file not found: %v", filename) + } + + baseName := filepath.Base(filename) + mess := fmt.Sprintf("%v - %v - Report file for *%v* on *%v*", GetEmoji(), baseName, options.Module.Name, options.Scan.ROptions["Workspace"]) + utils.DebugF("Sending %v message to %v", filename, channel) + + // sending file + api := slack.New(options.Noti.SlackToken) + params := slack.FileUploadParameters{ + Channels: []string{channel}, + Title: mess, + Filetype: "txt", + File: filename, + } + _, err := api.UploadFile(params) + if err != nil { + return err + } + return nil +} + +// TeleSendMess send message to telegram +func TeleSendMess(options libs.Options, content string, channel string, wrap bool) error { + + if options.NoNoti { + return fmt.Errorf("noti disabled") + } + bot, err := tgbotapi.NewBotAPI(options.Noti.TelegramToken) + content = tgbotapi.EscapeText(tgbotapi.ModeMarkdown, content) + if wrap { + content = fmt.Sprintf("```\n%s\n```", content) + } + if err != nil { + utils.DebugF("error init telegram: %v", err) + return err + } + + if channel == "" || channel == "general" { + channel = options.Noti.TelegramChannel + } + switch channel { + case "#status": + channel = options.Noti.TelegramStatusChannel + case "#r", "#report", "#reports", "#vuln": + channel = options.Noti.TelegramReportChannel + case "#s", "#sensitive", "#sen": + channel = options.Noti.TelegramSensitiveChannel + case "#dirb", "#dirscan": + channel = options.Noti.TelegramDirbChannel + case "#m", "#mics": + channel = options.Noti.TelegramMicsChannel + case "#default", "#general": + channel = options.Noti.TelegramChannel + } + telechannel := cast.ToInt64(channel) + utils.DebugF("send message to channel %v", channel) + msg := tgbotapi.NewMessage(telechannel, content) + msg.ParseMode = "markdown" + _, err = bot.Send(msg) + if err != nil { + utils.DebugF("error sending telegram to %v -- %v", channel, err) + } + return err +} + +// TeleSendFile send message to telegram +func TeleSendFile(options libs.Options, filename string, channel string) error { + if options.NoNoti { + return fmt.Errorf("noti disabled") + } + bot, err := tgbotapi.NewBotAPI(options.Noti.TelegramToken) + if err != nil { + utils.DebugF("error init telegram: %v", err) + return err + } + + if channel == "" || channel == "general" { + channel = options.Noti.TelegramChannel + } + switch channel { + case "#status": + channel = options.Noti.TelegramStatusChannel + case "#r", "#report", "#reports", "#vuln": + channel = options.Noti.TelegramReportChannel + case "#sensitive", "#sen": + channel = options.Noti.TelegramSensitiveChannel + case "#dirb", "#dirscan": + channel = options.Noti.TelegramDirbChannel + case "#m", "#mics": + channel = options.Noti.TelegramMicsChannel + case "#default": + channel = options.Noti.TelegramChannel + } + telechannel := cast.ToInt64(channel) + + filename = utils.NormalizePath(filename) + + msg := tgbotapi.NewDocument(telechannel, tgbotapi.FilePath(filename)) + utils.DebugF("send file %v to channel %v", filename, channel) + _, err = bot.Send(msg) + if err != nil { + utils.DebugF("error sending telegram to %v -- %v", channel, err) + } + return err +} + +/////// utils for slack message + +// GetEmoji get random emoji +func GetEmoji() string { + rand.Seed(time.Now().Unix()) + emojis := []string{ + ":robot_face:", + ":alien:", + ":gift:", + ":gun:", + ":diamond_shape_with_a_dot_inside:", + ":rocket:", + ":bug:", + ":broccoli:", + ":shamrock:", + } + n := rand.Int() % len(emojis) + return emojis[n] +} + +// GetIcon get random emoji +func GetIcon() string { + rand.Seed(time.Now().Unix()) + emojis := []string{ + "https://platform.slack-edge.com/img/default_application_icon.png", + } + n := rand.Int() % len(emojis) + return emojis[n] +} diff --git a/execution/noti_test.go b/execution/noti_test.go new file mode 100644 index 0000000..034a5ca --- /dev/null +++ b/execution/noti_test.go @@ -0,0 +1,44 @@ +package execution + +import ( + "github.com/j3ssie/osmedeus/libs" + "testing" +) + +func TestSlackWebHook(t *testing.T) { + + content := ` +[jira-subversion-xss][Tentative-Medium] - https://142.104.128.133:443/plugins/servlet/svnwebclient/error.jsp?errormessage=%27%22%3E%3Cscript%3Ealert(document.domain)%3C%2Fscript%3E&description=test - out/142.104.128.133/jira-subversion-xss-b38aaad1bc0567262e48e374e902bda60d522f94\n +[jira-subversion-xss][Tentative-Medium] - http://58.49.154.201:8080/plugins/servlet/svnwebclient/error.jsp?errormessage=%27%22%3E%3Cscript%3Ealert(document.domain)%3C%2Fscript%3E&description=test - out/58.49.154.201/jira-subversion-xss-2e18d80689f97485af742d8c0368ffd2cdc78d7a +` + err := SlackWebHook("https://hooks.slack.com/services/T01D9M3RSLA/B01CUUVD98X/xxxxxxxx", content) + if err != nil { + t.Error(err) + } +} + +func TestTeleSendMess(t *testing.T) { + var opt libs.Options + opt.Noti.TelegramChannel = "-1001166523435" + opt.Noti.TelegramToken = "1288534500:xxxxxx" + + content := ` +[jira-subversion-xss][Tentative-Medium] - https://142.104.128.133:443/plugins/servlet/svnwebclient/error.jsp?errormessage=%27%22%3E%3Cscript%3Ealert(document.domain)%3C%2Fscript%3E&description=test - out/142.104.128.133/jira-subversion-xss-b38aaad1bc0567262e48e374e902bda60d522f94\n +[jira-subversion-xss][Tentative-Medium] - http://58.49.154.201:8080/plugins/servlet/svnwebclient/error.jsp?errormessage=%27%22%3E%3Cscript%3Ealert(document.domain)%3C%2Fscript%3E&description=test - out/58.49.154.201/jira-subversion-xss-2e18d80689f97485af742d8c0368ffd2cdc78d7a +` + err := TeleSendMess(opt, content, "general", true) + if err != nil { + t.Error(err) + } +} + +func TestTeleSendFile(t *testing.T) { + var opt libs.Options + opt.Noti.TelegramChannel = "-1001166523435" + opt.Noti.TelegramToken = "1288534500:xxxx" + + err := TeleSendFile(opt, "/tmp/jtt/out/jaeles-summary.txt", "general") + if err != nil { + t.Error(err) + } +} diff --git a/execution/process.go b/execution/process.go new file mode 100644 index 0000000..5d6d879 --- /dev/null +++ b/execution/process.go @@ -0,0 +1,93 @@ +package execution + +import ( + "fmt" + "strings" + "sync" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + gops "github.com/mitchellh/go-ps" + "github.com/panjf2000/ants" + "github.com/shirou/gopsutil/process" + "github.com/spf13/cast" +) + +func ListAllOsmedeusProcess() (pids []int) { + processes, err := gops.Processes() + if err != nil { + return pids + } + var allProcess []OSProcess + + var wg sync.WaitGroup + p, _ := ants.NewPoolWithFunc(20, func(i interface{}) { + defer wg.Done() + + ps := i.(gops.Process) + pid := ps.Pid() + proc, _ := process.NewProcess(cast.ToInt32(pid)) + cmd, _ := proc.Cmdline() + if !strings.Contains(cmd, libs.BINARY) { + return + } + + osProcess := OSProcess{ + PID: pid, + Command: cmd, + } + fmt.Printf("pid:%v %s %v\n", color.HiCyanString("%v", osProcess.PID), color.HiMagentaString("--"), osProcess.Command) + allProcess = append(allProcess, osProcess) + pids = append(pids, osProcess.PID) + }, ants.WithPreAlloc(true)) + defer p.Release() + + for _, ps := range processes { + wg.Add(1) + _ = p.Invoke(ps) + + } + wg.Wait() + + return pids +} + +type OSProcess struct { + PID int `json:"pid"` + Command string `json:"command"` +} + +func GetOsmProcess(processName string) []OSProcess { + if processName == "" { + processName = libs.BINARY + } + var results []OSProcess + processes, err := gops.Processes() + if err != nil { + return results + } + + for _, ps := range processes { + pid := ps.Pid() + binary := ps.Executable() + + if strings.ToLower(binary) != strings.ToLower(processName) { + continue + } + + proc, _ := process.NewProcess(cast.ToInt32(pid)) + cmd, _ := proc.Cmdline() + + if strings.Contains(cmd, fmt.Sprintf("%s utils ps", libs.BINARY)) { + continue + } + + osmProcess := OSProcess{ + PID: pid, + Command: cmd, + } + results = append(results, osmProcess) + } + + return results +} diff --git a/execution/process_test.go b/execution/process_test.go new file mode 100644 index 0000000..4b7b469 --- /dev/null +++ b/execution/process_test.go @@ -0,0 +1,9 @@ +package execution + +import ( + "testing" +) + +func TestListProcess(t *testing.T) { + ListAllOsmedeusProcess() +} diff --git a/execution/remote.go b/execution/remote.go new file mode 100644 index 0000000..d761fbc --- /dev/null +++ b/execution/remote.go @@ -0,0 +1,126 @@ +package execution + +import ( + "fmt" + "net/url" + "path" + "strings" + + "github.com/Jeffail/gabs/v2" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/parnurzeal/gorequest" +) + +var remoteOptions libs.Options + +// RemoteLogin get to get JWT to run +func RemoteLogin(username string, password string, URL string, options libs.Options) { + u, err := url.Parse(URL) + if err != nil { + return + } + URL = fmt.Sprintf("%v://%v:%v", u.Scheme, u.Hostname(), u.Port()) + + request := gorequest.New() + _, body, _ := request.Post(fmt.Sprintf("%v/auth/login", URL)). + Set("Content-Type", "application/json"). + Send(fmt.Sprintf(`{"username":"%v", "password":"%v"}`, username, password)). + End() + + jsonParsed, _ := gabs.ParseJSON([]byte(body)) + + token := strings.Trim(jsonParsed.S("token").String(), `"`) + if token == "null" { + utils.WarnF("Login fail at %v", URL) + } + options.Client.JWT = fmt.Sprintf("Osmedeus %v", token) + options.Client.URL = URL + utils.DebugF("Got token: %v at %v", options.Client.JWT, options.Client.URL) + remoteOptions = options +} + +// RemoteUpload upload data to remote url +func RemoteUpload(src string, options libs.Options) { + if options.Client.JWT == "" || options.Client.URL == "" { + utils.WarnF("JWT Token was not set") + return + } + // keep \n not escape from JSON body + data := strings.Join(utils.ReadingFileUnique(utils.NormalizePath(src)), "\\n") + filename := path.Base(src) + jsonBody := fmt.Sprintf(`{"data":"%v", "filename":"%v"}`, data, filename) + + request := gorequest.New() + _, body, _ := request.Post(fmt.Sprintf("%v/api/upload/data", options.Client.URL)). + Set("Content-Type", "application/json"). + Set("Authorization", options.Client.JWT). + Send(jsonBody). + End() + // sample --> /tmp/data-osm-sample + jsonParsed, _ := gabs.ParseJSON([]byte(body)) + remoteFile := strings.Trim(jsonParsed.S("content").String(), `"`) + + if remoteFile == "null" { + utils.WarnF("Fail to Upload %v at %v", src, options.Client.URL) + } + utils.DebugF("Sucessfully uploaded: %v", remoteFile) +} + +// RemoteExec run command on a remote client +func RemoteExec(command string, options libs.Options) { + if options.Client.JWT == "" || options.Client.URL == "" { + utils.WarnF("JWT Token was not set") + return + } + + // default master password is blank now + mpassword := "" + request := gorequest.New() + _, body, _ := request.Post(fmt.Sprintf("%v/api/task/new", options.Client.URL)). + Set("Content-Type", "application/json"). + Set("Authorization", options.Client.JWT). + Send(fmt.Sprintf(`{"command": "%v","password": "%v"}`, command, mpassword)). + End() + + jsonParsed, _ := gabs.ParseJSON([]byte(body)) + message := jsonParsed.S("content").String() + if message == "null" { + utils.WarnF("Run Command Fail at %v", options.Client.URL) + } + utils.DebugF("Sucessfully run Command: %v", command) + +} + +// RemoteExecSchedule run command on a remote client with schedule +func RemoteExecSchedule(command string, schedule string, options libs.Options) { + if options.Client.JWT == "" || options.Client.URL == "" { + utils.WarnF("JWT Token was not set") + return + } + + // default master password is blank now + mpassword := "" + request := gorequest.New() + _, body, _ := request.Post(fmt.Sprintf("%v/api/task/new", options.Client.URL)). + Set("Content-Type", "application/json"). + Set("Authorization", options.Client.JWT). + Send(fmt.Sprintf(`{"command":"%v","seconds":%v,"password":"%v"}`, command, schedule, mpassword)). + End() + + jsonParsed, _ := gabs.ParseJSON([]byte(body)) + message := jsonParsed.S("content").String() + if message == "null" { + utils.WarnF("Run Schedule Command Fail at %v", options.Client.URL) + } + utils.DebugF("Sucessfully run Schedule Command: %v", command) + + /* + { + "name": "example", + "command": "id", + "minutes": 1, + "password": "321" + } + */ +} diff --git a/execution/request.go b/execution/request.go new file mode 100644 index 0000000..8d6670b --- /dev/null +++ b/execution/request.go @@ -0,0 +1,227 @@ +package execution + +import ( + "bufio" + "crypto/tls" + "encoding/base64" + "fmt" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/go-resty/resty/v2" + "github.com/sirupsen/logrus" +) + +func SendGET(token string, url string) (libs.Response, error) { + client := BuildClient(token, 2) + res, err := JustSend(url, client) + return res, err +} + +// BuildClient build base HTTP client +func BuildClient(token string, retry int) *resty.Client { + headers := map[string]string{ + "UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36", + "Content-Type": "application/json", + } + if token != "" { + headers["Authorization"] = fmt.Sprintf("Bearer %s", token) + } + + timeout := 15 + + //if len(options.Headers) > 0 { + // for _, head := range options.Headers { + // if strings.Contains(head, ":") { + // data := strings.Split(head, ":") + // if len(data) < 2 { + // continue + // } + // headers[data[0]] = strings.Join(data[1:], "") + // } + // } + //} + + // disable log when retry + logger := logrus.New() + logger.Out = io.Discard + + client := resty.New() + client.SetLogger(logger) + client.SetTransport(&http.Transport{ + MaxIdleConns: 100, + MaxConnsPerHost: 1000, + IdleConnTimeout: time.Duration(timeout) * time.Second, + ExpectContinueTimeout: time.Duration(timeout) * time.Second, + ResponseHeaderTimeout: time.Duration(timeout) * time.Second, + TLSHandshakeTimeout: time.Duration(timeout) * time.Second, + DisableCompression: true, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }) + + client.SetHeaders(headers) + client.SetCloseConnection(true) + client.SetRetryCount(retry) + client.SetTimeout(time.Duration(timeout) * time.Second) + client.SetRetryWaitTime(time.Duration(timeout/2) * time.Second) + client.SetRetryMaxWaitTime(time.Duration(timeout) * time.Second) + return client +} + +// JustSend just sending request +func JustSend(url string, client *resty.Client) (res libs.Response, err error) { + method := "GET" + // redirect policy + + var resp *resty.Response + // really sending things here + method = strings.ToLower(strings.TrimSpace(method)) + switch method { + case "get": + resp, err = client.R(). + Get(url) + break + case "post": + resp, err = client.R(). + Post(url) + break + } + + // in case we want to get redirect stuff + if res.StatusCode != 0 { + return res, nil + } + + if err != nil || resp == nil { + utils.WarnF("%v %v", url, err) + return libs.Response{}, err + } + + return ParseResponse(*resp), nil +} + +// ParseResponse field to Response +func ParseResponse(resp resty.Response) (res libs.Response) { + // var res libs.Response + resLength := len(string(resp.Body())) + // format the headers + var resHeaders []map[string]string + for k, v := range resp.RawResponse.Header { + if k == "Content-Type" { + res.ContentType = strings.Join(v[:], "") + } + if k == "Location" { + res.Location = strings.Join(v[:], "") + } + element := make(map[string]string) + element[k] = strings.Join(v[:], "") + resLength += len(fmt.Sprintf("%s: %s\n", k, strings.Join(v[:], ""))) + resHeaders = append(resHeaders, element) + } + // response time in second + resTime := float64(resp.Time()) / float64(time.Second) + resHeaders = append(resHeaders, + map[string]string{"Total Length": strconv.Itoa(resLength)}, + map[string]string{"Response Time": fmt.Sprintf("%f", resTime)}, + ) + + // set some variable + res.Headers = resHeaders + res.StatusCode = resp.StatusCode() + res.Status = fmt.Sprintf("%v %v", resp.Status(), resp.RawResponse.Proto) + res.Body = string(resp.Body()) + res.ResponseTime = resTime + res.Length = resLength + // beautify + res.Beautify = BeautifyResponse(res) + res.BeautifyHeader = BeautifyHeaders(res) + return res +} + +// BeautifyRequest beautify request +func BeautifyRequest(req libs.Request) string { + var beautifyReq string + // hardcoded HTTP/1.1 for now + beautifyReq += fmt.Sprintf("%v %v HTTP/1.1\n", req.Method, req.URL) + + for _, header := range req.Headers { + for key, value := range header { + if key != "" && value != "" { + beautifyReq += fmt.Sprintf("%v: %v\n", key, value) + } + } + } + if req.Body != "" { + beautifyReq += fmt.Sprintf("\n%v\n", req.Body) + } + return beautifyReq +} + +// BeautifyHeaders beautify headers +func BeautifyHeaders(res libs.Response) string { + beautifyHeader := fmt.Sprintf("< %v \n", res.Status) + for _, header := range res.Headers { + for key, value := range header { + beautifyHeader += fmt.Sprintf("< %v: %v\n", key, value) + } + } + return beautifyHeader +} + +// BeautifyResponse beautify response +func BeautifyResponse(res libs.Response) string { + var beautifyRes string + beautifyRes += fmt.Sprintf("%v \n", res.Status) + + for _, header := range res.Headers { + for key, value := range header { + beautifyRes += fmt.Sprintf("%v: %v\n", key, value) + } + } + + beautifyRes += fmt.Sprintf("\n%v\n", res.Body) + return beautifyRes +} + +// ParseBurpRequest parse burp style request +func ParseBurpRequest(raw string) string { + rawDecoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return "" + } + + var realReq libs.Request + reader := bufio.NewReader(strings.NewReader(string(rawDecoded))) + parsedReq, err := http.ReadRequest(reader) + if err != nil { + return "" + } + realReq.Method = parsedReq.Method + // URL part + if parsedReq.URL.Host == "" { + realReq.Host = parsedReq.Host + parsedReq.URL.Host = parsedReq.Host + } + if parsedReq.URL.Scheme == "" { + if parsedReq.Referer() == "" { + realReq.Scheme = "https" + parsedReq.URL.Scheme = "https" + } else { + u, err := url.Parse(parsedReq.Referer()) + if err == nil { + realReq.Scheme = u.Scheme + parsedReq.URL.Scheme = u.Scheme + } + } + } + realReq.URL = parsedReq.URL.String() + realReq.Path = parsedReq.RequestURI + + return realReq.URL +} diff --git a/execution/require.go b/execution/require.go new file mode 100644 index 0000000..be9f9a1 --- /dev/null +++ b/execution/require.go @@ -0,0 +1,106 @@ +package execution + +// +//// RunRequire is main function for generator +//func RunRequire(script string, options libs.Options) bool { +// // @NOTE: for some reason < auto translate to < in golang template +// if strings.Contains(script, "<") { +// script = strings.Replace(script, "<", "<", -1) +// } +// utils.DebugF("[Run-Require] %v", script) +// vm := otto.New() +// +// vm.Set("EmptyDir", func(call otto.FunctionCall) otto.Value { +// result, _ := vm.ToValue(utils.EmptyDir(call.Argument(0).String())) +// return result +// }) +// +// vm.Set("NotEmptyDir", func(call otto.FunctionCall) otto.Value { +// result, _ := vm.ToValue(!utils.EmptyDir(call.Argument(0).String())) +// return result +// }) +// +// vm.Set("NotEmptyFile", func(call otto.FunctionCall) otto.Value { +// result, _ := vm.ToValue(!utils.EmptyFile(call.Argument(0).String(), 0)) +// if len(call.ArgumentList) > 1 { +// num, _ := call.Argument(0).ToInteger() +// result, _ = vm.ToValue(!utils.EmptyFile(call.Argument(0).String(), int(num))) +// } +// return result +// }) +// +// vm.Set("EmptyFile", func(call otto.FunctionCall) otto.Value { +// result, _ := vm.ToValue(utils.EmptyFile(call.Argument(0).String(), 0)) +// if len(call.ArgumentList) > 1 { +// num, _ := call.Argument(0).ToInteger() +// result, _ = vm.ToValue(utils.EmptyFile(call.Argument(0).String(), int(num))) +// } +// return result +// }) +// +// vm.Set("ExecContain", func(call otto.FunctionCall) otto.Value { +// var validate bool +// args := call.ArgumentList +// cmd := args[0].String() +// search := args[1].String() +// out, _ := Execution(cmd, options) +// if strings.Contains(out, search) { +// validate = true +// } +// result, _ := vm.ToValue(validate) +// return result +// }) +// +// vm.Set("ExecMatch", func(call otto.FunctionCall) otto.Value { +// var validate bool +// args := call.ArgumentList +// cmd := args[0].String() +// search := args[1].String() +// out, _ := Execution(cmd, options) +// validate = RegexCount(out, search) +// result, _ := vm.ToValue(validate) +// return result +// }) +// +// vm.Set("DirLength", func(call otto.FunctionCall) otto.Value { +// validate := utils.DirLength(call.Argument(0).String()) +// result, err := vm.ToValue(validate) +// if err != nil { +// return otto.FalseValue() +// } +// return result +// }) +// +// vm.Set("FileLength", func(call otto.FunctionCall) otto.Value { +// validate := utils.FileLength(call.Argument(0).String()) +// result, err := vm.ToValue(validate) +// if err != nil { +// return otto.FalseValue() +// } +// return result +// }) +// +// result, serr := vm.Run(script) +// if serr != nil { +// return false +// } +// analyzeResult, err := result.Export() +// if err != nil || analyzeResult == nil { +// return false +// } +// utils.DebugF("Required: %v -- %v", script, result) +// return analyzeResult.(bool) +//} +// +//// RegexCount count regex string in component +//func RegexCount(component string, analyzeString string) bool { +// r, err := regexp.Compile(analyzeString) +// if err != nil { +// return false +// } +// matches := r.FindAllStringIndex(component, -1) +// if len(matches) > 0 { +// return true +// } +// return false +//} diff --git a/execution/s3_cdn.go b/execution/s3_cdn.go new file mode 100644 index 0000000..57d89b5 --- /dev/null +++ b/execution/s3_cdn.go @@ -0,0 +1,125 @@ +package execution + +import ( + "fmt" + "io" + "os" + "path" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +// UploadToS3 upload local file to s3 bucket +func UploadToS3(options libs.Options, source string, bucket string) { + if options.Cdn.AccessKeyId == "CDN_AWS_ACCESS_KEY" { + return + } + utils.InforF("Uploading %s to %s", color.HiCyanString(source), color.HiCyanString(bucket)) + // Set up a new session and get a reference to the S3 service. + sess, err := session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(options.Cdn.AccessKeyId, options.Cdn.SecretKey, ""), + Region: aws.String(options.Cdn.Region)}, + ) + if err != nil { + utils.ErrorF("error creating session: %s", err) + return + } + svc := s3.New(sess) + + if !utils.FileExists(source) { + utils.ErrorF("File not found: %s", source) + return + } + + // Open the file that you want to upload. + file, err := os.Open(source) + if err != nil { + utils.ErrorF("error opening file: %s", err) + return + } + defer file.Close() + + // Set up the parameters for the upload. + params := &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(source), + Body: file, + } + + // Upload the file to S3. + _, err = svc.PutObject(params) + if err != nil { + utils.ErrorF("error uploading file: %s", err) + return + } +} + +// DownloadFromS3 upload local file to s3 bucket +func DownloadFromS3(options libs.Options, source string, dest string, bucket string) { + if options.Cdn.AccessKeyId == "CDN_AWS_ACCESS_KEY" { + return + } + utils.InforF("Downloading %s from %s bucket to %s", color.HiCyanString(source), color.HiCyanString(bucket), color.HiBlueString(dest)) + // Set up a new session and get a reference to the S3 service. + sess, err := session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(options.Cdn.AccessKeyId, options.Cdn.SecretKey, ""), + Region: aws.String(options.Cdn.Region)}, + ) + if err != nil { + utils.ErrorF("error creating session: %s", err) + return + } + svc := s3.New(sess) + + // Set up the parameters for the download. + params := &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(source), + } + + // Download the file from S3. + result, err := svc.GetObject(params) + if err != nil { + utils.ErrorF("error downloading file: %s", err) + return + } + defer result.Body.Close() + + // Create a local file to store the downloaded data. + outFile, err := os.Create(dest) + if err != nil { + utils.ErrorF("error creating local file: %s", err) + return + } + defer outFile.Close() + + // Copy the data from the S3 object to the local file. + _, err = io.Copy(outFile, result.Body) + if err != nil { + utils.ErrorF("error copying file data: %s", err) + return + } +} + +// DownloadFile get file from CDN URL +func DownloadFile(options libs.Options, downloadURL string, dest string) { + utils.DebugF("Downloading: %s", downloadURL) + if !utils.FolderExists(path.Dir(dest)) { + utils.MakeDir(path.Dir(dest)) + } + + cmd := fmt.Sprintf("wget -qO %s %s", dest, downloadURL) + Execution(cmd, options) + + if utils.FileLength(dest) <= 0 { + os.RemoveAll(dest) + return + } + +} diff --git a/execution/scripts.go b/execution/scripts.go new file mode 100644 index 0000000..9b8501c --- /dev/null +++ b/execution/scripts.go @@ -0,0 +1,225 @@ +package execution + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "fmt" + "io" + "os" + "os/exec" + "path" + "regexp" + "sort" + "strings" + "time" + + "github.com/j3ssie/osmedeus/libs" + + "github.com/j3ssie/osmedeus/utils" +) + +// Execution Run a command +func Execution(cmd string, options libs.Options) (string, error) { + command := []string{ + "bash", + "-c", + cmd, + } + //var output string + utils.DebugF("[Exec] %v", command) + realCmd := exec.Command(command[0], command[1:]...) + if options.Quite == true { + realCmd.CombinedOutput() + } else { + // output command output to std too + cmdReader, _ := realCmd.StdoutPipe() + scanner := bufio.NewScanner(cmdReader) + //var out string + go func() { + for scanner.Scan() { + utils.InforF(scanner.Text()) + } + }() + if err := realCmd.Start(); err != nil { + return "", err + } + if err := realCmd.Wait(); err != nil { + return "", err + } + } + return "", nil +} + +// Echo just print testing +func Echo(script string) { + fmt.Println("testing:", script) +} + +// Sleep just print testing +func Sleep(raw string) { + time.Sleep(time.Second * time.Duration(utils.StrToInt(raw))) +} + +// Printf print out some string +func Printf(block string, content string) { + if block != "" { + utils.BlockF(block, content) + } else { + fmt.Println(content) + } +} + +// ErrPrintf print out some string +func ErrPrintf(block string, content string) { + if block != "" { + utils.BadBlockF(block, content) + } else { + fmt.Println(content) + } +} + +// Base just print testing +func Base(raw string) string { + raw = utils.NormalizePath(raw) + return path.Base(raw) +} + +// StripName strip a file name +func StripName(raw string) string { + result := strings.Replace(raw, "/", "_", -1) + result = strings.Replace(result, ":", "_", -1) + return strings.Trim(result, "_") +} + +// DeleteFile delete file +func DeleteFile(filename string) { + utils.DebugF("Delete: %v", filename) + os.Remove(utils.NormalizePath(filename)) +} + +// DeleteFolder delete entire folder +func DeleteFolder(path string) { + utils.DebugF("Delete: %v", path) + os.RemoveAll(utils.NormalizePath(path)) +} + +// Append append content to a file +func Append(dest string, src string) { + if !utils.FileExists(src) || utils.FileLength(src) <= 0 { + utils.DebugF("error to append %v", src) + return + } + data := utils.GetFileContent(src) + utils.AppendToContent(dest, data) +} + +// Copy append content to a file +func Copy(src string, dest string) { + if !utils.FileExists(src) || utils.FileLength(src) <= 0 { + return + } + input, _ := os.ReadFile(src) + os.WriteFile(dest, input, 0644) +} + +// Sort sort content of a file +func Sort(src string) { + data := utils.ReadingFileUnique(src) + if len(data) == 0 { + return + } + sort.Strings(data) + content := strings.Join(data, "\n") + // remove blank line + content = regexp.MustCompile(`[\t\r\n]+`).ReplaceAllString(strings.TrimSpace(content), "\n") + utils.WriteToFile(src, content) +} + +// SortU sort content of a file +func SortU(src string) { + if !utils.FileExists(src) { + utils.DebugF("File not found: %s", src) + } + cmd := fmt.Sprintf("LC_ALL=C sort -u -o %s %s", src, src) + utils.RunCmdWithOutput(cmd) +} + +// Unique unique content of a file and remove blank line +func Unique(filename string) { + data := utils.ReadingFileUnique(filename) + if len(data) > 0 { + content := strings.Join(data, "\n") + // remove blank line + content = regexp.MustCompile(`[\t\r\n]+`).ReplaceAllString(strings.TrimSpace(content), "\n") + utils.WriteToFile(filename, content) + } +} + +// Compress sort content of a file +func Compress(dest string, src string) { + if utils.FolderLength(src) < 1 { + utils.DebugF("Source folder is empty or not found: %s", src) + return + } + cmd := fmt.Sprintf("tar --use-compress-program='gzip -9' -C %s -cf %s .", strings.TrimRight(src, "/"), dest) + utils.RunCmdWithOutput(cmd) +} + +// Decompress sort content of a file +func Decompress(dest string, src string) { + if !utils.FileExists(src) { + utils.DebugF("File not found: %s", src) + return + } + utils.MakeDir(dest) + cmd := fmt.Sprintf("tar -xf %s -C %s", src, dest) + utils.RunCmdWithOutput(cmd) +} + +func ExtractTarGz(filename string) error { + r, err := os.Open(utils.NormalizePath(filename)) + if err != nil { + return err + } + + uncompressedStream, err := gzip.NewReader(r) + if err != nil { + return err + } + + tarReader := tar.NewReader(uncompressedStream) + for true { + header, err := tarReader.Next() + if err == io.EOF { + break + } + + if err != nil { + return err + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.Mkdir(header.Name, 0755); err != nil { + return err + } + case tar.TypeReg: + outFile, err := os.Create(header.Name) + if err != nil { + return err + } + if _, err := io.Copy(outFile, tarReader); err != nil { + return err + } + outFile.Close() + + default: + return err + + } + + } + + return nil +} diff --git a/execution/scripts_test.go b/execution/scripts_test.go new file mode 100644 index 0000000..8a86937 --- /dev/null +++ b/execution/scripts_test.go @@ -0,0 +1,19 @@ +package execution + +import ( + "testing" +) + +func TestChunkFile(t *testing.T) { + result := ChunkFileByPart("/tmp/oo/seqtest", 3) + t.Log(result) +} + +func TestSort(t *testing.T) { + Sort("/tmp/sam") +} + +func TestIsWildCard(t *testing.T) { + IsWildCard("github.com") + IsWildCard("tesla.com") +} diff --git a/execution/split.go b/execution/split.go new file mode 100644 index 0000000..ee58598 --- /dev/null +++ b/execution/split.go @@ -0,0 +1,126 @@ +package execution + +import ( + "fmt" + "github.com/j3ssie/osmedeus/utils" + "github.com/robertkrimen/otto" + "os" + "path" + "path/filepath" + "strings" +) + +/* + + SplitFile("src", "dest") + SplitFile("src", "base/dest", 500, "another/dest") + next step should be loop in file base/index + +*/ + +// SplitFile split file into multiple file +func SplitFile(kind string, arguments []otto.Value) { + source := arguments[0].String() + dest := arguments[1].String() + // prefix name of the output file + prefix := fmt.Sprintf("%v-chunked", filepath.Base(source)) + destDir := filepath.Dir(utils.NormalizePath(dest)) + if len(arguments) >= 4 { + destDir = arguments[3].String() + } + utils.MakeDir(destDir) + + chunk := 200 + // check if we change the size of it + if len(arguments) >= 3 { + chunkSize, _ := arguments[2].ToInteger() + chunk = int(chunkSize) + } + + chunkParts := chunk + // get number of part + if kind == "size" { + length := utils.FileLength(source) + chunkParts = length / chunk + } + + utils.DebugF("Splitting %v to %v %v", source, chunkParts, kind) + rawChunks, err := utils.SplitLineChunks(source, chunkParts) + if err != nil || len(rawChunks) == 0 { + utils.WarnF("error to split input file: %v", source) + return + } + fp, err := os.Open(source) + if err != nil { + utils.WarnF("error to open input file: %v", source) + return + } + + var sumFile []string + for index, offset := range rawChunks { + targetName := path.Join(destDir, fmt.Sprintf("%v-%v", prefix, index)) + reader := utils.NewRangeReader(fp, offset.Start, offset.Stop) + body := make([]byte, offset.Stop-offset.Start+1) + _, err := reader.Read(body) + if err != nil { + utils.WarnF("error to read chunk file: %s", err) + continue + } + sumFile = append(sumFile, targetName) + utils.DebugF("writing %v part to: %v", index, targetName) + utils.WriteToFile(targetName, string(body)) + } + + // write summary file + indexFile := path.Join(destDir, dest) + _, err = utils.WriteToFile(indexFile, strings.Join(sumFile, "\n")) + if err != nil { + utils.WarnF("Error writing to %v", indexFile) + } +} + +// ChunkFileByPart chunk file to multiple part +func ChunkFileByPart(source string, chunk int) [][]string { + var divided [][]string + data := utils.ReadingLines(source) + if len(data) <= 0 || chunk > len(data) { + if len(data) > 0 { + divided = append(divided, data) + } + return divided + } + + chunkSize := (len(data) + chunk - 1) / chunk + for i := 0; i < len(data); i += chunkSize { + end := i + chunkSize + if end > len(data) { + end = len(data) + } + + divided = append(divided, data[i:end]) + } + return divided +} + +// ChunkFileBySize chunk file to multiple part +func ChunkFileBySize(source string, chunk int) [][]string { + var divided [][]string + data := utils.ReadingLines(source) + if len(data) <= 0 || chunk > len(data) { + if len(data) > 0 { + divided = append(divided, data) + } + return divided + } + + chunkSize := chunk + for i := 0; i < len(data); i += chunkSize { + end := i + chunkSize + if end > len(data) { + end = len(data) + } + + divided = append(divided, data[i:end]) + } + return divided +} diff --git a/execution/wildcard.go b/execution/wildcard.go new file mode 100644 index 0000000..9573c3c --- /dev/null +++ b/execution/wildcard.go @@ -0,0 +1,61 @@ +package execution + +import ( + "fmt" + + //"github.com/OWASP/Amass/v3/requests" + //amassresolvers "github.com/OWASP/Amass/v3/resolvers" + "github.com/j3ssie/osmedeus/utils" +) + +var defaultResolvers = []string{ + "1.1.1.1:53", // Cloudflare + "8.8.8.8:53", // Google + "64.6.64.6:53", // Verisign + "8.8.4.4:53", // Google Secondary +} + +// IsWildCard check if target is wildcard or not +func IsWildCard(domain string) bool { + //var resolvers []string + //resolvers = defaultResolvers + //resolverPool := amassresolvers.SetupResolverPool(resolvers, 1000, false, nil) + //if resolverPool == nil { + // utils.WarnF("Failed to init DNS pool") + // return false + //} + // + //ctx := context.Background() + //defer ctx.Done() + //subdomains := genSubs(domain, 5) + //var totalWildCard int + //for _, subdomain := range subdomains { + // req := &requests.DNSRequest{ + // Name: subdomain, + // Domain: domain, + // } + // if !resolverPool.MatchesWildcard(ctx, req) { + // utils.DebugF("[wild] %s\n", req.Name) + // } else { + // totalWildCard += 1 + // utils.DebugF("[wild] %s\n", req.Name) + // } + //} + //utils.DebugF("Total number of wildcard: %v/%v\n", totalWildCard, len(subdomains)) + //if totalWildCard == len(subdomains) { + // utils.DebugF("Target %v is wildcard\n", domain) + // return true + //} + return false +} + +func genSubs(domain string, size int) []string { + var subdomains []string + subdomains = append(subdomains, fmt.Sprintf("notj3ssei.%s", domain)) + subdomains = append(subdomains, fmt.Sprintf("verylong.%s", domain)) + + for i := 0; i < (size - 2); i++ { + subdomains = append(subdomains, fmt.Sprintf("%s.%s", utils.RandomString(5), domain)) + } + return subdomains +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fb6c04f --- /dev/null +++ b/go.mod @@ -0,0 +1,113 @@ +module github.com/j3ssie/osmedeus + +go 1.20 + +require ( + github.com/Jeffail/gabs/v2 v2.7.0 + github.com/Shopify/yaml v2.1.0+incompatible + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 + github.com/aws/aws-sdk-go v1.44.327 + github.com/cenkalti/backoff/v4 v4.2.1 + github.com/davecgh/go-spew v1.1.1 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/digitalocean/godo v1.102.1 + github.com/fatih/color v1.15.0 + github.com/flosch/pongo2/v6 v6.0.0 + github.com/fsnotify/fsnotify v1.6.0 + github.com/go-playground/validator/v10 v10.15.1 + github.com/go-resty/resty/v2 v2.7.0 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/gofiber/fiber/v2 v2.48.0 + github.com/gofiber/jwt/v2 v2.2.7 + github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 + github.com/hashicorp/go-version v1.6.0 + github.com/jasonlvhit/gocron v0.0.1 + github.com/jinzhu/copier v0.4.0 + github.com/json-iterator/go v1.1.12 + github.com/kyokomi/emoji v2.2.4+incompatible + github.com/linode/linodego v1.20.1 + github.com/mackerelio/go-osstat v0.2.4 + github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/go-ps v1.0.0 + github.com/olekukonko/tablewriter v0.0.5 + github.com/panjf2000/ants v1.3.0 + github.com/parnurzeal/gorequest v0.2.16 + github.com/robertkrimen/otto v0.2.1 + github.com/shirou/gopsutil v3.21.11+incompatible + github.com/sirupsen/logrus v1.9.3 + github.com/slack-go/slack v0.12.2 + github.com/spf13/cast v1.5.1 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.16.0 + github.com/swaggo/swag v1.16.1 + github.com/thoas/go-funk v0.9.3 + github.com/x-cray/logrus-prefixed-formatter v0.5.2 + github.com/xanzy/go-gitlab v0.90.0 + golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 + golang.org/x/oauth2 v0.11.0 + golang.org/x/term v0.11.0 + golang.org/x/text v0.12.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/golang-jwt/jwt/v4 v4.0.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/smartystreets/goconvey v1.8.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.48.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/sourcemap.v1 v1.0.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + moul.io/http2curl v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cd51a50 --- /dev/null +++ b/go.sum @@ -0,0 +1,737 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg= +github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/yaml v2.1.0+incompatible h1:Y7Wj6eQo5Byeaxb2M9FiPdKBQ3ooLsRIEmUZMMxCI8g= +github.com/Shopify/yaml v2.1.0+incompatible/go.mod h1:Mrsv1G5Osez+VdHYcSI2zfaUzPu4lCdO9cW8R2lHPEc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/aws/aws-sdk-go v1.44.327 h1:ZS8oO4+7MOBLhkdwIhgtVeDzCeWOlTfKJS7EgggbIEY= +github.com/aws/aws-sdk-go v1.44.327/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/digitalocean/godo v1.102.1 h1:BrNePwIXjQWjOJXVTBqkURMjm70BRR0qXbRKfHNBF24= +github.com/digitalocean/godo v1.102.1/go.mod h1:SaUYccN7r+CO1QtsbXGypAsgobDrmSfVMJESEfXgoEg= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= +github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= +github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gofiber/fiber/v2 v2.17.0/go.mod h1:iftruuHGkRYGEXVISmdD7HTYWyfS2Bh+Dkfq4n/1Owg= +github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0= +github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8= +github.com/gofiber/jwt/v2 v2.2.7 h1:MgXZV+ak+FiRVepD3btHBxWcyxlFzTDGXJv78dU1sIE= +github.com/gofiber/jwt/v2 v2.2.7/go.mod h1:yaOHLccYXJidk1HX/EiIdIL+Z1xmY2wnIv6hgViw384= +github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= +github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU= +github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4= +github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/linode/linodego v1.20.1 h1:IW3SrZjRzrclYZnzFd80f8lSkTYAM7gVTJW0t7HnFKQ= +github.com/linode/linodego v1.20.1/go.mod h1:ggoWnJXssx9wPWNnR3x7WaOpOBOEhsPB/HO7iflF5qY= +github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs= +github.com/mackerelio/go-osstat v0.2.4/go.mod h1:Zy+qzGdZs3A9cuIqmgbJvwbmLQH9dJvtio5ZjJTbdlQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/panjf2000/ants v1.3.0 h1:8pQ+8leaLc9lys2viEEr8md0U4RN6uOSUCE9bOYjQ9M= +github.com/panjf2000/ants v1.3.0/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY= +github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ= +github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= +github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= +github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +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/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= +github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= +github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= +github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= +github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= +github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/xanzy/go-gitlab v0.90.0 h1:j8ZUHfLfXdnC+B8njeNaW/kM44c1zw8fiuNj7D+qQN8= +github.com/xanzy/go-gitlab v0.90.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/libs/cloud.go b/libs/cloud.go new file mode 100644 index 0000000..ce9ec54 --- /dev/null +++ b/libs/cloud.go @@ -0,0 +1,115 @@ +package libs + +// Cloud struct define folder to push data +type Cloud struct { + CheckingLimit bool + ReBuildBaseImage bool + IgnoreConfigFile bool + BackgroundRun bool + OnlyCreateDroplet bool + OnlyCreateInstance bool + EnablePrivateIP bool + + EnableSyncWorkflow bool + AddNewProvider bool + RemoteWorkflowFolder string + TokensFile string + + CopyWorkspaceToGit bool + ClearTime string + InstanceName string + TempTarget string + // content of secret key to avoid reading it too much + SecretKeyContent string + PublicKeyContent string + + // enable terraform + EnableTerraform bool + + // chunk options + ChunkInputs string + BaseWorkspace string + LocalSyncFolder string + DisableLocalSync bool + RemoteRunList bool + TargetAsFile bool + EnableChunk bool + IgnoreProcess bool + NumberOfParts int + Threads int + + // specific cloud instance resources + Size string + Region string + Token string + Provider string + IgnoreSetup bool + + // for pre-commands and post-commands + RemotePreRun []string + // run script on local machine after scan done + LocalSteps []Step `yaml:"local_steps"` + LocalPreRun []string + LocalPostRun []string + + // use to clone build-osm repo + SecretKey string + PublicKey string + BuildRepo string + Binary string + Retry int + UnzipResult bool + + // for health check + ForEverHealthCheck bool + NoDelete bool + + // raw command here + Extra string + Flow string + Module string + Workspace string + RawCommand string + Params []string + + WsSource string + WsDest string + + Input string + Inputs []string + InputsFile string + + Target map[string]string +} + +// Request all information about request +type Request struct { + Timeout int + Repeat int + Scheme string + Host string + Port string + Path string + URL string + Proxy string + Method string + Redirect bool + Headers []map[string]string + Body string + Beautify string +} + +// Response all information about response +type Response struct { + HasPopUp bool + StatusCode int + Status string + ContentType string + Headers []map[string]string + Body string + ResponseTime float64 + Length int + Beautify string + Location string + BeautifyHeader string +} diff --git a/libs/flow.go b/libs/flow.go new file mode 100644 index 0000000..762fc16 --- /dev/null +++ b/libs/flow.go @@ -0,0 +1,69 @@ +package libs + +// Routine for each scan +type Routine struct { + RoutineName string + FlowFolder string `yaml:"flow"` + Timeout string `yaml:"timeout"` + ParsedModules []Module + Modules []string +} + +// Flow struct to define specific field for a mode +type Flow struct { + NoDB bool `yaml:"nodb"` + SkipIndexed bool `yaml:"skip-indexed"` + ForceParams bool `yaml:"force-params"` + Input string + Validator string // domain, cidr, ip or domain-file, cidr-file and so on + + Name string + Type string + DefaultType string + Desc string + Usage string + + Params []map[string]string + Routines []Routine + + RemotePreRun []string `yaml:"remote_pre_run"` + // run script on local machine after scan done + LocalPreRun []string `yaml:"local_pre_run"` + LocalPostRun []string `yaml:"local_post_run"` +} + +// Module struct to define specific field for a module +type Module struct { + NoDB bool `yaml:"nodb"` + Validator string // domain, cidr, ip + ForceParams bool `yaml:"force-params"` + + // just for print some info + Name string + Desc string + Usage string + + // enable resume, if all reports file exist then skip the module + Resume bool + // run module despite resume enable + Forced bool + + MTimeout string `yaml:"mtimeout"` + Params []map[string]string + ModulePath string + + PreRun []string `yaml:"pre_run"` + Report struct { + Final []string + Noti []string + Diff []string + } + Steps []Step + PostRun []string `yaml:"post_run"` + + RemotePreRun []string `yaml:"remote_pre_run"` + // run script on local machine after scan done + LocalSteps []Step `yaml:"local_steps"` + LocalPreRun []string `yaml:"local_pre_run"` + LocalPostRun []string `yaml:"local_post_run"` +} diff --git a/libs/mics.go b/libs/mics.go new file mode 100644 index 0000000..af3fc70 --- /dev/null +++ b/libs/mics.go @@ -0,0 +1,61 @@ +package libs + +// Cdn credentials for other client +type Cdn struct { + Bucket string + Region string + SecretKey string + AccessKeyId string +} + +// Git credentials for other client +type Git struct { + BaseURL string + Username string + Password string + Token string + Group string + DefaultPrefix string + DefaultTag string + DefaultUser string + DefaultUID int + DeStorage string +} + +// TmuxOpt credentials for other client +type TmuxOpt struct { + ApplyAll bool + SelectedWindow string + Exclude string + Limit int +} + +// Cron credentials for other client +type Cron struct { + Command string + Schedule int + Forever bool +} + +// Remote credentials for other client +type Remote struct { + MasterHost string + MasterCred string + PoolHost string + PoolCred string +} + +// Sync credentials for other client +type Sync struct { + BaseURL string + Prefix string + Pool string +} + +// Client credentials for other client +type Client struct { + Username string + Password string + JWT string + URL string +} diff --git a/libs/noti.go b/libs/noti.go new file mode 100644 index 0000000..4ebba47 --- /dev/null +++ b/libs/noti.go @@ -0,0 +1,26 @@ +package libs + +// Notification struct define notification method +type Notification struct { + ClientName string + // SlacksWebHooks list + SlacksWebHooks map[string]string + // TelegramWebHooks list + TelegramWebHooks map[string]string + // Telegram part + TelegramToken string + TelegramChannel string + TelegramStatusChannel string + TelegramReportChannel string + TelegramDirbChannel string + TelegramSensitiveChannel string + TelegramMicsChannel string + // use this when we want to send a file to channel + SlackWebHook string + SlackToken string + SlackReportChannel string + SlackStatusChannel string + SlackDiffChannel string + // later then + DiscordToken string +} diff --git a/libs/options.go b/libs/options.go new file mode 100644 index 0000000..d5c082a --- /dev/null +++ b/libs/options.go @@ -0,0 +1,172 @@ +package libs + +// Options global options +type Options struct { + ConfigFile string + LogFile string + Concurrency int + + // default threads hold for running module + Tactics string + Threads int + + Timeout string + EnableFormatInput bool + Verbose bool + FullHelp bool + + // some disable options + NoPostRun bool + NoPreRun bool + NoNoti bool + NoBanner bool + NoGit bool + NoClean bool + NoDB bool + NoCdn bool + DisableValidateInput bool + + PremiumPackage bool + Resume bool + Quite bool + Force bool + WildCardCheck bool + Debug bool + EnableDeStorage bool + PID int + SyncTimes int + PollingTime int + MDCodeBlockLimit int + Exclude []string + Params []string + CustomGit bool + EnableBackup bool + JsonOutput bool + + Client Client + Queue Queue + Git Git + Sync Sync + Scan Scan + Server Server + Env Environment + Noti Notification + Flow Flow + Module Module + Tmux TmuxOpt + Cron Cron + Remote Remote + Cdn Cdn + Update Update + + ThreadsHold ThreadsHold + Cloud Cloud + Report Report + CloudConfigFile string + TokenConfigFile string + GitSync bool + + ScanID string + Storages map[string]string +} + +// Scan sub options for scan +type Scan struct { + ROptions map[string]string + Params []string + Input string + InputType string // domain, url, ip, cidr or domainList, urlList, ipList, cidrList + ParamsFile string + Inputs []string + InputList string + Modules []string + Flow string + + BaseWorkspace string + CustomWorkspace string + SuffixName string + Force bool + // this is true when calling from cloud scan + RemoteCall bool +} + +type ThreadsHold struct { + Default int + Aggressive int + Gently int +} + +// Report sub options for report +type Report struct { + CustomPreFix string + PublicIP string + ExtractFolder string + Static bool + Raw bool +} + +// Server sub options for api server +type Server struct { + DisableWorkspaceListing bool + DisableSSL bool + PreFork bool + NoAuthen bool + + PollingTime int + Bind string + Port string + StaticPrefix string + JWTSecret string + Cors string + UIPath string + MasterPassword string + + // database + DBPath string + DBType string + DBConnection string + DBName string + DBUser string + DBPass string + DBHost string + DBPort string + + // for SSL + CertFile string + KeyFile string +} + +// Storage struct define folder to push data +type Storage struct { + SecretKey string + SummaryStorage string + SummaryRepo string + HTTPStorage string + HTTPRepo string + AssetsStorage string + AssetsRepo string +} + +// Environment some config path +type Environment struct { + RootFolder string // ~/.osmedeus + StoragesFolder string // ~/.osmedeus/storages/ + WorkspacesFolder string // ~/workspaces-osmedeus/ + + // Base one + BaseFolder string // ~/osmedeus-base + BinariesFolder string // ~/osmedeus-base/binaries + DataFolder string // ~/osmedeus-base/data/ + OseFolder string // ~/osmedeus-base/ose/ + WorkFlowsFolder string // ~/osmedeus-base/workflow/ + + // cloud stuff + CloudConfigFolder string // ~/osmedeus-base/clouds/ + ProviderFolder string // ~/.osmedeus/providers/ + InstancesFolder string // ~/.osmedeus/instances/ + BackupFolder string + + // Mics + ScriptsFolder string + UIFolder string +} diff --git a/libs/queue.go b/libs/queue.go new file mode 100644 index 0000000..62065ac --- /dev/null +++ b/libs/queue.go @@ -0,0 +1,22 @@ +package libs + +// Queue sub options for quque +type Queue struct { + QueueFolder string + QueueFile string + RawCommand string + + InputAsFile bool + Add bool +} + +type InputFormat struct { + Input string `json:"input"` + Flow string `json:"flow"` + Modules []string `json:"module"` + Params []string `json:"params"` + Workspaces string `json:"workspace"` + Extra string `json:"extra"` + Command string `json:"command"` + InputAsFile bool `json:"input-as-file"` +} diff --git a/libs/step.go b/libs/step.go new file mode 100644 index 0000000..8ccd353 --- /dev/null +++ b/libs/step.go @@ -0,0 +1,30 @@ +package libs + +// Step struct to define component about a command +type Step struct { + // timeout for commands and script + Timeout string + // use for run loop command + Parallel int + Threads string + Source string + + Label string + + Conditions []string + Required []string + + Commands []string + Ose []string `yaml:"ose"` + Scripts []string + + // run when conditions are false + RCommands []string `yaml:"rcommands"` + RScripts []string `yaml:"rscripts"` + + // post condition and script + PConditions []string + PScripts []string + + Std string +} diff --git a/libs/update.go b/libs/update.go new file mode 100644 index 0000000..41a7f27 --- /dev/null +++ b/libs/update.go @@ -0,0 +1,28 @@ +package libs + +// Update some config path +type Update struct { + UpdateURL string // url to download the update script + UpdateScript string + MetaDataURL string + UpdateKey string // + UpdateType string // git, http + UpdateConfig string // ~/.osmedeus/update + + UpdateVersion string + UpdateFolder string + UpdateDate string + CleanOldData bool + VulnUpdate bool + GenerateMeta string + ForceUpdate bool + IsUpdateBin bool + EnableUpdate bool + NoUpdate bool +} + +type UpdateMetaData struct { + WorkflowVersion string `json:"workflow_version"` + CoreVersion string `json:"core_version"` + UpdatedAt string `json:"updated_at"` +} diff --git a/libs/version.go b/libs/version.go new file mode 100644 index 0000000..7bb3e2c --- /dev/null +++ b/libs/version.go @@ -0,0 +1,26 @@ +package libs + +import "fmt" + +const ( + // VERSION of this project + VERSION = "v4.6.0" + // DESC description of the tool + DESC = "A Workflow Engine for Offensive Security" + // BINARY name of osmedeus + BINARY = "osmedeus" + // SNAPSHOT binary name of osmedeus + SNAPSHOT = "osm" + // AUTHOR of this + AUTHOR = "@j3ssiejjj" + // DOCS private document + DOCS = "https://docs.osmedeus.org" + // METADATA domain for checking update + METADATA = "https://metadata.osmedeus.org" + // INSTALL default install script + INSTALL = "https://raw.githubusercontent.com/osmedeus/osmedeus-base/master/install.sh" +) + +// TEMP default folder to store inputs +var TEMP = fmt.Sprintf("/tmp/%s-inputs/", SNAPSHOT) +var LDIR = fmt.Sprintf("/tmp/%s-log/", SNAPSHOT) diff --git a/main.go b/main.go new file mode 100644 index 0000000..710da72 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/j3ssie/osmedeus/cmd" + +func main() { + cmd.Execute() +} diff --git a/provider/action.go b/provider/action.go new file mode 100644 index 0000000..6c24e8a --- /dev/null +++ b/provider/action.go @@ -0,0 +1,201 @@ +package provider + +import ( + "fmt" + "strings" + + "github.com/cenkalti/backoff/v4" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cast" +) + +const ( + GetSSHKey = "get-sshkey" + RunBuild = "run-build" + ListImage = "list-image" + ListInstance = "list-instance" + GetInstanceInfo = "get-instance" + CreateInstance = "create-instance" + BootInstance = "boot-instance" +) + +func (p *Provider) GetSSHKey() (err error) { + if strings.TrimSpace(p.SSHPublicKey) == "" { + return fmt.Errorf("error getting SSHKey -- blank ssh public key") + } + + switch p.ProviderName { + case "do", "digitalocean": + err = p.GetSSHKeyDO() + case "ln", "line", "linode": + err = p.GetSSHKeyLN() + case "aw", "aws", "asw": + err = p.GetSSHKeyAWS() + default: + err = p.GetSSHKeyDO() + } + return err +} + +func (p *Provider) ListInstance() (err error) { + switch p.ProviderName { + case "do", "digitalocean": + err = p.ListInstanceDO() + case "ln", "line", "linode": + err = p.ListInstanceLN() + case "aw", "aws", "asw": + err = p.ListInstanceAWS() + default: + err = p.ListInstanceDO() + } + return err +} + +func (p *Provider) ListSnapShot() (err error) { + switch p.ProviderName { + case "do", "digitalocean": + err = p.ListSnapshotDO() + case "ln", "line", "linode": + err = p.ListSnapshotLN() + case "aw", "aws", "asw": + err = p.ListSnapshotAWS() + default: + err = p.ListSnapshotDO() + } + + if !p.IsBackgroundCheck { + utils.InforF("Found base image snapshot with ID: %v", color.HiCyanString(p.SnapshotID)) + } + return err +} + +func (p *Provider) CreateInstance(name string) (err error) { + var id interface{} + operation := func() error { + switch p.ProviderName { + case "do", "digitalocean": + id, err = p.CreateInstanceDO(name) + if err == nil { + err = p.Action(GetInstanceInfo, id) + } + case "ln", "line", "linode": + id, err = p.CreateInstanceLN(name) + if err == nil { + err = p.Action(BootInstance, id) + err = p.Action(GetInstanceInfo, id) + } + + case "aw", "aws", "asw": + id, err = p.CreateInstanceAWS(name) + if err == nil { + err = p.Action(BootInstance, id) + err = p.Action(GetInstanceInfo, id) + } + default: + id, err = p.CreateInstanceDO(name) + if err == nil { + err = p.Action(GetInstanceInfo, id) + } + } + return err + } + err = backoff.Retry(operation, p.BackOff) + if err != nil { + utils.WarnF("error create instance action %v -- %v", p.ProviderName, name) + return err + } + return nil +} + +// func (p *Provider) CreateInstanceF(name string) (err error) { +// var id int +// switch p.ProviderName { +// case "do", "digitalocean": +// id, err = p.CreateInstanceDO(name) +// if err == nil { +// err = p.Action(GetInstanceInfo, id) +// } +// case "ln", "line", "linode": +// id, err = p.CreateInstanceLN(name) +// if err == nil { +// err = p.Action(BootInstance, id) +// err = p.Action(GetInstanceInfo, id) +// } +// default: +// id, err = p.CreateInstanceDO(name) +// if err == nil { +// err = p.Action(GetInstanceInfo, id) +// } +// } +// return err +// } + +func (p *Provider) BootInstance(id interface{}) (err error) { + switch p.ProviderName { + case "do", "digitalocean": + case "ln", "line", "linode": + err = p.BootInstanceLN(cast.ToInt(id)) + case "aw", "aws", "asw": + // err = p.AllowRootAccessAWS(cast.ToString(id)) + default: + err = p.BootInstanceLN(cast.ToInt(id)) + } + if err != nil { + utils.WarnF("error booting instance: %v", id) + return err + } + return nil +} + +func (p *Provider) GetInstanceInfo(id interface{}) (err error) { + var instance Instance + switch p.ProviderName { + case "do", "digitalocean": + instance, err = p.InstanceInfoDO(cast.ToInt(id)) + case "ln", "line", "linode": + instance, err = p.InstanceInfoLN(cast.ToInt(id)) + case "aw", "aws", "asw": + instance, err = p.InstanceInfoAWS(cast.ToString(id)) + default: + instance, err = p.InstanceInfoDO(cast.ToInt(id)) + } + if err != nil { + utils.WarnF("error getting public IP of instance: %v", color.HiBlueString("%v", id)) + return err + } + p.Instances = append(p.Instances, instance) + return nil +} + +func (p *Provider) DeleteInstance(id string) (err error) { + utils.DebugF("[%v] Delete instance: %v", p.ProviderName, id) + switch p.ProviderName { + case "do", "digitalocean": + err = p.DeleteInstanceDO(id) + case "ln", "line", "linode": + err = p.DeleteInstanceLN(id) + case "aw", "aws", "asw": + err = p.DeleteInstanceAWS(id) + default: + err = p.DeleteInstanceDO(id) + } + return err +} + +func (p *Provider) DeleteOldSnapshot() (err error) { + for _, id := range p.OldSnapShotID { + switch p.ProviderName { + case "do", "digitalocean": + err = p.DeleteSnapShotDO(id) + case "ln", "line", "linode": + err = p.DeleteSnapShotLN(id) + case "aw", "aws", "asw": + err = p.DeleteImageAWS(id) + default: + err = p.DeleteSnapShotDO(id) + } + } + + return err +} diff --git a/provider/building.go b/provider/building.go new file mode 100644 index 0000000..9dbcf00 --- /dev/null +++ b/provider/building.go @@ -0,0 +1,157 @@ +package provider + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +func (p *Provider) PrePareBuildData() { + contentFile := path.Join(p.Opt.Env.CloudConfigFolder, fmt.Sprintf("providers/%s.provider", p.ProviderName)) + content := utils.GetFileContent(contentFile) + + data := make(map[string]string) + data["snapshot_name"] = p.SnapshotName + data["api_token"] = p.Token + // for aws only + data["access_key"] = p.AccessKeyId + data["secret_key"] = p.SecretKey + data["source_ami"] = p.ProviderConfig.DefaultImage + + // c.Cloud.ProviderFolder --> ~/.osmedeus/provider/-v4.x-randomstring + p.ProviderConfig.ProviderFolder = path.Join(p.Opt.Env.ProviderFolder, fmt.Sprintf("%s-%s", p.SnapshotName, utils.RandomString(6))) + utils.MakeDir(p.ProviderConfig.ProviderFolder) + data["ProviderFolder"] = p.ProviderConfig.ProviderFolder + + data["image"] = p.ProviderConfig.DefaultImage + data["size"] = p.ProviderConfig.Size + data["region"] = p.ProviderConfig.Region + data["TS"] = utils.GetTS() + + // generate packer content file to run + providerString := utils.RenderText(content, data) + data["Builder"] = providerString + + // ~/osmedeus-base + data["BaseFolder"] = utils.NormalizePath(strings.TrimLeft(p.Opt.Env.BaseFolder, "/")) + data["Plugins"] = p.Opt.Env.BinariesFolder + data["OBin"] = p.Opt.Env.BinariesFolder + data["Data"] = p.Opt.Env.DataFolder + data["Cloud"] = p.Opt.Env.CloudConfigFolder + data["Workflow"] = p.Opt.Env.WorkFlowsFolder + + // ~/.osmedeus/workspaces + data["Workspaces"] = p.Opt.Env.WorkspacesFolder + data["Binary"] = libs.BINARY + data["VERSION"] = libs.VERSION + data["BuildRepo"] = p.Opt.Cloud.BuildRepo + + // for terraform + data["ssh_public_key"] = p.Opt.Cloud.PublicKeyContent + data["root_password"] = fmt.Sprintf("%s-%s", libs.SNAPSHOT, utils.RandomString(8)) + + //spew.Dump("data --> ", data) + //spew.Dump("p.ProviderConfig --> ", p.ProviderConfig) + + p.ProviderConfig.BuildData = data +} + +func (p *Provider) BuildImage() (err error) { + if p.SnapshotFound && !p.Opt.Cloud.ReBuildBaseImage { + return nil + } + + p.PrePareBuildData() + p.DeleteOldSnapshot() + + // p.ProviderConfig.ProviderFolder --> ~/.osmedeus/provider/ + + utils.DebugF("Cleaning old provider build: %s", p.ProviderConfig.ProviderFolder) + os.RemoveAll(p.ProviderConfig.ProviderFolder) + utils.MakeDir(p.ProviderConfig.ProviderFolder) + + // generate provision process + setupContent := utils.GetFileContent(path.Join(p.Opt.Env.CloudConfigFolder, "setup.sh")) + setupContent = utils.RenderText(setupContent, p.ProviderConfig.BuildData) + setupFile := path.Join(p.ProviderConfig.ProviderFolder, "setup.sh") + utils.WriteToFile(setupFile, setupContent) + + // generate build file + var buildContent string + buildContentFile := path.Join(p.Opt.Env.CloudConfigFolder, "general-build.packer") + switch p.ProviderName { + case "do", "digitalocean": + buildContentFile = path.Join(p.Opt.Env.CloudConfigFolder, "digitalocean-build.packer") + if !utils.FileExists(buildContentFile) { + buildContentFile = path.Join(p.Opt.Env.CloudConfigFolder, "do-build.packer") + if !utils.FileExists(buildContentFile) { + buildContentFile = path.Join(p.Opt.Env.CloudConfigFolder, "general-build.packer") + } + } + + case "ln", "line", "linode": + buildContentFile = path.Join(p.Opt.Env.CloudConfigFolder, "linode-build.packer") + if !utils.FileExists(buildContentFile) { + buildContentFile = path.Join(p.Opt.Env.CloudConfigFolder, "ln-build.packer") + } + default: + buildContentFile = path.Join(p.Opt.Env.CloudConfigFolder, "general-build.packer") + } + + buildContent = utils.GetFileContent(buildContentFile) + if buildContent == "" { + errStr := fmt.Sprintf("Build file content not found at: %v", buildContentFile) + utils.ErrorF(errStr) + return fmt.Errorf(errStr) + } + + buildContent = utils.RenderText(buildContent, p.ProviderConfig.BuildData) + buildFile := path.Join(p.ProviderConfig.ProviderFolder, "build.json") + p.ProviderConfig.BuildFile = buildFile + utils.WriteToFile(buildFile, buildContent) + utils.InforF("Write build provision of %s to: %s", color.HiYellowString(p.ProviderName), color.HiCyanString(buildFile)) + + // actually run building + err = p.Action(RunBuild) + if err != nil { + p.SnapshotFound = false + return err + } + + err = p.Action(ListImage) + return err +} + +// RunBuild run the packer command +func (p *Provider) RunBuild() error { + packerBinary := fmt.Sprintf("%s/packer", p.Opt.Env.BinariesFolder) + if !utils.FileExists(packerBinary) { + packerBinary = "packer" + } + + cmd := fmt.Sprintf("%s validate %s", packerBinary, p.ProviderConfig.BuildFile) + out, err := utils.RunCommandWithErr(cmd) + if err != nil { + utils.ErrorF(out) + return err + } + utils.InforF("The Packer file appears to be functioning properly: %s", color.HiCyanString(p.ProviderConfig.BuildFile)) + + // really start to build stuff here + utils.GoodF("Start packer build for: %s", color.HiCyanString(p.ProviderConfig.BuildFile)) + cmd = fmt.Sprintf("%s build %s", packerBinary, p.ProviderConfig.BuildFile) + out, _ = utils.RunCommandWithErr(cmd) + + if !strings.Contains(out, fmt.Sprintf("%v scan -f", libs.BINARY)) { + if !strings.Contains(out, fmt.Sprintf("%v: command not found", libs.BINARY)) { + utils.ErrorF(out) + return fmt.Errorf("error running provisioning") + } + } + return nil +} diff --git a/provider/parser.go b/provider/parser.go new file mode 100644 index 0000000..49bd5de --- /dev/null +++ b/provider/parser.go @@ -0,0 +1,127 @@ +package provider + +import ( + "os" + "math/rand" + "time" + + "github.com/Shopify/yaml" + "github.com/j3ssie/osmedeus/utils" +) + +// ConfigProviders cloud config file +type ConfigProviders struct { + Builder Builder `yaml:"builder"` + Clouds []ConfigProvider `yaml:"clouds"` +} + +// Builder config for builder file +type Builder struct { + BuildRepo string `yaml:"build_repo"` + PublicKey string `yaml:"public_key"` + SecretKey string `yaml:"secret_key"` +} + +// ConfigProvider cloud config file for each provider from ~/osmedeus-base/cloud/provider.yaml +type ConfigProvider struct { + // core part + Name string `yaml:"name"` + Token string `yaml:"token"` + + SecretKey string `yaml:"secret_key"` + AccessKeyId string `yaml:"access_key"` + + Provider string `yaml:"provider"` + DefaultImage string `yaml:"default_image"` + Size string `yaml:"size"` + Region string `yaml:"region"` + Limit int `yaml:"limit"` + Username string `yaml:"username"` + + // BaseImage string `yaml:"base"` + RedactedToken string `yaml:"-"` + Snapshot string `yaml:"-"` + SnapshotID string `yaml:"-"` + InstanceName string `yaml:"-"` + PublicIP string `yaml:"-"` + SshKey string `yaml:"-"` + + // for config + ProviderFolder string `yaml:"-"` + ConfigFile string `yaml:"-"` + BuildFile string `yaml:"-"` + BuildData map[string]string `yaml:"-"` + + // for building + VarsFile string `yaml:"-"` + RunnerFile string `yaml:"-"` + BuildCommand string `yaml:"-"` + BaseFolder string `yaml:"-"` + RawCommand string `yaml:"-"` +} + +// ParseProvider parse cloud file from ~/osmedeus-base/cloud/provider.yaml +func ParseProvider(cloudFile string) (ConfigProviders, error) { + var clouds ConfigProviders + cloudFile = utils.NormalizePath(cloudFile) + + yamlFile, err := os.ReadFile(cloudFile) + if err != nil { + utils.ErrorF("YAML parsing err #%v ", err) + return clouds, err + } + err = yaml.Unmarshal(yamlFile, &clouds) + if err != nil { + utils.ErrorF("Error: %v", err) + return clouds, err + } + + return clouds, nil +} + +// mics function + +const ( + lowerChars = "abcdefghijklmnopqrstuvwxyz" + upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + numberChars = "0123456789" + symbolChars = "!@#$%^&*()+-=[]{}<>?~" +) + +func GeneratePassword(length int) string { + rand.Seed(time.Now().UnixNano()) + + var passwordChars []byte + + // Add at least one of each character type + passwordChars = append(passwordChars, randomChar(lowerChars)) + passwordChars = append(passwordChars, randomChar(upperChars)) + passwordChars = append(passwordChars, randomChar(numberChars)) + passwordChars = append(passwordChars, randomChar(symbolChars)) + + // Add remaining characters randomly + for i := len(passwordChars); i < length; i++ { + charType := rand.Intn(4) // 0 for lower, 1 for upper, 2 for number, 3 for symbol + switch charType { + case 0: + passwordChars = append(passwordChars, randomChar(lowerChars)) + case 1: + passwordChars = append(passwordChars, randomChar(upperChars)) + case 2: + passwordChars = append(passwordChars, randomChar(numberChars)) + case 3: + passwordChars = append(passwordChars, randomChar(symbolChars)) + } + } + + // Shuffle the characters randomly + rand.Shuffle(len(passwordChars), func(i, j int) { + passwordChars[i], passwordChars[j] = passwordChars[j], passwordChars[i] + }) + + return string(passwordChars) +} + +func randomChar(charset string) byte { + return charset[rand.Intn(len(charset))] +} diff --git a/provider/provider.go b/provider/provider.go new file mode 100644 index 0000000..e64f06f --- /dev/null +++ b/provider/provider.go @@ -0,0 +1,265 @@ +package provider + +import ( + "fmt" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cast" +) + +type Provider struct { + ProviderName string + Token string + RedactedToken string + // for aws only + AccessKeyId string + SecretKey string + SecurityGroupID string + SecurityGroupName string + + Instances []Instance `json:"-"` + InstanceLimit int + Available bool + HealthCheck bool + + // for create snapshot + SnapshotID string + SnapshotName string + OldSnapShotID []string `json:"-"` + SnapshotFound bool + SSHKeyFound bool + SSHPublicKey string + SSHPrivateKey string + SSHKeyID string + SSHUser string + + // for create + CreatedInstance Instance `json:"-"` + Region string + Size string + SSHKeyName string + + // mics + SwapSizeMap map[string]int `json:"-"` + IsBackgroundCheck bool + + // for building + ProviderConfig ConfigProvider `json:"-"` + Opt libs.Options `json:"-"` + + // for retry + BackOff *backoff.ExponentialBackOff `json:"-"` + // client of vendor + Client interface{} `json:"-"` +} + +type Instance struct { + InstanceID string + InstanceName string + IPAddress string + + // meta data + Region string + Size string + Status string + ImageID string + ImageName string + CPU string + // MB + Memory string + // GB + Disk string + + // more for the osm + InputName string + ProviderName string + CreatedAt string +} + +// InitProvider init provider object to easier interact with cloud provider +func InitProvider(providerName string, token string) (Provider, error) { + var provider Provider + provider.ProviderName = providerName + provider.Token = token + + if providerName == "aws" { + // token should be 'AccessKeyId,SecretKey' + provider.AccessKeyId = strings.TrimSpace(strings.Split(token, ",")[0]) + provider.SecretKey = strings.TrimSpace(strings.Split(token, ",")[1]) + provider.Token = token + } + + provider.InitClient() + return provider, nil +} + +// InitProviderWithConfig init provider object to easier interact with cloud provider +func InitProviderWithConfig(opt libs.Options, providerConfig ConfigProvider) (Provider, error) { + var provider Provider + provider.ProviderName = providerConfig.Provider + provider.Token = providerConfig.Token + + // for aws only + provider.AccessKeyId = providerConfig.AccessKeyId + provider.SecretKey = providerConfig.SecretKey + if provider.AccessKeyId != "" { + provider.Token = provider.AccessKeyId + "," + provider.SecretKey + } + + provider.ProviderConfig = providerConfig + provider.Opt = opt + if opt.Cloud.BackgroundRun { + provider.IsBackgroundCheck = true + } + + if err := provider.InitClient(); err != nil { + return provider, fmt.Errorf("unable to validate token: %v", provider.Token) + } + + provider.Prepare() + return provider, nil +} + +func (p *Provider) InitClient() (err error) { + if p.Token == "" && p.AccessKeyId == "" { + utils.ErrorF("empty or invalid token: %v", p.Token) + return fmt.Errorf("empty or invalid token") + } + if len(p.Token) > 5 { + p.RedactedToken = p.Token[:5] + "***" + p.Token[len(p.Token)-5:len(p.Token)] + if !p.IsBackgroundCheck { + utils.InforF("Init %v provider with token: %v", color.HiYellowString(p.ProviderName), color.HiCyanString(p.RedactedToken)) + } + } + + p.Available = true + switch p.ProviderName { + case "do", "digitalocean": + p.ClientDO() + case "ln", "line", "linode": + p.ClientLinode() + case "aw", "aws", "asw": + p.ClientAWS() + default: + p.ClientDO() + } + + // skip balance check if health check + if p.HealthCheck { + return nil + } + + switch p.ProviderName { + case "do", "digitalocean": + err = p.AccountDO() + case "ln", "line", "linode": + err = p.AccountLN() + case "aw", "aws", "asw": + err = p.AccountAWS() + default: + err = p.AccountDO() + } + + return err +} + +// Prepare setup some default variables +func (p *Provider) Prepare() { + // get snapshot + version := strings.ReplaceAll(strings.TrimSpace(libs.VERSION), " ", "-") + SnapshotName := fmt.Sprintf("%s-base-%s", strings.TrimSpace(libs.SNAPSHOT), version) + p.SnapshotName = SnapshotName + + // sshKey + keyName := fmt.Sprintf("%s-cloud-key", strings.TrimSpace(libs.SNAPSHOT)) + p.SSHKeyName = keyName + + // for retry + b := backoff.NewExponentialBackOff() + // It never stops if MaxElapsedTime == 0. + b.MaxElapsedTime = 1200 * time.Second + b.Multiplier = 2.0 + b.InitialInterval = 30 * time.Second + p.BackOff = b + + if p.Opt.Cloud.Retry > 0 { + b.MaxElapsedTime = time.Duration(p.Opt.Cloud.Retry*60) * time.Second + } + + // setup ssh key + if p.SSHPublicKey == "" { + p.SSHPublicKey = p.Opt.Cloud.PublicKeyContent + } + if p.SSHPrivateKey == "" { + p.SSHPrivateKey = p.Opt.Cloud.SecretKeyContent + } + + utils.DebugF("Get data of cloud provider") + switch p.ProviderName { + case "do", "digitalocean": + p.DefaultDO() + case "ln", "line", "linode": + p.DefaultLinode() + case "aw", "aws", "asw": + p.DefaultAWS() + default: + p.DefaultDO() + } + + p.Action(GetSSHKey) + p.Action(ListImage) + + if p.SSHKeyID != "" { + if !p.IsBackgroundCheck { + utils.InforF("Found SSH Key ID: %v", color.HiBlueString(p.SSHKeyID)) + } + } +} + +func (p *Provider) Action(actionName string, params ...interface{}) error { + var err error + var param interface{} + if len(params) > 0 { + param = params[0] + } + + if !p.IsBackgroundCheck { + utils.InforF("[%v] running action: %v", p.ProviderName, color.HiBlueString(actionName)) + } + + operation := func() error { + switch actionName { + case GetSSHKey: + err = p.GetSSHKey() + case ListInstance: + err = p.ListInstance() + case ListImage: + err = p.ListSnapShot() + case RunBuild: + err = p.RunBuild() + case GetInstanceInfo: + err = p.GetInstanceInfo(param) + case BootInstance: + err = p.BootInstance(param) + case CreateInstance: + err = p.CreateInstance(cast.ToString(param)) + default: + err = p.ListInstance() + } + if err != nil { + utils.ErrorF("error running action %v: %v", color.HiCyanString(actionName), err) + } + return err + } + err = backoff.Retry(operation, p.BackOff) + if err != nil { + utils.ErrorF("error running action %v -- %v", actionName, p.ProviderName) + return err + } + return nil +} diff --git a/provider/provider_aws.go b/provider/provider_aws.go new file mode 100644 index 0000000..94e8f6b --- /dev/null +++ b/provider/provider_aws.go @@ -0,0 +1,516 @@ +package provider + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/spf13/cast" + "golang.org/x/crypto/ssh" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/costexplorer" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" +) + +// DefaultAWS set some default data for AWS provider +func (p *Provider) DefaultAWS() { + p.Region = "ap-southeast-1" + p.Size = "t2.medium" + p.SecurityGroupName = "osmp-allow-root-access" + p.SSHUser = p.ProviderConfig.Username + + if p.ProviderConfig.Username != "" { + p.SSHUser = "admin" + } + if p.ProviderConfig.Region != "" { + p.Region = p.ProviderConfig.Region + } + if p.ProviderConfig.Size != "" { + p.Size = p.ProviderConfig.Size + } +} + +func (p *Provider) InitSessionAWS() (*session.Session, error) { + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(p.Region), + Credentials: credentials.NewStaticCredentials(p.AccessKeyId, p.SecretKey, ""), + }) + + return sess, err +} + +func (p *Provider) ClientAWS() { + client, err := p.InitSessionAWS() + if err != nil { + panic(err) + } + p.ProviderName = "aws" + p.Client = client +} + +func (p *Provider) ConvertClientAWS() *session.Session { + sess, ok := p.Client.(*session.Session) + if !ok { + utils.ErrorF("error converting aws session %v", ok) + } + sess.Config.Region = aws.String(p.Region) + return sess +} + +func (p *Provider) AccountAWS() error { + ceSvc := costexplorer.New(p.ConvertClientAWS()) + // Set the parameters for the query + now := time.Now() + start := time.Date(now.Year(), now.Month()-1, 1, 0, 0, 0, 0, now.Location()) + end := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + params := &costexplorer.GetCostAndUsageInput{ + TimePeriod: &costexplorer.DateInterval{ + Start: aws.String(start.Format("2006-01-02")), + End: aws.String(end.Format("2006-01-02")), + }, + Granularity: aws.String(costexplorer.GranularityMonthly), + Metrics: []*string{aws.String("UnblendedCost")}, + } + + // Send the query and get the results + // result, err := ceSvc.GetCostAndUsage(params) + // if err != nil { + // utils.ErrorF("Error getting cost and usage:", err) + // return err + // } + + // Send the query and get the results + result, err := ceSvc.GetCostAndUsage(params) + if err != nil { + utils.ErrorF("Error getting cost and usage: %v", err) + return err + } + + // Print the total cost for the previous month + cost := *result.ResultsByTime[0].Total["UnblendedCost"].Amount + + if !p.IsBackgroundCheck { + utils.InforF("The total cost of AWS services for the this month was %s", color.HiRedString("$"+cost)) + } + return nil +} + +func (p *Provider) GetSSHKeyAWS() error { + ec2Svc := ec2.New(p.ConvertClientAWS()) + // Get a list of all the key pairs in the account + keyPairsOutput, err := ec2Svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{}) + if err != nil { + utils.ErrorF("Error describing key pairs: %v", err) + return err + } + + pubKeyBytes := []byte(p.SSHPublicKey) + // Parse the key, other info ignored + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyBytes) + if err != nil { + utils.ErrorF("%v", err) + return err + } + hash := sha256.Sum256(pubKey.Marshal()) + sshHash := base64.StdEncoding.EncodeToString(hash[:]) + + // Check if your SSH key is present in the list + for _, keyPair := range keyPairsOutput.KeyPairs { + // found the same key name but different key fingerprint + if *keyPair.KeyName == p.SSHKeyName && *keyPair.KeyFingerprint != sshHash { + // Delete the key pair + _, err := ec2Svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{ + KeyName: aws.String(*keyPair.KeyName), + }) + if err != nil { + utils.ErrorF("%v", err) + } + utils.InforF("Successfully deleted key pair %s", color.HiBlueString(*keyPair.KeyName)) + } + + if *keyPair.KeyFingerprint == sshHash { + p.SSHKeyID = cast.ToString(*keyPair.KeyPairId) + p.SSHKeyFound = true + break + } + + } + + if p.SSHKeyFound { + utils.DebugF("Your SSH key was found in the account: %v -- %v", color.HiCyanString(p.SSHKeyName), color.HiCyanString(p.SSHKeyID)) + return nil + } + + // Import the SSH key into your AWS account + result, err := ec2Svc.ImportKeyPair(&ec2.ImportKeyPairInput{ + KeyName: aws.String(p.SSHKeyName), + PublicKeyMaterial: []byte(p.SSHPublicKey), + }) + if err != nil { + utils.ErrorF("Error create key pairs: %v", err) + return err + } + utils.DebugF("Successfully imported SSH key: %v -- %v", color.HiCyanString(*result.KeyName), color.HiCyanString(*result.KeyPairId)) + p.SSHKeyID = cast.ToString(*result.KeyPairId) + p.SSHKeyFound = true + + return nil +} + +func (p *Provider) ListSnapshotAWS() error { + svc := ec2.New(p.ConvertClientAWS()) + + // listing only image that own by you + self := "self" + ownImages := &ec2.DescribeImagesInput{Owners: []*string{&self}} + result, err := svc.DescribeImages(ownImages) + if err != nil { + utils.ErrorF("err: Unable to list images, %v", err) + return err + } + + for _, item := range result.Images { + if strings.HasPrefix(*item.Name, libs.SNAPSHOT) { + p.OldSnapShotID = append(p.OldSnapShotID, *item.ImageId) + } + + if strings.TrimSpace(*item.Name) == strings.TrimSpace(p.SnapshotName) { + utils.DebugF("Found base image snapshot with ID: %s", color.HiBlueString(*item.ImageId)) + p.SnapshotID = *item.ImageId + p.SnapshotName = *item.Name + p.SnapshotFound = true + } + } + + return nil +} + +func (p *Provider) DeleteImageAWS(id string) error { + if p.SnapshotID == "" { + return nil + } + svc := ec2.New(p.ConvertClientAWS()) + deletedImage := &ec2.DeregisterImageInput{ImageId: &p.SnapshotID} + _, err := svc.DeregisterImage(deletedImage) + if err != nil { + utils.ErrorF("err: Unable to delete snapshot: %v -- %v", id, err) + return err + } + utils.InforF("Deleted image ID: %v", color.HiRedString(p.SnapshotID)) + p.DeleteSnapshotAWS() + return nil +} + +func (p *Provider) DeleteSnapshotAWS() error { + svc := ec2.New(p.ConvertClientAWS()) + + // List all snapshots + result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ + OwnerIds: []*string{aws.String("self")}, + MaxResults: aws.Int64(1000), + }) + if err != nil { + utils.ErrorF("Error listing snapshots: %v", err) + return err + } + + var snapshotIDs []string + for _, snapshot := range result.Snapshots { + + for _, tag := range snapshot.Tags { + if *tag.Key == "Name" && *tag.Value == "Osmedeus Premium Image" { + snapshotIDs = append(snapshotIDs, *snapshot.SnapshotId) + } + } + } + + for _, snapshotID := range snapshotIDs { + _, err = svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{ + SnapshotId: aws.String(snapshotID), + }) + if err != nil { + utils.ErrorF("Error deleting snapshot: %v -- %v", snapshotID, err) + } else { + utils.DebugF("Delted snapshot ID: %v", color.HiRedString(snapshotID)) + } + } + + return nil +} + +func (p *Provider) ListInstanceAWS() error { + svc := ec2.New(p.ConvertClientAWS()) + result, err := svc.DescribeInstances(nil) + if err != nil { + utils.ErrorF("err: Unable to list ec2 instances: %v", err) + return err + } + + var numberOfInstance int + for i := range result.Reservations { + for _, instance := range result.Reservations[i].Instances { + if *instance.State.Name != "running" { + continue + } + numberOfInstance += 1 + + launchTime := *instance.LaunchTime + creationDate := launchTime.Format(time.RFC1123) + parsedInstance := Instance{ + InstanceID: cast.ToString(*instance.InstanceId), + IPAddress: *instance.PublicIpAddress, + InstanceName: *instance.State.Name, + ImageID: cast.ToString(*instance.ImageId), + // ImageName: instance.Image.Name, + // Region: instance.Region.Slug, + // Region: *instance.Architecture, + // Memory: cast.ToString(instance.Memory), + // CPU: cast.ToString(instance.Vcpus), + // Disk: cast.ToString(instance.Disk), + Status: *instance.State.Name, + CreatedAt: cast.ToString(creationDate), + InputName: "", + ProviderName: "aws", + } + + p.Instances = append(p.Instances, parsedInstance) + } + } + + utils.InforF("Found %v running instances", color.HiMagentaString("%v", numberOfInstance)) + // check if we reach max instance number + if p.InstanceLimit > 0 { + if len(p.Instances) >= p.InstanceLimit { + p.Available = false + } + } + + return nil +} + +func (p *Provider) CreateInstanceAWS(InstanceName string) (instanctID string, err error) { + svc := ec2.New(p.ConvertClientAWS()) + p.CreateSecurityGroup() + + // Set the parameters for the instance + params := &ec2.RunInstancesInput{ + // InstanceName: aws.String(name), + ImageId: aws.String(p.SnapshotID), // Replace with the ID of the image you want to use + InstanceType: aws.String(p.Size), // Specify the instance type like t2.micro + MinCount: aws.Int64(1), + MaxCount: aws.Int64(1), + KeyName: aws.String(p.SSHKeyName), + SecurityGroups: []*string{ + aws.String(p.SecurityGroupName), + }, + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("instance"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(InstanceName), + }, + }, + }, + }, + } + + // Create the instance + result, err := svc.RunInstances(params) + if err != nil { + utils.ErrorF("Error creating instance: %v", err) + return + } + + // Get the instance ID + instanctID = *result.Instances[0].InstanceId + utils.InforF("Successfully Created Instance ID: %v -- %v", color.HiBlueString(instanctID), color.HiBlueString(InstanceName)) + utils.DebugF("Waiting for the instance %v to be ready...", color.HiBlueString(instanctID)) + + time.Sleep(60 * time.Second) + // Get the instance state + for i := 0; i < 10; i++ { + if p.InstanceReady(instanctID) == nil { + return instanctID, nil + } + time.Sleep(60 * time.Second) + } + + return instanctID, nil +} + +func (p *Provider) InstanceReady(instanceID string) error { + svc := ec2.New(p.ConvertClientAWS()) + + // Describe the instance + params := &ec2.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(instanceID), + }, + } + _, err := svc.DescribeInstances(params) + if err != nil { + return err + } + + return nil +} + +func (p *Provider) InstanceReboot(instanceID string) error { + svc := ec2.New(p.ConvertClientAWS()) + + // Create the input for the RebootInstances operation + params := &ec2.RebootInstancesInput{ + InstanceIds: []*string{aws.String(instanceID)}, + } + + // Call the RebootInstances operation + _, err := svc.RebootInstances(params) + if err != nil { + fmt.Println("Error rebooting instance:", err) + return err + } + return nil +} + +func (p *Provider) DeleteInstanceAWS(id string) error { + svc := ec2.New(p.ConvertClientAWS()) + + // Set the parameters for the instance + params := &ec2.TerminateInstancesInput{ + InstanceIds: []*string{ + aws.String(id), // Replace with the ID of the instance you want to delete + }, + } + + // Delete the instance + _, err := svc.TerminateInstances(params) + if err != nil { + utils.ErrorF("Error deleting instance: %v", err) + return err + } + + utils.InforF("Successfully Deleted instance ID: %v", color.HiRedString(id)) + return nil +} + +func (p *Provider) InstanceInfoAWS(id string) (Instance, error) { + var parsedInstance Instance + + svc := ec2.New(p.ConvertClientAWS()) + + // Set the parameters for the instance + params := &ec2.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(id), + }, + } + + // Get the instance information + result, err := svc.DescribeInstances(params) + if err != nil { + utils.ErrorF("Error getting instance information: %v", err) + return parsedInstance, err + } + + // Print the instance information + for _, reservation := range result.Reservations { + for _, instance := range reservation.Instances { + + launchTime := *instance.LaunchTime + creationDate := launchTime.Format(time.RFC1123) + + var instanceName string + for _, tag := range instance.Tags { + if *tag.Key == "Name" { + instanceName = *tag.Value + break + } + } + + parsedInstance = Instance{ + InstanceID: cast.ToString(*instance.InstanceId), + IPAddress: *instance.PublicIpAddress, + InstanceName: instanceName, + ImageID: cast.ToString(*instance.ImageId), + Status: *instance.State.Name, + CreatedAt: cast.ToString(creationDate), + InputName: "", + ProviderName: "aws", + } + + } + } + + p.CreatedInstance = parsedInstance + utils.DebugF("Instance ID Info: %v -- %v -- %v", color.HiBlueString(p.CreatedInstance.InstanceID), p.CreatedInstance.InstanceName, p.CreatedInstance.IPAddress) + return parsedInstance, nil +} + +func (p *Provider) CreateSecurityGroup() error { + svc := ec2.New(p.ConvertClientAWS()) + + // Set the parameters for the security group + params := &ec2.DescribeSecurityGroupsInput{ + GroupNames: []*string{ + aws.String(p.SecurityGroupName), // Replace with the ID of the security group you want to check + }, + } + + // Get the security group information + scGroups, err := svc.DescribeSecurityGroups(params) + if err == nil { + // Print the security group information + for _, group := range scGroups.SecurityGroups { + if *group.GroupName == p.SecurityGroupName { + p.SecurityGroupID = *group.GroupId + utils.DebugF("Security Group allow root access has been found: %v", color.HiBlueString(p.SecurityGroupID)) + return nil + } + } + } + + // only create if not found + + // Create the security group + result, err := svc.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ + GroupName: aws.String("osmp-allow-root-access"), + Description: aws.String("Security group for allowing root access to EC2 instances"), + }) + if err != nil { + utils.ErrorF("Error creating security group: %v", err) + return err + } + + // Add a rule to the security group to allow SSH access from any IP address + _, err = svc.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ + GroupId: aws.String(*result.GroupId), + IpPermissions: []*ec2.IpPermission{ + { + FromPort: aws.Int64(22), + ToPort: aws.Int64(22), + IpProtocol: aws.String("tcp"), + IpRanges: []*ec2.IpRange{ + {CidrIp: aws.String("0.0.0.0/0")}, + }, + }, + }, + }) + if err != nil { + utils.ErrorF("Error adding rule to security group: %v", err) + return err + } + + p.SecurityGroupID = *result.GroupId + utils.DebugF("Security Group allow root access has been found: %v", color.HiBlueString(p.SecurityGroupID)) + return nil +} diff --git a/provider/provider_digitalocean.go b/provider/provider_digitalocean.go new file mode 100644 index 0000000..a001c90 --- /dev/null +++ b/provider/provider_digitalocean.go @@ -0,0 +1,294 @@ +package provider + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + "github.com/digitalocean/godo" + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/spf13/cast" +) + +// DefaultDO set some default data for DO provider +func (p *Provider) DefaultDO() { + p.Region = "sfo3" + p.Size = "s-2vcpu-4gb" + p.SSHUser = p.ProviderConfig.Username + + if p.ProviderConfig.Username != "" { + p.SSHUser = "root" + } + if p.ProviderConfig.Region != "" { + p.Region = p.ProviderConfig.Region + } + if p.ProviderConfig.Size != "" { + p.Size = p.ProviderConfig.Size + } +} + +func (p *Provider) ClientDO() { + client := godo.NewFromToken(p.Token) + p.Client = client +} + +func (p *Provider) ConvertClientDO() *godo.Client { + client, ok := p.Client.(*godo.Client) + if !ok { + utils.ErrorF("error converting digital ocean session %v", ok) + } + return client +} + +func (p *Provider) AccountDO() error { + client := p.ConvertClientDO() + ctx := context.TODO() + + account, _, err := client.Account.Get(ctx) + if err != nil { + return fmt.Errorf("error getting account information") + } + p.InstanceLimit = account.DropletLimit + + bill, _, err := client.Balance.Get(ctx) + if err != nil { + return fmt.Errorf("error getting account information") + } + + if !p.IsBackgroundCheck { + utils.InforF("Account Billing Information: MonthToDateBalance: %v -- AccountBalance: %v", strings.TrimLeft(color.HiRedString(bill.MonthToDateBalance), "-"), color.HiGreenString(bill.AccountBalance)) + } + + return nil +} + +func (p *Provider) ListInstanceDO() error { + client := p.ConvertClientDO() + + ctx := context.TODO() + opt := &godo.ListOptions{ + Page: 1, + PerPage: 1000, + } + + droplets, _, err := client.Droplets.List(ctx, opt) + if err != nil { + return fmt.Errorf("error getting digital ocean instance") + } + utils.DebugF("found %v instances", len(droplets)) + + for _, instance := range droplets { + ipAddress, ok := instance.PublicIPv4() + if ok != nil || ipAddress == "" { + utils.ErrorF("Instance has no public IP: %v -- %v", instance.ID, instance.Name) + continue + } + parsedInstance := Instance{ + InstanceID: cast.ToString(instance.ID), + IPAddress: ipAddress, + InstanceName: instance.Name, + ImageID: cast.ToString(instance.Image.ID), + ImageName: instance.Image.Name, + Region: instance.Region.Slug, + Memory: cast.ToString(instance.Memory), + CPU: cast.ToString(instance.Vcpus), + Disk: cast.ToString(instance.Disk), + Status: instance.Status, + CreatedAt: instance.Created, + + InputName: "", + ProviderName: "do", + } + + p.Instances = append(p.Instances, parsedInstance) + } + + // check if we reach max instance number + if p.InstanceLimit > 0 { + if len(p.Instances) >= p.InstanceLimit { + p.Available = false + } + } + + return nil +} + +func (p *Provider) GetSSHKeyDO() error { + client := p.ConvertClientDO() + + ctx := context.TODO() + opt := &godo.ListOptions{ + Page: 1, + PerPage: 1000, + } + + keys, _, err := client.Keys.List(ctx, opt) + if err != nil { + return fmt.Errorf("error listing ssh key -- %v", err) + } + + for _, key := range keys { + if strings.TrimSpace(key.PublicKey) == strings.TrimSpace(p.SSHPublicKey) { + p.SSHKeyID = cast.ToString(key.ID) + p.SSHKeyFound = true + utils.DebugF("Found SSH Key: %v -- %v ", color.HiCyanString(key.Name), color.HiCyanString(p.SSHKeyID)) + } + } + + // create one if not found + if !p.SSHKeyFound { + utils.DebugF("No SSHKey found. create a new one") + createRequest := &godo.KeyCreateRequest{ + Name: p.SSHKeyName, + PublicKey: p.SSHPublicKey, + } + transfer, _, err := client.Keys.Create(ctx, createRequest) + if err == nil { + p.SSHKeyID = cast.ToString(transfer.ID) + p.SSHKeyFound = true + utils.DebugF("Created new SSH Key: %v", color.HiCyanString(p.SSHKeyID)) + } else { + return fmt.Errorf("error create ssh key -- %v", err) + } + } + + return nil +} + +func (p *Provider) ListSnapshotDO() error { + client := p.ConvertClientDO() + + ctx := context.TODO() + opt := &godo.ListOptions{ + Page: 1, + PerPage: 1000, + } + + snapshots, _, err := client.Snapshots.List(ctx, opt) + if err != nil { + return fmt.Errorf("error getting digital ocean snapshot") + } + for _, instance := range snapshots { + name := instance.Name + id := cast.ToString(instance.ID) + + if strings.HasPrefix(name, libs.SNAPSHOT) { + p.OldSnapShotID = append(p.OldSnapShotID, id) + } + + if strings.TrimSpace(name) == strings.TrimSpace(p.SnapshotName) { + utils.DebugF("Found base image snapshot with ID: %s", color.HiBlueString(id)) + p.SnapshotID = id + p.SnapshotName = name + p.SnapshotFound = true + + } + } + + return nil +} + +func (p *Provider) CreateInstanceDO(name string) (dropletId int, err error) { + client := p.ConvertClientDO() + + ctx := context.TODO() + createRequest := &godo.DropletCreateRequest{ + Name: name, + Region: p.Region, + Size: p.Size, + Image: godo.DropletCreateImage{ + ID: cast.ToInt(p.SnapshotID), + //Slug: "ubuntu-16-04-x64", + }, + // SSHKeys: []godo.DropletCreateSSHKey{ + // godo.DropletCreateSSHKey{ID: cast.ToInt(p.SSHKeyID)}, + // }, + // Tags: []string{libs.SNAPSHOT}, + + SSHKeys: []godo.DropletCreateSSHKey{ + {ID: cast.ToInt(p.SSHKeyID)}, + }, + Tags: []string{libs.SNAPSHOT}, + } + + instance, res, err := client.Droplets.Create(ctx, createRequest) + if err != nil { + utils.ErrorF("error create digital ocean instance -- %v", err) + content, ok := io.ReadAll(res.Body) + if ok == nil { + fmt.Println(string(content)) + } + return dropletId, fmt.Errorf("error create digital ocean instance -- %v", err) + } + + // get droplet IP info + dropletId = instance.ID + utils.DebugF("Created instance %v", color.HiBlueString("%v", instance.ID)) + utils.DebugF("Waiting for the instance %v to be ready...", color.HiBlueString("%v", instance.ID)) + time.Sleep(100 * time.Second) + return dropletId, nil +} + +func (p *Provider) InstanceInfoDO(id int) (Instance, error) { + var parsedInstance Instance + client := p.ConvertClientDO() + ctx := context.TODO() + instance, _, err := client.Droplets.Get(ctx, id) + if err != nil { + return parsedInstance, fmt.Errorf("error get instance info:") + } + + ipAddress, err := instance.PublicIPv4() + if err != nil || ipAddress == "" { + return parsedInstance, fmt.Errorf("no public ip address yet") + } + parsedInstance = Instance{ + InstanceID: cast.ToString(instance.ID), + IPAddress: ipAddress, + InstanceName: instance.Name, + ImageID: cast.ToString(instance.Image.ID), + ImageName: instance.Image.Name, + Region: instance.Region.Slug, + Memory: cast.ToString(instance.Memory), + CPU: cast.ToString(instance.Vcpus), + Disk: cast.ToString(instance.Disk), + Status: instance.Status, + CreatedAt: instance.Created, + + InputName: "", + ProviderName: "do", + } + p.CreatedInstance = parsedInstance + utils.DebugF("Successfully Created Instance: %v -- %v -- %v", p.CreatedInstance.InstanceID, p.CreatedInstance.InstanceName, p.CreatedInstance.IPAddress) + + return parsedInstance, nil +} + +func (p *Provider) DeleteInstanceDO(id string) error { + client := p.ConvertClientDO() + ctx := context.TODO() + _, err := client.Droplets.Delete(ctx, cast.ToInt(id)) + if err != nil { + utils.ErrorF("error delete instance -- %v", err) + return fmt.Errorf("error delete instance") + } + utils.InforF("Successfully Deleted instance ID: %v", color.HiRedString(id)) + return nil +} + +func (p *Provider) DeleteSnapShotDO(id string) error { + client := p.ConvertClientDO() + ctx := context.TODO() + + _, err := client.Snapshots.Delete(ctx, id) + if err != nil { + utils.ErrorF("error delete snapshot -- %v", err) + return fmt.Errorf("error delete instance") + } + utils.InforF("Deleted snapshot ID: %v", color.HiRedString(id)) + return nil +} diff --git a/provider/provider_linode.go b/provider/provider_linode.go new file mode 100644 index 0000000..1cb7cae --- /dev/null +++ b/provider/provider_linode.go @@ -0,0 +1,477 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "github.com/linode/linodego" + "github.com/spf13/cast" + "golang.org/x/oauth2" +) + +// DefaultLinode set some default data for DO provider +func (p *Provider) DefaultLinode() { + p.Region = "us-east" + p.Size = "g6-standard-1" + p.SSHUser = p.ProviderConfig.Username + + if p.ProviderConfig.Username != "" { + p.SSHUser = "root" + } + if p.ProviderConfig.Region != "" { + p.Region = p.ProviderConfig.Region + } + if p.ProviderConfig.Size != "" { + p.Size = p.ProviderConfig.Size + } + + if p.Opt.Cloud.Size != "" { + p.Size = p.Opt.Cloud.Size + } + if p.Opt.Cloud.Region != "" { + p.Region = p.Opt.Cloud.Region + } + + p.LinodeDiskMap() +} + +func (p *Provider) ClientLinode() error { + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: p.Token}) + oauth2Client := &http.Client{ + Transport: &oauth2.Transport{ + Source: tokenSource, + }, + } + + linodeClient := linodego.NewClient(oauth2Client) + + p.Client = linodeClient + return nil +} + +func (p *Provider) ConvertClientLinode() linodego.Client { + client, ok := p.Client.(linodego.Client) + if !ok { + utils.ErrorF("error converting linode session %v", ok) + } + return client +} + +func (p *Provider) AccountLN() error { + client := p.ConvertClientLinode() + ctx := context.TODO() + account, err := client.GetAccount(ctx) + if err != nil { + return fmt.Errorf("error getting account information") + } + if !p.IsBackgroundCheck { + utils.InforF("Account Billing Information: BalanceUninvoiced: %v -- AccountBalance: %v", color.HiRedString("%v", account.BalanceUninvoiced), color.HiGreenString("%v", account.Balance)) + } + helper := false + opt := linodego.AccountSettingsUpdateOptions{ + NetworkHelper: &helper, + } + + // @TODO: no idea why this function false + // client.UpdateAccountSettings(context.Background(), opt) + + req := client.R(ctx).SetResult(&linodego.AccountSettings{}) + if bodyData, err := json.Marshal(&opt); err == nil { + req.URL = "https://api.linode.com/v4/account/settings" + //fmt.Println("err marshal", err) + body := string(bodyData) + req.SetBody(body).Put(req.URL) + } + //} + return err +} + +// LinodeTest list all instances +func (p *Provider) LinodeTest() error { + linodeClient := p.ConvertClientLinode() + + res, err := linodeClient.GetInstance(context.Background(), 4090913) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%v", res) + + return nil +} + +func (p *Provider) ListInstanceLN() error { + client := p.ConvertClientLinode() + + opt := &linodego.ListOptions{ + PageOptions: nil, + Filter: "", + } + + instances, err := client.ListInstances(context.TODO(), opt) + if err != nil { + return fmt.Errorf("error getting linode instance") + } + + for _, instance := range instances { + //instance.IPv4 + if len(instance.IPv4) == 0 { + utils.ErrorF("Instance has no public IP: %v -- %v", instance.ID, instance.Label) + continue + } + ipAddress := instance.IPv4[0] + + parsedInstance := Instance{ + InstanceID: cast.ToString(instance.ID), + IPAddress: cast.ToString(ipAddress), + InstanceName: instance.Label, + ImageID: cast.ToString(instance.Image), + ImageName: instance.Image, + Region: instance.Region, + Memory: cast.ToString(instance.Specs.Memory), + CPU: cast.ToString(instance.Specs.VCPUs), + Disk: cast.ToString(instance.Specs.Disk), + Status: cast.ToString(instance.Status), + CreatedAt: cast.ToString(instance.Created), + + InputName: "", + ProviderName: "do", + } + + p.Instances = append(p.Instances, parsedInstance) + } + return nil +} + +func (p *Provider) GetSSHKeyLN() error { + client := p.ConvertClientLinode() + ctx := context.TODO() + opt := &linodego.ListOptions{ + PageOptions: nil, + Filter: "", + } + + keys, err := client.ListSSHKeys(ctx, opt) + if err != nil { + return fmt.Errorf("error listing ssh key -- %v", err) + } + + for _, key := range keys { + if strings.TrimSpace(key.SSHKey) == strings.TrimSpace(p.SSHPublicKey) { + p.SSHKeyID = cast.ToString(key.ID) + p.SSHKeyFound = true + utils.DebugF("Found SSH Key: %v -- %v ", key.Label, p.SSHKeyID) + } + } + + // create one if not found + if !p.SSHKeyFound { + utils.DebugF("No SSHKey found. create a new one") + createRequest := linodego.SSHKeyCreateOptions{ + Label: p.SSHKeyName, + SSHKey: p.SSHPublicKey, + } + key, err := client.CreateSSHKey(ctx, createRequest) + if err == nil { + p.SSHKeyID = cast.ToString(key.ID) + p.SSHKeyFound = true + utils.DebugF("Created new SSH Key: %v", p.SSHKeyID) + } + } + + return nil +} + +func (p *Provider) ListSnapshotLN() error { + client := p.ConvertClientLinode() + ctx := context.TODO() + opt := &linodego.ListOptions{ + PageOptions: nil, + Filter: "", + } + + snapshots, err := client.ListImages(ctx, opt) + if err != nil { + return fmt.Errorf("error getting linode images") + } + for _, image := range snapshots { + name := image.Label + id := cast.ToString(image.ID) + if strings.HasPrefix(name, libs.SNAPSHOT) { + p.OldSnapShotID = append(p.OldSnapShotID, id) + } + + if strings.TrimSpace(name) == strings.TrimSpace(p.SnapshotName) { + utils.DebugF("Found base image snapshot with ID: %s", id) + p.SnapshotID = id + p.SnapshotName = name + p.SnapshotFound = true + } + } + + return nil +} + +func (p *Provider) LinodeDiskMap() { + p.SwapSizeMap = make(map[string]int) + p.SwapSizeMap["g6-nanode-1"] = 20000 + p.SwapSizeMap["g6-standard-1"] = 4000 + p.SwapSizeMap["g6-standard-2"] = 8000 + p.SwapSizeMap["g6-standard-4"] = 16000 + p.SwapSizeMap["g6-standard-6"] = 32000 +} + +func (p *Provider) CreateInstanceLN(name string) (dropletId int, err error) { + client := p.ConvertClientLinode() + + ctx := context.TODO() + booted := false + swapSize := 4000 + if swap, ok := p.SwapSizeMap[p.Size]; ok { + swapSize = swap + } + + createRequest := linodego.InstanceCreateOptions{ + Region: p.Region, + Type: p.Size, + Label: name, + // RootPass: utils.RandomString(10), + RootPass: GeneratePassword(16), + AuthorizedKeys: []string{ + p.SSHPublicKey, + }, + //AuthorizedUsers: []string{"root"}, + Image: p.SnapshotID, + Tags: []string{libs.SNAPSHOT}, + + SwapSize: &swapSize, + Booted: &booted, + //StackScriptID: 0, + //Group: "", + //StackScriptData: nil, + //BackupID: 0, + //BackupsEnabled: false, + //PrivateIP: false, + } + + utils.DebugF("Creating instance based on %v/%v image with password: %v", p.Size, p.SnapshotID, createRequest.RootPass) + instance, err := client.CreateInstance(ctx, createRequest) + if err != nil { + utils.ErrorF("error create linode instance %v -- %v", name, err) + return dropletId, fmt.Errorf("error create linode instance -- %v", err) + } + + //spew.Dump(instance) + // get droplet IP info + dropletId = instance.ID + time.Sleep(60 * time.Second) + return dropletId, nil +} + +func (p *Provider) BootInstanceLN(dropletId int) error { + client := p.ConvertClientLinode() + + ctx := context.TODO() + err := client.BootInstance(ctx, dropletId, 0) + return err +} + +func (p *Provider) MountDiskLN(dropletId int) error { + client := p.ConvertClientLinode() + utils.InforF("Mounting disk: %v", dropletId) + ctx := context.TODO() + + disk, err := client.CreateInstanceDisk(ctx, dropletId, linodego.InstanceDiskCreateOptions{ + Label: "test1", + Filesystem: "ext4", + Size: 2000, + }) + if err != nil { + utils.ErrorF("Error creating disk for resize: %s", err) + } + + disk, err = client.WaitForInstanceDiskStatus(ctx, dropletId, disk.ID, linodego.DiskReady, 180) + if err != nil { + utils.ErrorF("Error waiting for disk readiness for resize: %s", err) + return err + } + err = client.ResizeInstanceDisk(ctx, dropletId, disk.ID, 4000) + if err != nil { + utils.ErrorF("Error resizing instance disk: %s", err) + } + + return nil +} + +func (p *Provider) InstanceInfoLN(id int) (Instance, error) { + var parsedInstance Instance + client := p.ConvertClientLinode() + + instance, err := client.GetInstance(context.TODO(), id) + if err != nil { + return parsedInstance, fmt.Errorf("error getting linode instance") + } + + if len(instance.IPv4) == 0 { + utils.ErrorF("Instance has no public IP: %v -- %v", instance.ID, instance.Label) + return parsedInstance, fmt.Errorf("no public ip address yet") + } + ipAddress := instance.IPv4[0] + parsedInstance = Instance{ + InstanceID: cast.ToString(instance.ID), + IPAddress: cast.ToString(ipAddress), + InstanceName: instance.Label, + ImageID: cast.ToString(instance.Image), + ImageName: instance.Image, + Region: instance.Region, + Memory: cast.ToString(instance.Specs.Memory), + CPU: cast.ToString(instance.Specs.VCPUs), + Disk: cast.ToString(instance.Specs.Disk), + Status: cast.ToString(instance.Status), + CreatedAt: cast.ToString(instance.Created), + InputName: "", + ProviderName: "linode", + } + p.CreatedInstance = parsedInstance + utils.DebugF("Created instance ID: %v -- %v -- %v", p.CreatedInstance.InstanceID, p.CreatedInstance.InstanceName, p.CreatedInstance.IPAddress) + + return parsedInstance, nil +} + +func (p *Provider) DeleteInstanceLN(id string) error { + client := p.ConvertClientLinode() + + ctx := context.TODO() + err := client.DeleteInstance(ctx, cast.ToInt(id)) + if err != nil { + utils.ErrorF("error delete instance -- %v", err) + return fmt.Errorf("error delete instance") + } + utils.InforF("Deleted instance ID: %v", color.HiRedString(id)) + return nil +} + +func (p *Provider) DeleteSnapShotLN(id string) error { + client := p.ConvertClientLinode() + + ctx := context.TODO() + err := client.DeleteImage(ctx, id) + if err != nil { + utils.ErrorF("error delete snapshot -- %v", err) + return fmt.Errorf("error delete instance") + } + utils.InforF("Deleted snapshot ID: %v", color.HiRedString(id)) + return nil +} + +// @NOTE: old note for increase disk in linode +//func (c *CloudRunner) MountDiskLinode() error { +// diskSize := p.DisksMap[c.Cloud.Size] +// if diskSize == "" { +// return fmt.Errorf("can't found disk size") +// } +// +// utils.InforF("Mounting Disk size %s for instance %s", diskSize, c.InstanceID) +// cmd := c.Prefix + fmt.Sprintf(`linodes disks-list %s`, c.InstanceID) +// out := c.RetryCommandWithExcludeString(cmd, `Request failed:`) +// jsonParsed, err := gabs.ParseJSON([]byte(out)) +// if err != nil { +// utils.ErrorF("error when parsing content of droplet list") +// return err +// } +// +// var diskID string +// for _, instance := range jsonParsed.Children() { +// filesystem := cast.ToString(instance.S("filesystem").Data()) +// if strings.HasPrefix(filesystem, "ext") { +// diskID = cast.ToString(instance.S("id").Data()) +// } +// } +// if diskID == "" { +// return fmt.Errorf("error to find disk ID") +// } +// +// cmd = c.Prefix + fmt.Sprintf(`linodes disk-resize %s %s --size %s`, c.InstanceID, diskID, diskSize) +// out = c.RetryCommandWithExcludeString(cmd, `Request failed:`) +// if strings.Contains(out, "Request failed") { +// return fmt.Errorf("error to mount disk") +// } +// +// time.Sleep(5 * time.Second) +// // everything done booting the instance up +// cmd = c.Prefix + fmt.Sprintf(`linodes boot %s `, c.InstanceID) +// out = c.RetryCommandWithExcludeString(cmd, `Request failed:`) +// if strings.Contains(out, "Request failed") { +// return fmt.Errorf("error to mount disk") +// } +// +// return nil +//} +// +//func (c *CloudRunner) LinodeDiskMap() { +// c.DisksMap = make(map[string]string) +// c.DisksMap["g6-nanode-1"] = "20000" +// c.DisksMap["g6-standard-1"] = "45000" +// c.DisksMap["g6-standard-2"] = "75000" +// c.DisksMap["g6-standard-4"] = "150000" +// c.DisksMap["g6-standard-6"] = "320000" +//} +// +//var reservedAddrRanges []*net.IPNet +// +//var ReservedCIDRs = []string{ +// "192.168.0.0/16", +// "172.16.0.0/12", +// "10.0.0.0/8", +// "127.0.0.0/8", +// "224.0.0.0/4", +// "240.0.0.0/4", +// "100.64.0.0/10", +// "198.18.0.0/15", +// "169.254.0.0/16", +// "192.88.99.0/24", +// "192.0.0.0/24", +// "192.0.2.0/24", +// "192.94.77.0/24", +// "192.94.78.0/24", +// "192.52.193.0/24", +// "192.12.109.0/24", +// "192.31.196.0/24", +// "192.0.0.0/29", +//} +// +//func init() { +// for _, cidr := range ReservedCIDRs { +// if _, ipnet, err := net.ParseCIDR(cidr); err == nil { +// reservedAddrRanges = append(reservedAddrRanges, ipnet) +// } +// } +//} +// +//// IsPrivateIP checks if the addr parameter is within one of the address ranges in the ReservedCIDRs slice. +//func IsPrivateIP(addr string) bool { +// ip := net.ParseIP(addr) +// if ip == nil { +// return false +// } +// +// var cidr string +// for _, block := range reservedAddrRanges { +// if block.Contains(ip) { +// cidr = block.String() +// break +// } +// } +// +// if cidr != "" { +// return true +// } +// return false +//} diff --git a/provider/provider_test.go b/provider/provider_test.go new file mode 100644 index 0000000..ceafda8 --- /dev/null +++ b/provider/provider_test.go @@ -0,0 +1,95 @@ +package provider + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" +) + +var sampleToken = "d17ab13b0a3fccafefc932b9db95be45464339073100423336045977c0924491" + +func TestProviderList(t *testing.T) { + provider, err := InitProvider("do", sampleToken) + if err != nil { + t.Errorf("error") + } + provider.SSHPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0ZmMumN5GTKuPXVVqugz+6BZs6JyNaQGsPgsZk86uON//oXi5fkutsNu5IIPDZXph3P5NbUj2dPalNyzNY5jClPJeT0F/eowIhPeo5GfjmJlXR4TTgHSwPYrOQNop2w+xh2z5h4IXMEPVLpEsN67MuUjTKzRwirwPjigZS/gayhwfOfDsPaIwBhpoBGuR1x+Rzzxuiy7TToNoWhF6pT9qONoCtr0VrPMsmjVpEPKD/uTW/8KeFL0pb/9z18M4IlbtvkO0Y6RhrpFGNSmZTWc1eDsJpFJerrVd48rgx3aRHriijl4zX4GBhc0zjqJwv+nGTGFPJ9Tx/3kPMDUGna/f91VU7sL7YqeiSed8S0YcWfntYy64OknvMpN8VIoQ7WiJAkR3wPw+tL3ZduXXAiKHFTAiXev02mOvo2F2nQKdGS98lOH5m+zuUm8abYbyXYlGNEzz576ksb6nMWCSSXwhA5f4clPKaPmgBQFQMUtq6Wgb8Fjq2r1MpjIWwUvx84s= osmp-cloud" + provider.Prepare() + + provider.ListInstance() + spew.Dump(provider.Instances) +} + +func TestProviderCreate(t *testing.T) { + provider, err := InitProvider("do", sampleToken) + if err != nil { + t.Errorf("error ") + } + provider.SSHPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0ZmMumN5GTKuPXVVqugz+6BZs6JyNaQGsPgsZk86uON//oXi5fkutsNu5IIPDZXph3P5NbUj2dPalNyzNY5jClPJeT0F/eowIhPeo5GfjmJlXR4TTgHSwPYrOQNop2w+xh2z5h4IXMEPVLpEsN67MuUjTKzRwirwPjigZS/gayhwfOfDsPaIwBhpoBGuR1x+Rzzxuiy7TToNoWhF6pT9qONoCtr0VrPMsmjVpEPKD/uTW/8KeFL0pb/9z18M4IlbtvkO0Y6RhrpFGNSmZTWc1eDsJpFJerrVd48rgx3aRHriijl4zX4GBhc0zjqJwv+nGTGFPJ9Tx/3kPMDUGna/f91VU7sL7YqeiSed8S0YcWfntYy64OknvMpN8VIoQ7WiJAkR3wPw+tL3ZduXXAiKHFTAiXev02mOvo2F2nQKdGS98lOH5m+zuUm8abYbyXYlGNEzz576ksb6nMWCSSXwhA5f4clPKaPmgBQFQMUtq6Wgb8Fjq2r1MpjIWwUvx84s= osmp-cloud" + provider.Prepare() + + id, err := provider.CreateInstanceDO("example.com") + if err == nil { + provider.GetInstanceInfo(id) + } + spew.Dump(provider.Instances) +} + +func TestProviderDelete(t *testing.T) { + provider, err := InitProvider("do", sampleToken) + if err != nil { + t.Errorf("error ") + } + + provider.DeleteInstance("258443728") + spew.Dump(provider.Instances) +} + +func TestProviderAccount(t *testing.T) { + provider, err := InitProvider("do", sampleToken) + if err != nil { + t.Errorf("error ") + } + + provider.AccountDO() +} + +func TestProvider_DeleteSnapshot(t *testing.T) { + provider, err := InitProvider("do", sampleToken) + if err != nil { + t.Errorf("error ") + } + + provider.DeleteSnapShotDO("89053110") + provider.ListSnapshotDO() + spew.Dump(provider.OldSnapShotID) + spew.Dump(provider.SnapshotID) +} + +func TestProviderLN(t *testing.T) { + sampleToken = "6634197757df67b24753d0d241003ae09afd53bc7f9648c191e37acb61bfef37" + provider, err := InitProvider("linode", sampleToken) + if err != nil { + t.Errorf("error ") + } + provider.SSHPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0ZmMumN5GTKuPXVVqugz+6BZs6JyNaQGsPgsZk86uON//oXi5fkutsNu5IIPDZXph3P5NbUj2dPalNyzNY5jClPJeT0F/eowIhPeo5GfjmJlXR4TTgHSwPYrOQNop2w+xh2z5h4IXMEPVLpEsN67MuUjTKzRwirwPjigZS/gayhwfOfDsPaIwBhpoBGuR1x+Rzzxuiy7TToNoWhF6pT9qONoCtr0VrPMsmjVpEPKD/uTW/8KeFL0pb/9z18M4IlbtvkO0Y6RhrpFGNSmZTWc1eDsJpFJerrVd48rgx3aRHriijl4zX4GBhc0zjqJwv+nGTGFPJ9Tx/3kPMDUGna/f91VU7sL7YqeiSed8S0YcWfntYy64OknvMpN8VIoQ7WiJAkR3wPw+tL3ZduXXAiKHFTAiXev02mOvo2F2nQKdGS98lOH5m+zuUm8abYbyXYlGNEzz576ksb6nMWCSSXwhA5f4clPKaPmgBQFQMUtq6Wgb8Fjq2r1MpjIWwUvx84s= osmp-cloud" + //provider.GetSSHKey() + + provider.Prepare() + //id, _ := provider.CreateInstanceLN("new2") + //time.Sleep(90*time.Second) + //id := 29530061 + //provider.MountDiskLN(id) + provider.DeleteInstance("29530163") + //provider.AccountLN() + spew.Dump(provider.Instances) +} + +func TestProviderAccountAWS(t *testing.T) { + provider, err := InitProvider("aws", sampleToken) + if err != nil { + t.Errorf("error ") + } + + provider.AccountDO() +} diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..3f7a11f --- /dev/null +++ b/server/auth.go @@ -0,0 +1,68 @@ +package server + +import ( + "time" + + // jwtware "github.com/gofiber/jwt/v2" + + "github.com/dgrijalva/jwt-go" + "github.com/gofiber/fiber/v2" +) + +// authen stuff + +func Login(c *fiber.Ctx) error { + type LoginInput struct { + Username string `json:"username"` + Password string `json:"password"` + } + var input LoginInput + if err := c.BodyParser(&input); err != nil { + return c.SendStatus(fiber.StatusUnauthorized) + } + user := input.Username + pass := input.Password + + dbUser := Opt.Client.Username + dbPass := Opt.Client.Password + + // auto pass if -A is specify + if Opt.Server.NoAuthen { + user = dbUser + pass = dbPass + } + + // Throws Unauthorized error + if dbUser != user || dbPass != pass { + return c.SendStatus(fiber.StatusUnauthorized) + } + + // Create the Claims + claims := jwt.MapClaims{ + "name": "Osmdeus Default", + "admin": true, + "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte(Opt.Server.JWTSecret)) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(fiber.Map{"status": "success", "message": "Successfully login", "token": t}) + +} + +func jwtError(c *fiber.Ctx, err error) error { + if err.Error() == "Missing or malformed JWT" { + c.Status(fiber.StatusBadRequest) + return c.JSON(fiber.Map{"status": "error", "message": "Missing or malformed JWT", "data": nil}) + + } else { + c.Status(fiber.StatusUnauthorized) + return c.JSON(fiber.Map{"status": "error", "message": "Invalid or expired JWT", "data": nil}) + } +} diff --git a/server/builder.go b/server/builder.go new file mode 100644 index 0000000..eee0a13 --- /dev/null +++ b/server/builder.go @@ -0,0 +1,170 @@ +package server + +import ( + "fmt" + "github.com/gofiber/fiber/v2" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "os" + "path" + "strings" +) + +// UploadData data required in json form +type UploadData struct { + Data string `json:"data"` + Filename string `json:"filename"` +} + +// Upload testing authenticated connection +func Upload(c *fiber.Ctx) error { + var uploadData UploadData + + err := c.BodyParser(&uploadData) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Cannot parse JSON", + }) + } + + if uploadData.Filename == "" { + uploadData.Filename = utils.RandomString(6) + } + tmpFile := path.Base(utils.NormalizePath(uploadData.Filename)) + baseDir := fmt.Sprintf("/tmp/%v-input/", libs.BINARY) + if !utils.FolderExists(baseDir) { + os.MkdirAll(baseDir, 0755) + } + + filename := path.Join(baseDir, tmpFile) + data := uploadData.Data + + utils.WriteToFile(filename, data) + + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "filepath": filename, + }, + Type: "upload", + Message: "New Data Uploaded", + }) + +} + +// SaveTargets save upload data to /tmp/osm-input/data-osm-xxx.txt +func SaveTargets(targets []string) string { + tmpFile := fmt.Sprintf("data-%v-%v-%v.txt", utils.GetTS(), libs.BINARY, utils.RandomString(6)) + baseDir := fmt.Sprintf("/tmp/%v-input/", libs.BINARY) + if !utils.FolderExists(baseDir) { + os.MkdirAll(baseDir, 0755) + } + targetFile := path.Join(baseDir, tmpFile) + data := strings.Join(targets, "\n") + filename, err := utils.WriteToFile(targetFile, data) + if err != nil { + return "" + } + return filename +} + +// CommandBuilder build core command from API +func CommandBuilder(taskData *TaskData) string { + binary := fmt.Sprintf("%s scan", libs.BINARY) + if taskData.Distributed { + binary = fmt.Sprintf("%s cloud", libs.BINARY) + if taskData.Chunk { + binary = fmt.Sprintf("%s cloud --chunk", libs.BINARY) + } + } + + if taskData.ScanID != "" { + binary += fmt.Sprintf(" --sid %v ", taskData.ScanID) + } + + var command string + var workspace, concurrency, timeout, params, workflow, plugin, scanID string + + if len(taskData.TargetsList) > 0 { + taskData.TargetsFile = SaveTargets(taskData.TargetsList) + utils.DebugF("Save targets list to: %v", taskData.TargetsFile) + } + + // get workspace + if taskData.Workspace != "" { + taskData.Workspace = utils.CleanPath(taskData.Workspace) + workspace = fmt.Sprintf(" -w '%v'", taskData.Workspace) + } + + if taskData.Binary != "" { + binary = taskData.Binary + } + + //if taskData.RawName { + // binary = binary + " --rt " + //} + + if taskData.ViewOnly { + binary = binary + " --view-only " + } + + // default workflow is general + if taskData.WorkFlow != "" { + workflow = fmt.Sprintf(" -f '%v'", taskData.WorkFlow) + } + + // some mics options + if taskData.Timeout != "" { + timeout = fmt.Sprintf(" --timeout '%v'", taskData.Timeout) + } + if taskData.Concurrency > 0 { + concurrency = fmt.Sprintf(" -c %v", taskData.Concurrency) + } + if len(taskData.Params) > 0 { + for _, param := range taskData.Params { + // @NOTE replace ',' from request to ';;' first because corbra auto split ',' + if strings.Contains(param, ",") { + param = strings.Replace(param, ",", ";;", -1) + } + + if strings.HasPrefix(param, "'") && strings.HasSuffix(param, "'") { + params += fmt.Sprintf(" -p %s", param) + continue + } + params += fmt.Sprintf(" -p '%s'", param) + } + } + + if taskData.PluginName != "" { + plugin = fmt.Sprintf(" -m '%v'", taskData.PluginName) + } + + // override everything + if taskData.Command != "" { + return taskData.Command + } + + // mean general scan + if taskData.PluginName == "" { + command = fmt.Sprintf("%v %v -t %v %v%v%v%v", binary, workflow, taskData.Target, concurrency, timeout, workspace, params) + if taskData.TargetsFile != "" { + command = fmt.Sprintf("%v %v -T %v %v%v%v%v", binary, workflow, taskData.TargetsFile, concurrency, timeout, workspace, params) + } + command = strings.TrimSpace(command) + if taskData.Debug { + command = command + " --debug" + } + return command + } + + command = fmt.Sprintf("%v %v -t %v %v %v%v%v%v", binary, plugin, taskData.Target, scanID, concurrency, timeout, workspace, params) + if taskData.TargetsFile != "" { + command = fmt.Sprintf("%v %v -t %v %v %v%v%v%v", binary, plugin, taskData.TargetsFile, scanID, concurrency, timeout, workspace, params) + } + + command = strings.TrimSpace(command) + if taskData.Debug { + command = command + " --debug" + } + return command +} diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 0000000..43b1bb3 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,120 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag + +package docs + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/alecthomas/template" + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{.Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/v1/books": { + "get": { + "description": "Get all books", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "books" + ], + "summary": "Get all books", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponseHTTP" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/handler.ResponseHTTP" + } + } + } + } + } + }, + "definitions": { + "handler.ResponseHTTP": { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/server/docs/swagger.json b/server/docs/swagger.json new file mode 100644 index 0000000..041a76b --- /dev/null +++ b/server/docs/swagger.json @@ -0,0 +1,53 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/v1/books": { + "get": { + "description": "Get all books", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "books" + ], + "summary": "Get all books", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponseHTTP" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/handler.ResponseHTTP" + } + } + } + } + } + }, + "definitions": { + "handler.ResponseHTTP": { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 0000000..9b3a48a --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,33 @@ +definitions: + handler.ResponseHTTP: + properties: + data: + type: object + message: + type: string + success: + type: boolean + type: object +info: + contact: { } +paths: + /v1/books: + get: + consumes: + - application/json + description: Get all books + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ResponseHTTP' + "503": + description: Service Unavailable + schema: + $ref: '#/definitions/handler.ResponseHTTP' + summary: Get all books + tags: + - books +swagger: "2.0" diff --git a/server/mics.go b/server/mics.go new file mode 100644 index 0000000..ba035e4 --- /dev/null +++ b/server/mics.go @@ -0,0 +1,100 @@ +package server + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/j3ssie/osmedeus/core" + "github.com/j3ssie/osmedeus/execution" + "github.com/j3ssie/osmedeus/libs" +) + +func Process(c *fiber.Ctx) error { + processes := execution.GetOsmProcess("") + return c.JSON(ResponseHTTP{ + Status: 200, + Data: processes, + Type: "processes", + Total: len(processes), + Message: "List all osm process", + }) +} + +func RawWorkspace(c *fiber.Ctx) error { + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "storages": fmt.Sprintf("/%s/storages/", Opt.Server.StaticPrefix), + "workspaces": fmt.Sprintf("/%s/workspaces/", Opt.Server.StaticPrefix), + "logs": fmt.Sprintf("/%s/logs/", Opt.Server.StaticPrefix), + }, + Type: "raw", + Message: "Raw directory", + }) +} + +func ListFlows(c *fiber.Ctx) error { + flows := core.ListFlow(Opt) + if len(flows) == 0 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Can't list workflow", + }) + } + + var result []map[string]string + + for _, flow := range flows { + if flow != "" { + item := make(map[string]string) + item["name"] = strings.TrimSuffix(filepath.Base(flow), ".yaml") + + // get modules + Opt.Flow.Type = strings.TrimSuffix(item["name"], path.Ext(item["name"])) + rawModules := core.ListModules(Opt) + var modules []string + for _, module := range rawModules { + if module != "" { + modules = append(modules, strings.TrimSuffix(filepath.Base(module), ".yaml")) + } + } + + item["desc"] = "" + parsedFlow, err := core.ParseFlow(flow) + if err == nil { + item["desc"] = parsedFlow.Desc + } + + item["modules"] = strings.Join(modules, ",") + result = append(result, item) + + } + } + + return c.JSON(ResponseHTTP{ + Status: 200, + Data: result, + Total: len(flows), + Type: "flows", + Message: "Workflows Listing", + }) +} + +func HelperMessage(c *fiber.Ctx) error { + message := fmt.Sprintf(` +[*] Visit this page for complete Usage: %s +`, libs.DOCS) + + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "version": libs.VERSION, + "doc": libs.DOCS, + "message": message, + }, + Type: "helper", + Message: "Helper message", + }) +} diff --git a/server/ping.go b/server/ping.go new file mode 100644 index 0000000..9852e82 --- /dev/null +++ b/server/ping.go @@ -0,0 +1,41 @@ +package server + +import ( + "github.com/gofiber/fiber/v2" + "github.com/j3ssie/osmedeus/libs" +) + +// Ping is a function to get all books data from database +// @Summary Get all books +// @Description Get all books +// @Tags books +// @Accept json +// @Produce json +// @Success 200 {object} ResponseHTTP{} +// @Failure 503 {object} ResponseHTTP{} +// @Router /v1/books [get] +func Ping(c *fiber.Ctx) error { + return c.JSON(ResponseHTTP{ + Status: 200, + Message: "pong", + }) +} + +// Health is a function to get all books data from database +// @Summary Get all books +// @Description Get all books +// @Tags books +// @Accept json +// @Produce json +// @Success 200 {object} ResponseHTTP{} +// @Failure 503 {object} ResponseHTTP{} +// @Router /v1/books [get] +func Health(c *fiber.Ctx) error { + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "version": libs.VERSION, + }, + Message: "server is up", + }) +} diff --git a/server/router.go b/server/router.go new file mode 100644 index 0000000..033c5af --- /dev/null +++ b/server/router.go @@ -0,0 +1,152 @@ +package server + +import ( + "crypto/tls" + "fmt" + "log" + "net" + "net/http" + "os" + "path" + + "github.com/fatih/color" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + jwtware "github.com/gofiber/jwt/v2" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + + "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +// var DB *gorm.DB +var Opt libs.Options + +func StartServer(options libs.Options) { + Opt = options + var err error + + if options.Server.NoAuthen { + fmt.Fprintf(os.Stderr, color.RedString("[Critical] The server is currently being executed %v mechanism enabled.\n", color.HiYellowString("WITHOUT ANY AUTHENTICATION"))) + } + + app := fiber.New(fiber.Config{ + Prefork: options.Server.PreFork, + }) + app.Use(cors.New()) + SetupRoutes(app) + + // mean enable SSL + var enableSSL bool + var ln net.Listener + if !options.Server.DisableSSL { + err = EnableSSL(options) + if err == nil { + enableSSL = true + } + cer, err := tls.LoadX509KeyPair(options.Server.CertFile, options.Server.KeyFile) + if err != nil { + enableSSL = false + utils.ErrorF("error create ssl listener: %v", err) + } + config := &tls.Config{Certificates: []tls.Certificate{cer}} + + // Create custom listener + ln, err = tls.Listen("tcp", options.Server.Bind, config) + if err != nil { + utils.ErrorF("error create ssl listener: %v", err) + enableSSL = false + } + } + + if enableSSL { + utils.GoodF("Web UI available at: %v ", color.HiMagentaString("https://%v/ui/", options.Server.Bind)) + utils.GoodF("Static Content available at: %v", color.HiMagentaString("https://%v/%s/workspaces/", options.Server.Bind, Opt.Server.StaticPrefix)) + log.Fatal(app.Listener(ln)) + } else { + utils.GoodF("Web UI available at: http://%v/ui/", options.Server.Bind) + log.Fatal(app.Listen(options.Server.Bind)) + } +} + +// SetupRoutes setup router api +func SetupRoutes(app *fiber.App) { + // for UI + app.Static("/ui", Opt.Server.UIPath) + app.Get("/ui/*", func(ctx *fiber.Ctx) error { + return ctx.SendFile(path.Join(Opt.Server.UIPath, "index.html")) + }) + + // for static report file + app.Use(fmt.Sprintf("%s/workspaces/", Opt.Server.StaticPrefix), filesystem.New(filesystem.Config{ + Root: http.Dir(Opt.Env.WorkspacesFolder), + Browse: !Opt.Server.DisableWorkspaceListing, + MaxAge: 3600, + NotFoundFile: "", + })) + app.Use(fmt.Sprintf("%s/storages/", Opt.Server.StaticPrefix), filesystem.New(filesystem.Config{ + Root: http.Dir(Opt.Env.StoragesFolder), + Browse: !Opt.Server.DisableWorkspaceListing, + MaxAge: 3600, + NotFoundFile: "", + })) + + // for swagger document + //app.Get("/docs/*", swagger.Handler) // default + //app.Get("/docs/*", swagger.New(swagger.Config{ // custom + // URL: "https://osmp.io/doc.json", + // DeepLinking: false, + //})) + + app.Get("/ping", Ping) + api := app.Group("/api", logger.New()) + api.Post("/login", Login) + + // disable JWT Middleware when -A is set + if !Opt.Server.NoAuthen { + app.Use(jwtware.New(jwtware.Config{ + SigningKey: []byte(Opt.Server.JWTSecret), + Filter: nil, + SuccessHandler: nil, + ErrorHandler: jwtError, + SigningKeys: nil, + SigningMethod: "", + ContextKey: "", + Claims: nil, + TokenLookup: "", + AuthScheme: "Osmedeus", + })) + } + + // Middleware + osmp := api.Group("/osmp") + // /api/osmp/health + osmp.Get("/health", Health) + + // core API e.g: /api/osmp/workspaces + osmp.Get("/workspaces", ListWorkspaces) + osmp.Get("/workspace/:wsname/", WorkspaceDetail) + osmp.Get("/scans", ListAllScan) + osmp.Delete("/delete/:wsname/", DeleteWorkspace) + + osmp.Get("/ps", Process) + osmp.Get("/raw", RawWorkspace) + osmp.Get("/flows", ListFlows) + osmp.Get("/help", HelperMessage) + + //api.Use(basicauth.New(basicauth.Config{ + // Users: map[string]string{ + // Opt.Client.Username: Opt.Client.Password, + // }, + // Realm: "Forbidden", + // Unauthorized: func(c *fiber.Ctx) error { + // return c.SendString("404 not found") + // }, + //})) + // + + // execute endpoints + osmp.Post("/execute", NewScan) + osmp.Post("/upload", Upload) +} diff --git a/server/scan.go b/server/scan.go new file mode 100644 index 0000000..42ec9d6 --- /dev/null +++ b/server/scan.go @@ -0,0 +1,130 @@ +package server + +import ( + "github.com/gofiber/fiber/v2" + "github.com/j3ssie/osmedeus/utils" +) + +// TaskData data required in json form +type TaskData struct { + MasterPassword string `json:"password"` + Binary string `json:"binary"` + // override everything below + Command string `json:"command"` + + // these two not be blank when run with plugins + WorkFlow string `json:"workflow"` + PluginName string `json:"plugin"` + ScanID string `json:"scan_id"` + + // for select scan + task + Workspace string `json:"workspace"` + Target string `json:"target"` + TargetsList []string `json:"targets"` + TargetsFile string `json:"targets_file"` + + AliveAssets bool `json:"alive_assets"` // skip targets part and select the assets from DB + AllAssets bool `json:"all_assets"` // skip targets part and select the assets from DB + + // just more mics info for custom command later + Params []string `json:"params"` + Timeout string `json:"timeout"` + Concurrency int `json:"concurrency"` + + // enable distributed scan + Distributed bool `json:"distributed"` + + // for chunk mode only + Threads int `json:"threads"` + Chunk bool `json:"chunk"` + TargetAsFile bool `json:"as_file"` + + // only select record not run the command + RawName bool `json:"RawName"` + WildCard bool `json:"wildcard"` + ViewOnly bool `json:"view_only"` + Debug bool `json:"debug"` + Test bool `json:"test"` +} + +// NewScan new scan +func NewScan(c *fiber.Ctx) error { + var taskData TaskData + var invalid bool + + err := c.BodyParser(&taskData) + if err != nil { + invalid = true + } + + if invalid { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Cannot parse JSON", + }) + } + + taskData.Command = CommandBuilder(&taskData) + // get workspace if we didn't have one + if taskData.Workspace == "" { + taskData.Workspace = utils.CleanPath(taskData.Target) + } + utils.InforF("Running command: %v", taskData.Command) + + if !taskData.Test { + go func() { + utils.RunOSCommand(taskData.Command) + return + }() + } + + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "command": taskData.Command, + }, + Type: "new-scan", + Message: "New Scan Imported", + }) +} + +// NewScanCloud new scan +func NewScanCloud(c *fiber.Ctx) error { + var taskData TaskData + var invalid bool + + err := c.BodyParser(&taskData) + if err != nil { + invalid = true + } + + if invalid { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Cannot parse JSON", + }) + } + + taskData.Command = CommandBuilder(&taskData) + // get workspace if we didn't have one + if taskData.Workspace == "" { + taskData.Workspace = utils.CleanPath(taskData.Target) + } + utils.InforF("Running command: %v", taskData.Command) + + if !taskData.Test { + go func() { + utils.RunOSCommand(taskData.Command) + return + }() + } + + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "input": taskData.Target, + "scan_id": taskData.ScanID, + "workflow": taskData.WorkFlow, + }, + Type: "new-scan", + Message: "New Scan Imported", + }) +} diff --git a/server/ssl.go b/server/ssl.go new file mode 100644 index 0000000..53da63a --- /dev/null +++ b/server/ssl.go @@ -0,0 +1,191 @@ +package server + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Generate a self-signed X.509 certificate for a TLS server. Outputs to +// 'cert.pem' and 'key.pem' and will overwrite existing files. + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "github.com/j3ssie/osmedeus/libs" + "github.com/j3ssie/osmedeus/utils" + "log" + "math/big" + "net" + "os" + "strings" + "time" +) + +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + case ed25519.PrivateKey: + return k.Public().(ed25519.PublicKey) + default: + return nil + } +} + +// EnableSSL do some check when ssl enabled +func EnableSSL(options libs.Options) error { + if options.Server.DisableSSL { + return nil + } + + if utils.FileExists(options.Server.CertFile) && utils.FileExists(options.Server.KeyFile) { + return nil + } + + ok := GenerateSSL(options.Server.CertFile, options.Server.KeyFile) + if ok { + return nil + } + utils.ErrorF("error create ssl key at: %v", options.Server.CertFile) + return fmt.Errorf("error create SSL Key") +} + +// GenerateSSL generate SSL key +func GenerateSSL(certFile string, keyFile string) bool { + host := "localhost" // "Comma-separated hostnames and IPs to generate a certificate for + validFrom := "" // "Creation date formatted as Jan 1 15:04:05 2011 + validFor := 365 * 24 * time.Hour // "Duration that certificate is valid for + isCA := false // "whether this cert should be its own Certificate Authority + rsaBits := 4096 // "Size of RSA key to generate. Ignored if --ecdsa-curve is set + ecdsaCurve := "P256" // "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521 + ed25519Key := false // "Generate an Ed25519 key" + + var priv interface{} + var err error + switch ecdsaCurve { + case "": + if ed25519Key { + _, priv, err = ed25519.GenerateKey(rand.Reader) + } else { + priv, err = rsa.GenerateKey(rand.Reader, rsaBits) + } + case "P224": + priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + case "P256": + priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case "P384": + priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case "P521": + priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + default: + log.Fatalf("Unrecognized elliptic curve: %q", ecdsaCurve) + } + if err != nil { + log.Fatalf("Failed to generate private key: %v", err) + } + + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + if _, isRSA := priv.(*rsa.PrivateKey); isRSA { + keyUsage |= x509.KeyUsageKeyEncipherment + } + + var notBefore time.Time + if len(validFrom) == 0 { + notBefore = time.Now() + } else { + notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom) + if err != nil { + log.Fatalf("Failed to parse creation date: %v", err) + } + } + + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("Failed to generate serial number: %v", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Not Localhost"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + if isCA { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) + if err != nil { + utils.ErrorF("Failed to create certificate: %v", err) + return false + } + + certOut, err := os.Create(certFile) + if err != nil { + utils.ErrorF("Failed to open %s for writing: %v", certFile, err) + return false + } + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + utils.ErrorF("Failed to write data to cert.pem: %v", err) + return false + } + if err := certOut.Close(); err != nil { + utils.ErrorF("Error closing cert.pem: %v", err) + return false + } + //log.Print("wrote cert.pem\n") + + keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + utils.ErrorF("Failed to open %sfor writing: %v", keyFile, err) + return false + } + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + utils.ErrorF("Unable to marshal private key: %v", err) + return false + } + if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { + utils.ErrorF("Failed to write data to key.pem: %v", err) + return false + } + if err := keyOut.Close(); err != nil { + utils.ErrorF("Error closing key.pem: %v", err) + return false + } + //log.Print("wrote key.pem\n") + return true +} diff --git a/server/workspace.go b/server/workspace.go new file mode 100644 index 0000000..3a1746e --- /dev/null +++ b/server/workspace.go @@ -0,0 +1,118 @@ +package server + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/j3ssie/osmedeus/database" + "github.com/j3ssie/osmedeus/utils" + "github.com/thoas/go-funk" +) + +// ResponseHTTP represents response body of this API +type ResponseHTTP struct { + Status int `json:"status"` + Data interface{} `json:"data"` + Type string `json:"type,omitempty"` + Total int `json:"total,omitempty"` + Message string `json:"message"` +} + +// Workspace is a function to get all books data from database +// @Summary Get all books +// @Description Get all books +// @Tags books +// @Accept json +// @Produce json +// @Success 200 {object} ResponseHTTP{} +// @Router /v1/workspaces [get] +func ListWorkspaces(c *fiber.Ctx) error { + workspaces := database.GetAllScan(Opt) + return c.JSON(ResponseHTTP{ + Status: 200, + Data: workspaces, + Type: "workspaces", + Total: len(workspaces), + Message: "List all of Workspaces", + }) +} + +func WorkspaceDetail(c *fiber.Ctx) error { + wsname := c.Params("wsname") + workspace := database.GetSingleScan(wsname, Opt) + + // make a reports map + rawReports := workspace.Target.Reports + reports := make(map[string][]string) + + for _, report := range rawReports { + // replace the home folder first + if strings.Contains(report.ReportPath, workspace.Target.Workspace) { + // /root/workspaces-osmedeus/ + homeFolder := strings.Split(report.ReportPath, workspace.Target.Workspace)[0] + report.ReportPath = strings.ReplaceAll(report.ReportPath, homeFolder, Opt.Env.WorkspacesFolder+"/") + } + + if utils.FileLength(report.ReportPath) == 0 { + if utils.FolderExists(report.ReportPath) && utils.FolderLength(report.ReportPath) == 0 { + continue + } else { + continue + } + } + + if strings.HasPrefix(report.ReportPath, Opt.Env.WorkspacesFolder) { + report.ReportPath = strings.ReplaceAll(report.ReportPath, Opt.Env.WorkspacesFolder, fmt.Sprintf("/%v/workspaces", Opt.Server.StaticPrefix)) + } else if strings.HasPrefix(report.ReportPath, Opt.Env.StoragesFolder) { + report.ReportPath = strings.ReplaceAll(report.ReportPath, Opt.Env.StoragesFolder, fmt.Sprintf("/%v/storages", Opt.Server.StaticPrefix)) + } + + if !funk.Contains(reports[report.Module], report.ReportPath) { + reports[report.Module] = append(reports[report.Module], report.ReportPath) + } + } + + return c.JSON(ResponseHTTP{ + Status: 200, + Data: fiber.Map{ + "workspace": workspace, + "reports": reports, + }, + Type: "workspace", + Message: "Workspace Detail ", + }) +} + +func ListAllScan(c *fiber.Ctx) error { + scan := database.GetScanProgress(Opt) + return c.JSON(ResponseHTTP{ + Status: 200, + Data: scan, + Type: "scans", + Total: len(scan), + Message: "List all the scan process", + }) +} + +func DeleteWorkspace(c *fiber.Ctx) error { + wsname := c.Params("wsname") + wsDir := path.Join(Opt.Env.WorkspacesFolder, utils.NormalizePath(wsname)) + if !utils.FolderExists(wsDir) { + wsDir = path.Join(Opt.Env.WorkspacesFolder, utils.StripPath(wsname)) + if !utils.FolderExists(wsDir) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "workspace didn't exist", + }) + } + } + + os.RemoveAll(wsDir) + return c.JSON(ResponseHTTP{ + Status: 200, + Type: "delete", + Message: "Workspace Deleted", + }) +} diff --git a/test-workflows/flow-with-params.yaml b/test-workflows/flow-with-params.yaml new file mode 100644 index 0000000..0a7fb01 --- /dev/null +++ b/test-workflows/flow-with-params.yaml @@ -0,0 +1,11 @@ +name: flow-with-params +desc: run normal routine +type: sample + +params: + - firstTimeout: '5s' + +routines: + - modules: + - timeout-module + diff --git a/test-workflows/general.yaml b/test-workflows/general.yaml new file mode 100644 index 0000000..9510506 --- /dev/null +++ b/test-workflows/general.yaml @@ -0,0 +1,31 @@ +name: general +desc: run normal routine +type: general +validator: domain + +routines: + - modules: + - subdomain + - modules: + - probing + - modules: + - fingerprint + - modules: + - portscan + - modules: + - ipspace + - modules: + - archive + - modules: + - spider + - modules: + - dirbscan + - modules: + - vulnscan + - modules: + - cloudbrute + - modules: + - credintel + - modules: + - summary + diff --git a/test-workflows/general/archive.yaml b/test-workflows/general/archive.yaml new file mode 100644 index 0000000..bba8b29 --- /dev/null +++ b/test-workflows/general/archive.yaml @@ -0,0 +1,10 @@ +name: archive +desc: archive + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportArchive("{{testData}}/{{Workspace}}/archive/archive.txt") \ No newline at end of file diff --git a/test-workflows/general/cloudbrute.yaml b/test-workflows/general/cloudbrute.yaml new file mode 100644 index 0000000..dc5337d --- /dev/null +++ b/test-workflows/general/cloudbrute.yaml @@ -0,0 +1,10 @@ +name: cloudbrute +desc: cloudbrute + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportCloudBrute("{{testData}}/{{Workspace}}/clouds-data.txt") diff --git a/test-workflows/general/credintel.yaml b/test-workflows/general/credintel.yaml new file mode 100644 index 0000000..6fe2ba4 --- /dev/null +++ b/test-workflows/general/credintel.yaml @@ -0,0 +1,10 @@ +name: credintel +desc: credintel + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportCred("{{testData}}/{{Workspace}}/new-intel-cred.txt") diff --git a/test-workflows/general/dirbscan.yaml b/test-workflows/general/dirbscan.yaml new file mode 100644 index 0000000..7ad2bb2 --- /dev/null +++ b/test-workflows/general/dirbscan.yaml @@ -0,0 +1,10 @@ +name: dirbscan +desc: dirbscan + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportDirectoryJson("{{testData}}/{{Workspace}}/directory/directory-json.txt") diff --git a/test-workflows/general/fingerprint.yaml b/test-workflows/general/fingerprint.yaml new file mode 100644 index 0000000..84fc4c2 --- /dev/null +++ b/test-workflows/general/fingerprint.yaml @@ -0,0 +1,13 @@ +name: fingerprint +desc: Scanning for subdomain + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportTech("{{testData}}/{{Workspace}}/fingerprint/{{Workspace}}-technologies.txt") + - ImportHTTPJson("{{testData}}/{{Workspace}}/fingerprint/{{Workspace}}-technologies.txt") + - ImportScreenShotJson("{{testData}}/{{Workspace}}/screenshot/goverview/screenshot-summary.txt") + - ImportHTTPJson("{{testData}}/{{Workspace}}/screenshot/goverview/content-summary.txt") \ No newline at end of file diff --git a/test-workflows/general/ipspace.yaml b/test-workflows/general/ipspace.yaml new file mode 100644 index 0000000..ef22a7d --- /dev/null +++ b/test-workflows/general/ipspace.yaml @@ -0,0 +1,10 @@ +name: ipspace +desc: ipspace + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportIPRange("{{testData}}/{{Workspace}}/ipspace/ipspace-summary.txt") \ No newline at end of file diff --git a/test-workflows/general/portscan.yaml b/test-workflows/general/portscan.yaml new file mode 100644 index 0000000..3f9a8dc --- /dev/null +++ b/test-workflows/general/portscan.yaml @@ -0,0 +1,10 @@ +name: portscan +desc: Scanning for subdomain + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportPortJson("{{testData}}/{{Workspace}}/portscan/portscan-json.txt") diff --git a/test-workflows/general/probing.yaml b/test-workflows/general/probing.yaml new file mode 100644 index 0000000..898d5d7 --- /dev/null +++ b/test-workflows/general/probing.yaml @@ -0,0 +1,10 @@ +name: probing +desc: Scanning for subdomain + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportDns("{{testData}}/{{Workspace}}/probing/dns-{{Workspace}}.txt") diff --git a/test-workflows/general/spider.yaml b/test-workflows/general/spider.yaml new file mode 100644 index 0000000..ecebbd4 --- /dev/null +++ b/test-workflows/general/spider.yaml @@ -0,0 +1,10 @@ +name: spider +desc: spider + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportLinks("{{testData}}/{{Workspace}}/linkfinding/links-json.txt") diff --git a/test-workflows/general/subdomain.yaml b/test-workflows/general/subdomain.yaml new file mode 100644 index 0000000..7c939f8 --- /dev/null +++ b/test-workflows/general/subdomain.yaml @@ -0,0 +1,10 @@ +name: subdomain +desc: Scanning for subdomain + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportSubdomain("{{testData}}/{{Workspace}}/subdomain/final-{{Workspace}}.txt") diff --git a/test-workflows/general/summary.yaml b/test-workflows/general/summary.yaml new file mode 100644 index 0000000..345cb77 --- /dev/null +++ b/test-workflows/general/summary.yaml @@ -0,0 +1,10 @@ +name: summary +desc: summary + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - SummaryTarget() \ No newline at end of file diff --git a/test-workflows/general/vulnscan.yaml b/test-workflows/general/vulnscan.yaml new file mode 100644 index 0000000..0057805 --- /dev/null +++ b/test-workflows/general/vulnscan.yaml @@ -0,0 +1,11 @@ +name: vulnscan +desc: vulnscan + +params: + - testData: "~/go/src/github.com/j3ssie/osmedeus/test-data" + +steps: + # get data from cdn + - scripts: + - ImportJaelesVulnJson("{{testData}}/{{Workspace}}/vuln/active/jaeles-summary.txt") + - ImportNucleiVulnJson("{{testData}}/{{Workspace}}/vuln/nuclei-sto.txt") diff --git a/test-workflows/parallel.yaml b/test-workflows/parallel.yaml new file mode 100644 index 0000000..ff386d2 --- /dev/null +++ b/test-workflows/parallel.yaml @@ -0,0 +1,14 @@ +name: flow-with-params +desc: run normal routine +type: sample + +params: + - firstTimeout: '5s' + +routines: + - modules: + - parallel + - parallel2 + - modules: + - timeout-module + diff --git a/test-workflows/pre-run-cloud.yaml b/test-workflows/pre-run-cloud.yaml new file mode 100644 index 0000000..8ac58d6 --- /dev/null +++ b/test-workflows/pre-run-cloud.yaml @@ -0,0 +1,17 @@ +name: test +desc: testing workflow +type: test +validator: domain + +params: + - repo: "test-module" + +local_pre_run: + - 'RRSync("root@{{RemoteIP}}", "/tmp/sam/", "/tmp/sam/")' + - ExecCmd('echo --> {{repo}}') + +routines: + - modules: + - sub-test + - modules: + - prob-test diff --git a/test-workflows/sample/parallel.yaml b/test-workflows/sample/parallel.yaml new file mode 100644 index 0000000..f3862ca --- /dev/null +++ b/test-workflows/sample/parallel.yaml @@ -0,0 +1,20 @@ +name: parallel1 +desc: Run dirbscan scan on list of HTTP file + +params: + - limit: '5000' + +steps: + - label: 'Start step 111' + commands: + - "echo '---> {{Target}} '" + - "seq {{limit}} > /tmp/source.txt" + + - source: '/tmp/source.txt' + threads: '200' + commands: + - "echo '---> [[.line]]'" + # - "sleep 1 && echo '---> done [[.line]]'" + + - commands: + - "echo 'done parallel 111111'" \ No newline at end of file diff --git a/test-workflows/sample/parallel2.yaml b/test-workflows/sample/parallel2.yaml new file mode 100644 index 0000000..093ac83 --- /dev/null +++ b/test-workflows/sample/parallel2.yaml @@ -0,0 +1,12 @@ +name: parallel2 +desc: Run dirbscan scan on list of HTTP file + +steps: + - label: 'Start step 222' + commands: + - "sleep 5" + - "echo '==>>>> {{Target}} '" + + - commands: + - "echo 'done parallel 2222'" + \ No newline at end of file diff --git a/test-workflows/sample/timeout-module.yaml b/test-workflows/sample/timeout-module.yaml new file mode 100644 index 0000000..99df50c --- /dev/null +++ b/test-workflows/sample/timeout-module.yaml @@ -0,0 +1,6 @@ +name: timeout-module +desc: + +steps: + - commands: + - "echo '==> Done here <==" \ No newline at end of file diff --git a/test-workflows/serial.yaml b/test-workflows/serial.yaml new file mode 100644 index 0000000..e439d22 --- /dev/null +++ b/test-workflows/serial.yaml @@ -0,0 +1,12 @@ +name: flow-with-params +desc: run normal routine +type: sample + +params: + - firstTimeout: '5s' + +routines: + - modules: + - parallel + + diff --git a/test-workflows/test-module/loop-step.yaml b/test-workflows/test-module/loop-step.yaml new file mode 100644 index 0000000..0c7e421 --- /dev/null +++ b/test-workflows/test-module/loop-step.yaml @@ -0,0 +1,20 @@ +name: loop-step + +report: + final: + - "/tmp/ott/source.txt" + +steps: + - scripts: + - "ExecCmd('mkdir -p /tmp/ott/')" + - "ExecCmd('seq 4 > /tmp/ott/source.txt')" + + - source: '/tmp/ott/source.txt' + threads: '{{ 9 / 2 }}' + commands: + - "echo '---> {{line}} '" + - "sleep 1 && echo '---> done {{line}}'" + + - scripts: + - "ExecCmd('sleep 3')" + - "ExecCmd('echo '<======')" diff --git a/test-workflows/test-module/markdown-generate.yaml b/test-workflows/test-module/markdown-generate.yaml new file mode 100644 index 0000000..34dc94b --- /dev/null +++ b/test-workflows/test-module/markdown-generate.yaml @@ -0,0 +1,15 @@ +name: markdown-generate +desc: HTML report generator based on markdown template + +# go run main.go scan -m test-workflows/test-module/markdown-generate.yaml --debug -t target.io + +report: + final: + - "{{Output}}/subdomain/final-{{Workspace}}.txt" + - "{{Output}}/vuln/active/{{Workspace}}-report.html" + - "{{Output}}/vuln/active/jaeles-summary.txt" + +steps: + - scripts: + - GenMarkdownReport("{{Data}}/markdown/general-template.md", "{{Output}}/summary.html") + - GenMarkdownReport("{{Data}}/markdown/simple-template.md", "{{Output}}/simple.html") diff --git a/test-workflows/test-module/ose.yaml b/test-workflows/test-module/ose.yaml new file mode 100644 index 0000000..f5cfef0 --- /dev/null +++ b/test-workflows/test-module/ose.yaml @@ -0,0 +1,20 @@ +name: test-ose +desc: test-ose + + +steps: + # get data from cdn + - ose: + # run the script directly + - | + if (FileLength('/tmp/ott/sam') > 0) { + ExecCmd('touch /tmp/ott/from-ose && sleep 10') + } + # run the script directly + - | + if (FileLength('/tmp/ott/sam') > 0) { + ExecCmdB('touch /tmp/ott/after-5-ose && sleep 10'); + ExecCmdB('touch /tmp/ott/after-10-ose'); + } + # this will get JS file from ~/osmedeus-plugins/ose/sample.js + - sample.js \ No newline at end of file diff --git a/test-workflows/test-module/s3-cdn.yaml b/test-workflows/test-module/s3-cdn.yaml new file mode 100644 index 0000000..e2f2fed --- /dev/null +++ b/test-workflows/test-module/s3-cdn.yaml @@ -0,0 +1,8 @@ +name: s3-cdn + +steps: + - scripts: + - "ExecCmd('mkdir -p /tmp/ott/')" + - "ExecCmd('seq 10 > /tmp/ott/source.txt')" + - "UploadToS3('/tmp/ott/on-s3.txt')" + - "DownloadFromS3('/tmp/ott/on-s3.txt', '/tmp/on-local-s3.txt')" diff --git a/test-workflows/test-module/set-var.yaml b/test-workflows/test-module/set-var.yaml new file mode 100644 index 0000000..e0da42f --- /dev/null +++ b/test-workflows/test-module/set-var.yaml @@ -0,0 +1,9 @@ +name: set OS Variable + +steps: + + - scripts: + - "SetOSVar('FROM_OSM', 'IS_TRUE')" + - commands: + - echo "=> \$FROM_OSM = $FROM_OSM" + diff --git a/test-workflows/test-module/test-parallel.yaml b/test-workflows/test-module/test-parallel.yaml new file mode 100644 index 0000000..55ce33f --- /dev/null +++ b/test-workflows/test-module/test-parallel.yaml @@ -0,0 +1,18 @@ +name: partest +desc: partest + +params: + - testFile: "/tmp/partest.txt" + - splitLines: "10" + +steps: + - required: + - "{{testFile}}" + scripts: + - SplitFile("{{testFile}}", "{{Workspace}}-index", {{splitLines}}, "/tmp/pt") + + - label: 'Parallel test' + source: "{{testFile}}" + parallel: 1 + commands: + - "echo [[.line]]" \ No newline at end of file diff --git a/test-workflows/test-module/with-threadshold.yaml b/test-workflows/test-module/with-threadshold.yaml new file mode 100644 index 0000000..b44fb9a --- /dev/null +++ b/test-workflows/test-module/with-threadshold.yaml @@ -0,0 +1,14 @@ +name: with-threads-hold + +steps: + - commands: + - "echo 'calc ---> {{ 5555 * threads}} '" + scripts: + - "ExecCmd('mkdir -p /tmp/ott/')" + - "ExecCmd('seq 10 > /tmp/ott/source.txt')" + + - source: '/tmp/ott/source.txt' + threads: '2' + commands: + - "echo '---> {{ 2 * threads}} '" + # - "sleep 1 && echo '---> done [[.line]]'" diff --git a/utils/helper.go b/utils/helper.go new file mode 100644 index 0000000..0ad16fe --- /dev/null +++ b/utils/helper.go @@ -0,0 +1,897 @@ +package utils + +import ( + "archive/zip" + "bufio" + "bytes" + "context" + "crypto/sha1" + "encoding/base64" + "fmt" + "io" + "math/rand" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + + //"syscall" + "text/template" + "time" + + "github.com/mitchellh/go-homedir" +) + +// CalcTimeout calculate timeout +func CalcTimeout(raw string) int { + raw = strings.ToLower(strings.TrimSpace(raw)) + seconds := raw + multiply := 1 + + matched, _ := regexp.MatchString(`.*[a-z]`, raw) + if matched { + unitTime := fmt.Sprintf("%c", raw[len(raw)-1]) + seconds = raw[:len(raw)-1] + switch unitTime { + case "s": + multiply = 1 + break + case "m": + multiply = 60 + break + case "h": + multiply = 3600 + break + } + } + + timeout, err := strconv.Atoi(seconds) + if err != nil { + return 0 + } + return timeout * multiply +} + +// GetDomain get domain from the URL +func GetDomain(raw string) (string, error) { + u, err := url.Parse(raw) + if err == nil { + return u.Hostname(), nil + } + return raw, err +} + +// EmptyDir check if directory is empty or not +func EmptyDir(dir string) bool { + if !FolderExists(dir) { + return true + } + f, err := os.Open(NormalizePath(dir)) + if err != nil { + return false + } + defer f.Close() + + _, err = f.Readdirnames(1) + if err == io.EOF { + return true + } + return false +} + +// EmptyFile check if file is empty or not +func EmptyFile(filename string, num int) bool { + filename = NormalizePath(filename) + if !FileExists(filename) { + return true + } + data := ReadingLines(filename) + if len(data) > num { + return false + } + return true +} + +// StrToInt string to int +func StrToInt(data string) int { + i, err := strconv.Atoi(data) + if err != nil { + return 0 + } + return i +} + +// GetOSEnv get environment variable +func GetOSEnv(name string, defaultValue string) string { + variable, ok := os.LookupEnv(name) + if !ok { + if defaultValue != "" { + return defaultValue + } + return name + } + return variable +} + +// MakeDir just make a folder +func MakeDir(folder string) { + folder = NormalizePath(folder) + os.MkdirAll(folder, 0750) +} + +// GetCurrentDay get current day +func GetCurrentDay() string { + currentTime := time.Now() + return fmt.Sprintf("%v", currentTime.Format("2006-01-02_3:4:5")) +} + +// NormalizePath the path +func NormalizePath(path string) string { + if strings.HasPrefix(path, "~") { + path, _ = homedir.Expand(path) + } + return path +} + +// GetFileContent Reading file and return content of it +func GetFileContent(filename string) string { + var result string + if strings.Contains(filename, "~") { + filename, _ = homedir.Expand(filename) + } + file, err := os.Open(filename) + if err != nil { + return result + } + defer file.Close() + b, err := io.ReadAll(file) + if err != nil { + return result + } + return string(b) +} + +// ReadingLines Reading file and return content as []string +func ReadingLines(filename string) []string { + var result []string + if strings.HasPrefix(filename, "~") { + filename, _ = homedir.Expand(filename) + } + file, err := os.Open(filename) + if err != nil { + return result + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + val := strings.TrimSpace(scanner.Text()) + if val == "" { + continue + } + result = append(result, val) + } + + if err := scanner.Err(); err != nil { + return result + } + return result +} + +// Cat Reading file and return content as []string +func Cat(filename string) { + filename = NormalizePath(filename) + if !FileExists(filename) { + return + } + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + fmt.Println(line) + } + return +} + +// ReadingFileUnique Reading file and return content as []string +func ReadingFileUnique(filename string) []string { + var result []string + if strings.Contains(filename, "~") { + filename, _ = homedir.Expand(filename) + } + file, err := os.Open(filename) + if err != nil { + return result + } + defer file.Close() + + seen := make(map[string]bool) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + val := strings.TrimSpace(scanner.Text()) + // unique stuff + if val == "" { + continue + } + if seen[val] { + continue + } + + seen[val] = true + result = append(result, val) + } + + if err := scanner.Err(); err != nil { + return result + } + return result +} + +// WriteToFile write string to a file +func WriteToFile(filename string, data string) (string, error) { + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return "", err + } + defer file.Close() + + _, err = io.WriteString(file, data+"\n") + if err != nil { + return "", err + } + return filename, file.Sync() +} + +// AppendToContent append string to a file +func AppendToContent(filename string, data string) (string, error) { + // If the file doesn't exist, create it, or append to the file + f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return "", err + } + if _, err := f.Write([]byte(data + "\n")); err != nil { + return "", err + } + if err := f.Close(); err != nil { + return "", err + } + return filename, nil +} + +// FileExists check if file is exist or not +func FileExists(filename string) bool { + filename = NormalizePath(filename) + _, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return true +} + +// FolderExists check if file is exist or not +func FolderExists(foldername string) bool { + foldername = NormalizePath(foldername) + if _, err := os.Stat(foldername); os.IsNotExist(err) { + return false + } + return true +} + +// FileLength count len of file +func FileLength(filename string) int { + filename = NormalizePath(filename) + if !FileExists(filename) { + return 0 + } + return CountLines(filename) +} + +// DirLength count len of file +func DirLength(dir string) int { + dir = NormalizePath(dir) + files, err := os.ReadDir(dir) + if err != nil { + dir = dir + "/" + files, err = os.ReadDir(dir) + if err == nil { + return len(files) + } + return 0 + } + return len(files) +} + +// Copy append content to a file +func Copy(src string, dest string) { + src = NormalizePath(src) + dest = NormalizePath(dest) + if !FileExists(src) || FileLength(src) <= 0 { + return + } + input, _ := os.ReadFile(src) + os.WriteFile(dest, input, 0644) +} + +// GetTS get current timestamp and return a string +func GetTS() string { + return strconv.FormatInt(time.Now().Unix(), 10) +} + +// GenHash gen SHA1 hash from string +func GenHash(text string) string { + h := sha1.New() + h.Write([]byte(text)) + hashed := h.Sum(nil) + return fmt.Sprintf("%x", hashed) +} + +// GetFileSize get file size of a file in GB +func GetFileSize(src string) float64 { + var sizeGB float64 + fi, err := os.Stat(NormalizePath(src)) + if err != nil { + return sizeGB + } + // get the size + size := fi.Size() + sizeGB = float64(size) / (1024 * 1024 * 1024) + return sizeGB +} + +// RandomString return a random string with length +func RandomString(n int) string { + var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) + var letter = []rune("abcdefghijklmnopqrstuvwxyz") + b := make([]rune, n) + for i := range b { + b[i] = letter[seededRand.Intn(len(letter))] + } + return string(b) +} + +// runCmdWithOutput just run os command +func runCmdWithOutput(cmd string) string { + DebugF("Execute: %s", cmd) + command := []string{ + "bash", + "-c", + cmd, + } + realCmd := exec.Command(command[0], command[1:]...) + // output command output to std too + output, _ := realCmd.CombinedOutput() + return string(output) +} + +// RunCmdWithOutput run command with timeout +func RunCmdWithOutput(command string, timeoutRaw ...string) string { + if len(timeoutRaw) == 0 { + return runCmdWithOutput(command) + } + + timeout := CalcTimeout(timeoutRaw[0]) + DebugF("Run command with %v seconds timeout", timeout) + var out string + + c := context.Background() + deadline := time.Now().Add(time.Duration(timeout) * time.Second) + c, cancel := context.WithDeadline(c, deadline) + defer cancel() + go func() { + out = RunCmdWithOutput(command) + cancel() + }() + + select { + case <-c.Done(): + return out + case <-time.After(time.Duration(timeout) * time.Second): + return out + "\n[err] command got timeout" + } +} + +func runCommandWithError(cmd string) (string, error) { + DebugF("Execute: %s", cmd) + command := []string{ + "bash", + "-c", + cmd, + } + var output string + realCmd := exec.Command(command[0], command[1:]...) + + // output command output to std too + cmdReader, _ := realCmd.StdoutPipe() + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + out := scanner.Text() + DebugF(out) + output += out + "\n" + } + }() + if err := realCmd.Start(); err != nil { + return output, err + } + if err := realCmd.Wait(); err != nil { + return output, err + } + return output, nil +} + +// RunCommandWithErr Run a command +func RunCommandWithErr(command string, timeoutRaw ...string) (string, error) { + if len(timeoutRaw) == 0 { + return runCommandWithError(command) + } + var output string + var err error + + timeout := CalcTimeout(timeoutRaw[0]) + DebugF("Run command with %v seconds timeout", timeout) + var out string + + c := context.Background() + deadline := time.Now().Add(time.Duration(timeout) * time.Second) + c, cancel := context.WithDeadline(c, deadline) + defer cancel() + go func() { + out, err = runCommandWithError(command) + cancel() + }() + + select { + case <-c.Done(): + return output, err + case <-time.After(time.Duration(timeout) * time.Second): + return out, fmt.Errorf("command got timeout") + } +} + +func RunCommandSteamOutput(cmd string) (string, error) { + DebugF("Execute: %s", cmd) + command := []string{ + "bash", + "-c", + cmd, + } + var output string + realCmd := exec.Command(command[0], command[1:]...) + + // output command output to std too + cmdReader, _ := realCmd.StdoutPipe() + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + out := scanner.Text() + fmt.Println(out) + output += out + } + }() + if err := realCmd.Start(); err != nil { + return output, err + } + if err := realCmd.Wait(); err != nil { + return output, err + } + return output, nil +} + +func RunOSCommand(cmd string) (string, error) { + DebugF("Execute: %s", cmd) + command := []string{ + "bash", + "-c", + cmd, + } + var output string + realCmd := exec.Command(command[0], command[1:]...) + + // output command output to std too + cmdReader, _ := realCmd.StdoutPipe() + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + out := scanner.Text() + DebugF(out) + output += out + } + }() + if err := realCmd.Start(); err != nil { + return output, err + } + if err := realCmd.Wait(); err != nil { + return output, err + } + return output, nil +} + +// RunCommandWithoutOutput Run a command +func RunCommandWithoutOutput(cmd string) error { + command := []string{ + "bash", + "-c", + cmd, + } + DebugF("[Exec] %v", command) + realCmd := exec.Command(command[0], command[1:]...) + cmdReader, _ := realCmd.StdoutPipe() + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + InforF(scanner.Text()) + } + }() + if err := realCmd.Start(); err != nil { + return err + } + if err := realCmd.Wait(); err != nil { + return err + } + return nil +} + +// StripPath just Base64 Encode +func StripPath(raw string) string { + raw = NormalizePath(raw) + raw = strings.Replace(raw, "/", "_", -1) + raw = strings.Replace(raw, "..", "_", -1) + return raw +} + +// ZippedFolder zip a folder +func ZippedFolder(src string, dest string) error { + baseDest := path.Base(dest) + if FileExists(baseDest) { + os.RemoveAll(baseDest) + } + file, err := os.Create(dest + ".zip") + if err != nil { + return err + } + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + walker := func(asbPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + file, err := os.Open(asbPath) + if err != nil { + return err + } + defer file.Close() + relPath := strings.Replace(asbPath, src, baseDest, -1) + f, err := w.Create(relPath) + if err != nil { + return err + } + + _, err = io.Copy(f, file) + if err != nil { + return err + } + + return nil + } + err = filepath.Walk(src, walker) + if err != nil { + return err + } + return nil +} + +// CountLines Return the lines amount of the file +func CountLines(filename string) int { + var amount int + if strings.HasPrefix(filename, "~") { + filename, _ = homedir.Expand(filename) + } + file, err := os.Open(filename) + if err != nil { + return amount + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + val := strings.TrimSpace(scanner.Text()) + if val == "" { + continue + } + amount++ + } + + if err := scanner.Err(); err != nil { + return amount + } + return amount +} + +// CleanPath get environment variable +func CleanPath(raw string) string { + var out string + raw = NormalizePath(raw) + base := raw + if FileExists(base) { + base = filepath.Base(raw) + } + + if strings.Count(base, "/") > 2 { + base = base[strings.LastIndex(base, "/")+1:] + if strings.TrimSpace(base) == "" { + domain, err := GetDomain(raw) + if err == nil { + base = domain + } else { + base = RandomString(8) + } + } + } + + out = strings.ReplaceAll(base, "/", "_") + out = strings.ReplaceAll(out, ":", "_") + // DebugF("CleanPath: %s -- %s", raw, out) + return out +} + +func IsFile(src string) bool { + fi, err := os.Stat(NormalizePath(src)) + if err != nil { + return false + } + switch mode := fi.Mode(); { + case mode.IsDir(): + return false + case mode.IsRegular(): + if FileLength(src) > 0 { + return true + } + return false + } + return false +} + +func FolderLength(dir string) int { + dir = NormalizePath(dir) + var length int + if FileExists(dir) { + dir = path.Dir(dir) + } + files, err := os.ReadDir(dir) + if err != nil { + return length + } + length = len(files) + return length +} + +// ImageAsBase64 read image file as a string +func ImageAsBase64(src string) string { + src = NormalizePath(src) + f, err := os.Open(src) + if err != nil { + ErrorF("File not found: %v", src) + return "" + } + + // Read entire JPG into byte slice. + reader := bufio.NewReader(f) + content, err := io.ReadAll(reader) + if err != nil { + return "" + } + // Encode as base64. + encoded := base64.StdEncoding.EncodeToString(content) + return encoded +} + +// Base64Encode read image file as a string +func Base64Encode(raw string) string { + return base64.StdEncoding.EncodeToString([]byte(raw)) +} + +const bufSize = 1024 + +// OffsetRange represents a content block of a file. +type OffsetRange struct { + File string + Start int64 + Stop int64 +} + +// SplitLineChunks splits file into chunks. +// The whole line are guaranteed to be split in the same chunk. +func SplitLineChunks(filename string, chunks int) ([]OffsetRange, error) { + info, err := os.Stat(filename) + if err != nil { + return nil, err + } + + if chunks <= 1 { + return []OffsetRange{ + { + File: filename, + Start: 0, + Stop: info.Size(), + }, + }, nil + } + + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var ranges []OffsetRange + var offset int64 + // avoid the last chunk too few bytes + preferSize := info.Size()/int64(chunks) + 1 + for { + if offset+preferSize >= info.Size() { + ranges = append(ranges, OffsetRange{ + File: filename, + Start: offset, + Stop: info.Size(), + }) + break + } + + offsetRange, err := nextRange(file, offset, offset+preferSize) + if err != nil { + return nil, err + } + + ranges = append(ranges, offsetRange) + if offsetRange.Stop < info.Size() { + offset = offsetRange.Stop + } else { + break + } + } + + return ranges, nil +} + +func nextRange(file *os.File, start, stop int64) (OffsetRange, error) { + offset, err := skipPartialLine(file, stop) + if err != nil { + return OffsetRange{}, err + } + + return OffsetRange{ + File: file.Name(), + Start: start, + Stop: offset, + }, nil +} + +func skipPartialLine(file *os.File, offset int64) (int64, error) { + for { + skipBuf := make([]byte, bufSize) + n, err := file.ReadAt(skipBuf, offset) + if err != nil && err != io.EOF { + return 0, err + } + if n == 0 { + return 0, io.EOF + } + + for i := 0; i < n; i++ { + if skipBuf[i] != '\r' && skipBuf[i] != '\n' { + offset++ + } else { + for ; i < n; i++ { + if skipBuf[i] == '\r' || skipBuf[i] == '\n' { + offset++ + } else { + return offset, nil + } + } + return offset, nil + } + } + } +} + +// A RangeReader is used to read a range of content from a file. +type RangeReader struct { + file *os.File + start int64 + stop int64 +} + +// NewRangeReader returns a RangeReader, which will read the range of content from file. +func NewRangeReader(file *os.File, start, stop int64) *RangeReader { + return &RangeReader{ + file: file, + start: start, + stop: stop, + } +} + +// Read reads the range of content into p. +func (rr *RangeReader) Read(p []byte) (n int, err error) { + stat, err := rr.file.Stat() + if err != nil { + return 0, err + } + + if rr.stop < rr.start || rr.start >= stat.Size() { + return 0, fmt.Errorf("exceed file size") + } + + if rr.stop-rr.start < int64(len(p)) { + p = p[:rr.stop-rr.start] + } + + n, err = rr.file.ReadAt(p, rr.start) + if err != nil { + return n, err + } + + rr.start += int64(n) + return +} + +func Move(src string, dest string) error { + src = NormalizePath(src) + if !IsFile(src) && DirLength(src) == 0 { + return fmt.Errorf("source does not exist: %v", src) + } + + dest = NormalizePath(dest) + os.RemoveAll(dest) + DebugF("Moving %v --> %v", src, dest) + return os.Rename(src, dest) +} + +func IsWritable(filename string) (isWritable bool, err error) { + isWritable = false + info, err := os.Stat(filename) + if err != nil { + return + } + + err = nil + if !info.IsDir() { + return + } + + // Check if the user write bit is enabled in file permission + if info.Mode().Perm()&0200 == 0 { + return + } + + isWritable = true + return +} + +// RenderText resolve template from signature file +func RenderText(format string, data map[string]string) string { + t := template.Must(template.New("").Parse(format)) + buf := &bytes.Buffer{} + err := t.Execute(buf, data) + if err != nil { + return format + } + return buf.String() +} diff --git a/utils/log.go b/utils/log.go new file mode 100644 index 0000000..f88063c --- /dev/null +++ b/utils/log.go @@ -0,0 +1,128 @@ +package utils + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/fatih/color" + "github.com/j3ssie/osmedeus/libs" + "github.com/kyokomi/emoji" + "github.com/sirupsen/logrus" + prefixed "github.com/x-cray/logrus-prefixed-formatter" +) + +var logger = logrus.New() + +// InitLog init log +func InitLog(options *libs.Options) { + mwr := io.MultiWriter(os.Stdout) + logDir := libs.LDIR + if options.LogFile == "" { + if !FolderExists(logDir) { + os.MkdirAll(logDir, 0777) + } + tmpFile, err := os.CreateTemp(logDir, "osmedeus-*.log") + if err == nil { + options.LogFile = tmpFile.Name() + } else { + tmpFile, _ := os.CreateTemp("/tmp/", "osmedeus-*.log") + options.LogFile = tmpFile.Name() + } + } + + logDir = filepath.Dir(options.LogFile) + if !FolderExists(logDir) { + os.MkdirAll(logDir, 0777) + } + + f, err := os.OpenFile(options.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Fprintf(os.Stderr, "error opening log file: %v\n", options.LogFile) + fmt.Fprintf(os.Stderr, "šŸ’” You might want to switch to %v first via %v command", color.HiMagentaString("root user"), color.HiCyanString("sudo su")) + } else { + mwr = io.MultiWriter(os.Stdout, f) + } + + logger = &logrus.Logger{ + Out: mwr, + Level: logrus.InfoLevel, + Formatter: &prefixed.TextFormatter{ + ForceColors: true, + ForceFormatting: true, + FullTimestamp: true, + TimestampFormat: "2006-01-02T15:04:05", + }, + } + + if options.Debug == true { + logger.SetLevel(logrus.DebugLevel) + } else if options.Verbose == true { + logger.SetLevel(logrus.ErrorLevel) + } else if options.Quite == true { + logger.SetOutput(io.Discard) + } +} + +// PrintLine print seperate line +func PrintLine() { + dash := color.HiWhiteString("-") + fmt.Println(strings.Repeat(dash, 40)) +} + +// GoodF print good message +func GoodF(format string, args ...interface{}) { + good := color.HiGreenString("[+]") + fmt.Printf("%s %s\n", good, fmt.Sprintf(format, args...)) +} + +// BannerF print info message +func BannerF(format string, data string) { + banner := fmt.Sprintf("%v%v%v ", color.WhiteString("["), color.BlueString(format), color.WhiteString("]")) + fmt.Printf("%v%v\n", banner, color.HiGreenString(data)) +} + +// BlockF print info message +func BlockF(name string, data string) { + banner := fmt.Sprintf("%v%v%v ", color.WhiteString("["), color.GreenString(name), color.WhiteString("]")) + fmt.Printf(fmt.Sprintf("%v%v\n", banner, data)) +} + +// BadBlockF print info message +func BadBlockF(name string, data string) { + banner := fmt.Sprintf("%v%v%v ", color.WhiteString("["), color.RedString(name), color.WhiteString("]")) + fmt.Printf(fmt.Sprintf("%v%v\n", banner, data)) +} + +// InforF print info message +func InforF(format string, args ...interface{}) { + logger.Info(fmt.Sprintf(format, args...)) +} + +// ErrorF print good message +func ErrorF(format string, args ...interface{}) { + logger.Error(fmt.Sprintf(format, args...)) +} + +// WarnF print good message +func WarnF(format string, args ...interface{}) { + logger.Warning(fmt.Sprintf(format, args...)) +} + +// TraceF print good message +func TraceF(format string, args ...interface{}) { + logger.Trace(fmt.Sprintf(format, args...)) +} + +// DebugF print debug message +func DebugF(format string, args ...interface{}) { + logger.Debug(fmt.Sprintf(format, args...)) +} + +// Emojif print good message +func Emojif(e string, format string, args ...interface{}) string { + emj := strings.TrimSpace(emoji.Sprint(e)) + return fmt.Sprintf("%1s %s", emj, fmt.Sprintf(format, args...)) +} diff --git a/utils/request.go b/utils/request.go new file mode 100644 index 0000000..d8715b3 --- /dev/null +++ b/utils/request.go @@ -0,0 +1,122 @@ +package utils + +import ( + "bytes" + "crypto/tls" + "fmt" + "github.com/j3ssie/osmedeus/libs" + "io" + "net" + "net/http" + "time" +) + +var DefaultClient *http.Client +var ( + UA string +) + +type Response struct { + StatusCode int + Status string + Body string +} + +func InitHTTPClient() { + UA = fmt.Sprintf("Osmedeus/%s by %s", libs.VERSION, libs.AUTHOR) + var transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + DisableKeepAlives: true, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: time.Second, + }).DialContext, + } + + //if options.Proxy != "" { + // proxyUrl, err := url.Parse(options.Proxy) + // if err == nil { + // transport.Proxy = http.ProxyURL(proxyUrl) + // } + //} + + DefaultClient = &http.Client{ + Transport: transport, + } +} + +// SendGET sending GET request +func SendGET(cred string, url string) (res Response) { + DebugF("Sending GET request to: %v", url) + req, err := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", UA) + if cred != "" { + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", cred)) + } + resp, err := DefaultClient.Do(req) + + if err != nil { + ErrorF("Error sending to %v - %v", url, err) + return res + } + + defer resp.Body.Close() + resbody, err := io.ReadAll(resp.Body) + if err != nil { + return res + } + res.StatusCode = resp.StatusCode + res.Status = resp.Status + res.Body = string(resbody) + return res +} + +// SendPOST sending POST request +func SendPOST(cred string, url string, body string) (res Response) { + DebugF("Sending POST request to: %v", url) + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(body))) + req.Header.Set("User-Agent", UA) + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", cred)) + req.Header.Set("Content-Type", "application/json") + + resp, err := DefaultClient.Do(req) + if err != nil { + ErrorF("Error sending to %v - %v", url, err) + return res + } + defer resp.Body.Close() + resbody, err := io.ReadAll(resp.Body) + + if err != nil { + return res + } + res.StatusCode = resp.StatusCode + res.Status = resp.Status + res.Body = string(resbody) + return res +} + +// SendPUT sending POST request +func SendPUT(cred string, url string, body string) (res Response) { + DebugF("Sending PUT request to: %v", url) + req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(body))) + req.Header.Set("User-Agent", UA) + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", cred)) + req.Header.Set("Content-Type", "application/json") + + resp, err := DefaultClient.Do(req) + if err != nil { + ErrorF("Error sending to %v - %v", url, err) + return res + } + defer resp.Body.Close() + resbody, err := io.ReadAll(resp.Body) + + if err != nil { + return res + } + res.StatusCode = resp.StatusCode + res.Status = resp.Status + res.Body = string(resbody) + return res +}