diff --git a/easyeasm/main.go b/easyeasm/main.go index 46a2afe..cd80f2f 100644 --- a/easyeasm/main.go +++ b/easyeasm/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "fmt" "os" "strings" @@ -8,6 +9,7 @@ import ( "github.com/g0ldencybersec/EasyEASM/pkg/active" "github.com/g0ldencybersec/EasyEASM/pkg/configparser" "github.com/g0ldencybersec/EasyEASM/pkg/passive" + "github.com/g0ldencybersec/EasyEASM/pkg/passive/flags" "github.com/g0ldencybersec/EasyEASM/pkg/utils" ) @@ -19,8 +21,11 @@ func main() { banner := "\x1b[36m****************\n\nEASY EASM\n\n***************\x1b[0m\n" fmt.Println(banner) + //check if flag '-i' is provided when running the tool, if yes return the interactive parameter + flag := flags.ParsingFlags() + // parse the configuration file - cfg := configparser.ParseConfig() + cfg := configparser.ParseConfig(flag) // check for previous run file var prevRun bool @@ -59,6 +64,9 @@ func main() { // run Httpx to check live domains Runner.RunHttpx() + //start the nuclei func + PromptOptionsNuclei(Runner, cfg, flag) + // notify about new domains if prevRun is true if prevRun && strings.Contains(cfg.RunConfig.SlackWebhook, "https") { utils.NotifyNewDomainsSlack(Runner.Subdomains, cfg.RunConfig.SlackWebhook) @@ -101,6 +109,9 @@ func main() { fmt.Println("Checking which domains are live and generating assets csv...") ActiveRunner.RunHttpx() + //nuclei function start + PromptOptionsNuclei(PassiveRunner, cfg, flag) + // notify about new domains if prevRun is true if prevRun && strings.Contains(cfg.RunConfig.SlackWebhook, "https") { utils.NotifyNewDomainsSlack(ActiveRunner.Subdomains, cfg.RunConfig.SlackWebhook) @@ -114,3 +125,77 @@ func main() { panic("Please pick a valid run mode and add it to your config.yml file! You can set runType to either 'fast' or 'complete'") } } + +// func is here and not in nuclei path to avoid having to modify the current structure of the pkg (import cycle with passive) +// it can probably be adjusted to be make the main cleaner +func PromptOptionsNuclei(r passive.PassiveRunner, cfg configparser.Config, flags string) { + + //check if interactive mod is active (flag -i) + if flags == "interactive" { + //vuln scan starting + reader := bufio.NewReader(os.Stdin) + opt, _ := utils.GetInput("Do you want to run the vulnerability scanner? y/n\n", reader) + switch opt { + case "y": + fmt.Println("Running Nuclei") + + var prevRunNuclei bool + if _, err := os.Stat("EasyEASM.json"); err == nil { + fmt.Println("Found data from previous Nuclei scan!") + prevRunNuclei = true + e := os.Rename("EasyEASM.json", "old_EasyEASM.json") + if e != nil { + panic(e) + } + } else { + fmt.Println("No previous Nuclei scan data found") + prevRunNuclei = false + } + r.RunNuclei(flags) + + //notify discord and slack if present + if prevRunNuclei && strings.Contains(cfg.RunConfig.SlackWebhook, "https") { + utils.NotifyVulnSlack(cfg.RunConfig.SlackWebhook) + os.Remove("old_EasyEASM.json") + } else if prevRunNuclei && strings.Contains(cfg.RunConfig.DiscordWebhook, "https") { + utils.NotifyVulnDiscord(cfg.RunConfig.DiscordWebhook) + os.Remove("old_EasyEASM.json") + } + + case "n": + return + + default: + //invalid option chosen at runtime + fmt.Println("Choose a valid option") + PromptOptionsNuclei(r, cfg, flags) + } + } else { + //std run without any console prompt + fmt.Println("Running Nuclei") + + var prevRunNuclei bool + if _, err := os.Stat("EasyEASM.json"); err == nil { + fmt.Println("Found data from previous Nuclei scan!") + prevRunNuclei = true + e := os.Rename("EasyEASM.json", "old_EasyEASM.json") + if e != nil { + panic(e) + } + } else { + fmt.Println("No previous Nuclei scan data found") + prevRunNuclei = false + } + r.RunNuclei(flags) + + //notify discord and slack if presents + if prevRunNuclei && strings.Contains(cfg.RunConfig.SlackWebhook, "https") { + utils.NotifyVulnSlack(cfg.RunConfig.SlackWebhook) + os.Remove("old_EasyEASM.json") + } else if prevRunNuclei && strings.Contains(cfg.RunConfig.DiscordWebhook, "https") { + utils.NotifyVulnDiscord(cfg.RunConfig.DiscordWebhook) + os.Remove("old_EasyEASM.json") + } + return + } +} diff --git a/pkg/active/httpx/httpx.go b/pkg/active/httpx/httpx.go index d99c3b5..aa8c743 100644 --- a/pkg/active/httpx/httpx.go +++ b/pkg/active/httpx/httpx.go @@ -15,7 +15,7 @@ func RunHttpx(domains []string) { if err != nil { panic(err) } - fmt.Printf("Httpx run completed") + fmt.Printf("Httpx run completed\n") processCSV() os.Remove("tempHttpx.txt") os.Remove("temp.csv") @@ -56,7 +56,7 @@ func processCSV() { } // Specify the indices of the columns to keep - columnsToKeep := []int{0,1,7,8,10,13,17,20,26,27,28,32,33,35,37} // Keeping only the first and third columns (0-indexed) + columnsToKeep := []int{0, 1, 7, 8, 10, 13, 17, 20, 26, 27, 28, 32, 33, 35, 37} // Keeping only the first and third columns (0-indexed) // Open the output CSV file outputFile, err := os.Create("EasyEASM.csv") diff --git a/pkg/configparser/configparser.go b/pkg/configparser/configparser.go index 7859acf..ef19c0d 100644 --- a/pkg/configparser/configparser.go +++ b/pkg/configparser/configparser.go @@ -1,9 +1,13 @@ package configparser import ( + "bufio" + "fmt" "log" "os" + "strconv" + "github.com/g0ldencybersec/EasyEASM/pkg/utils" "gopkg.in/yaml.v3" ) @@ -18,7 +22,7 @@ type Config struct { } `yaml:"runConfig"` } -func ParseConfig() Config { +func ParseConfig(flags string) Config { // Read file data data, err := os.ReadFile("config.yml") if err != nil { @@ -33,5 +37,148 @@ func ParseConfig() Config { if err != nil { log.Fatalf("error: %v", err) } + + //runtime config modification if flag -i is provided + if flags == "interactive" { + reader := bufio.NewReader(os.Stdin) + fmt.Println("Do you want to change anything in the config?") + opt, _ := utils.GetInput("Press \"y\" to make changes or any characther to keep running\n", reader) + if opt == "y" { + config = PromptConfigChange(config) + } + } + + return config +} + +func PromptConfigChange(config Config) (cfg Config) { + //runtime changes to the config file if flag -i is provided + fmt.Println("Choose an option or press any other character to run without anymore changes") + reader := bufio.NewReader(os.Stdin) + + opt, _ := utils.GetInput("1.Domains - 2.Slack - 3.Discord - 4.Run Type 5.N of Threads\n", reader) + switch opt { + + //add domains to the list + case "1": + opt, _ = utils.GetInput("Write the domain you would like to add\n", reader) + + //check if the domain is in a valid format (doesnt ensure that the domain exists) + if utils.ValidDomain(opt) { + config.RunConfig.Domains = append(config.RunConfig.Domains, opt) + + yamlData, err := yaml.Marshal(config) + if err != nil { + log.Fatalf("error marshalling YAML: %v", err) + } + + // Write modified data back to YAML file + err = os.WriteFile("config.yml", yamlData, 0644) + if err != nil { + log.Fatalf("error writing YAML file: %v", err) + } + + fmt.Println("Domain added successfully") + PromptConfigChange(config) + } else { + fmt.Printf("Invalid Domain format\n\n") + PromptConfigChange(config) + } + + //add slack webhook at runtime + case "2": + opt, _ = utils.GetInput("Insert the Slack Webhook, end by pressing \"Enter\"\n", reader) + config.RunConfig.SlackWebhook = opt + + //marshal back the data to yml + yamlData, err := yaml.Marshal(config) + if err != nil { + log.Fatalf("error marshalling YAML: %v", err) + } + + // Write modified data back to YAML file + err = os.WriteFile("config.yml", yamlData, 0644) + if err != nil { + log.Fatalf("error writing YAML file: %v", err) + } + + fmt.Printf("Slack Webhook added successfully\n\n") + PromptConfigChange(config) + + //add discord webhook at runtime + case "3": + opt, _ = utils.GetInput("Insert the Discord Webhook, end by pressing \"Enter\"\n", reader) + config.RunConfig.DiscordWebhook = opt + + //marshal back the data to yml + yamlData, err := yaml.Marshal(config) + if err != nil { + log.Fatalf("error marshalling YAML: %v", err) + } + + // Write modified data back to YAML file + err = os.WriteFile("config.yml", yamlData, 0644) + if err != nil { + log.Fatalf("error writing YAML file: %v", err) + } + + fmt.Printf("Slack Webhook added successfully\n\n") + PromptConfigChange(config) + + //change the configuration type + case "4": + opt, _ = utils.GetInput("Insert the run type (fast or complete). End by pressing \"Enter\"\n", reader) + + //check if the type is setted correctly + if opt == "fast" || opt == "complete" { + config.RunConfig.RunType = opt + yamlData, err := yaml.Marshal(config) + if err != nil { + log.Fatalf("error marshalling YAML: %v", err) + } + + // Write modified data back to YAML file + err = os.WriteFile("config.yml", yamlData, 0644) + if err != nil { + log.Fatalf("error writing YAML file: %v", err) + } + + fmt.Printf("Config type setted correctly\n\n") + PromptConfigChange(config) + } else { + //restart the config if the type was invalid + fmt.Printf("Config type invalid, please choose fast or complete\n\n") + PromptConfigChange(config) + } + + //change the number of threads + case "5": + opt, _ = utils.GetInput("Insert the number of threads you want to run. End by pressing \"Enter\"\n", reader) + + //check if the value inserted is a number + num, err := strconv.Atoi(opt) + if err != nil { + log.Fatalf("error converting thread number: %v", err) + } + + //set the number back in the config file + config.RunConfig.ActiveThreads = num + yamlData, err := yaml.Marshal(config) + if err != nil { + log.Fatalf("error marshalling YAML: %v", err) + } + + // Write modified data back to YAML file + err = os.WriteFile("config.yml", yamlData, 0644) + if err != nil { + log.Fatalf("error writing YAML file: %v", err) + } + + fmt.Println("Thread number setted correctly") + PromptConfigChange(config) + + default: + return config + } return config } diff --git a/pkg/passive/flags/flags.go b/pkg/passive/flags/flags.go new file mode 100644 index 0000000..2602502 --- /dev/null +++ b/pkg/passive/flags/flags.go @@ -0,0 +1,26 @@ +package flags + +import ( + "flag" + "fmt" +) + +func ParsingFlags() string { + interactive := flag.Bool("i", false, "interactive mode selected") + //periodic := flag.Bool("p", false, "periodic scan") + flag.Parse() + + //start the interactive mode with runtime config + if *interactive { + fmt.Println("Interactive Mode selected") + return "interactive" + } + + // //enter the periodic mode NOT IMPLEMENTED YET + // if *periodic { + // panic("periodic scan still not implemented") + // } + + //std run read from config file and doesnt prompt anything to console at runtime + return "std" +} diff --git a/pkg/passive/httpx/httpx.go b/pkg/passive/httpx/httpx.go index d99c3b5..aa8c743 100644 --- a/pkg/passive/httpx/httpx.go +++ b/pkg/passive/httpx/httpx.go @@ -15,7 +15,7 @@ func RunHttpx(domains []string) { if err != nil { panic(err) } - fmt.Printf("Httpx run completed") + fmt.Printf("Httpx run completed\n") processCSV() os.Remove("tempHttpx.txt") os.Remove("temp.csv") @@ -56,7 +56,7 @@ func processCSV() { } // Specify the indices of the columns to keep - columnsToKeep := []int{0,1,7,8,10,13,17,20,26,27,28,32,33,35,37} // Keeping only the first and third columns (0-indexed) + columnsToKeep := []int{0, 1, 7, 8, 10, 13, 17, 20, 26, 27, 28, 32, 33, 35, 37} // Keeping only the first and third columns (0-indexed) // Open the output CSV file outputFile, err := os.Create("EasyEASM.csv") diff --git a/pkg/passive/nuclei/nuclei.go b/pkg/passive/nuclei/nuclei.go new file mode 100644 index 0000000..f918b74 --- /dev/null +++ b/pkg/passive/nuclei/nuclei.go @@ -0,0 +1,105 @@ +package nuclei + +import ( + "bufio" + "fmt" + "os" + "os/exec" + + "github.com/g0ldencybersec/EasyEASM/pkg/utils" +) + +func RunNuclei(domains []string, flags string) { + //run the nuclei tool + writeTempFile(domains) + + //run the interactive mode if flag is provided + if flags == "interactive" { + reader := bufio.NewReader(os.Stdin) + std, _ := utils.GetInput("Press y if you want to insert template directory, or any other character to run standard\n", reader) + switch std { + case "y": + reader = bufio.NewReader(os.Stdin) + opt, _ := utils.GetInput("Insert the template directory or press enter to run standard list\n", reader) + + if _, err := os.Stat(opt); os.IsNotExist(err) { + fmt.Println("DIRECTORY DOES NOT EXISTS -> Running standard config...") + //run the nuclei std command + cmd := exec.Command("nuclei", "-l", "tempNuclei.txt", "-silent", "-o", "temp.json", "-j", "-exclude-severity", "info", "-exclude-severity", "unknown") + err := cmd.Run() + if err != nil { + panic(err) + } + } else { + //run the nuclei cmd on the selected list of templetes + fmt.Println("Running found template lists...") + cmd := exec.Command("nuclei", "-l", "tempNuclei.txt", "-silent", "-t", opt, "-o", "temp.json", "-j", "-exclude-severity", "info", "-exclude-severity", "unknown") + err := cmd.Run() + if err != nil { + panic(err) + } + } + + default: + //run the nuclei std command + cmd := exec.Command("nuclei", "-l", "tempNuclei.txt", "-silent", "-o", "temp.json", "-j", "-exclude-severity", "info", "-exclude-severity", "unknown") + err := cmd.Run() + if err != nil { + panic(err) + } + } + } else { + //run the standard scan with nuclei, on the provided targets + cmd := exec.Command("nuclei", "-l", "tempNuclei.txt", "-silent", "-o", "temp.json", "-j", "-exclude-severity", "info", "-exclude-severity", "unknown") + err := cmd.Run() + if err != nil { + panic(err) + } + } + processJson() + fmt.Printf("Nuclei scan completed!\n") + os.Remove("temp.json") + os.Remove("tempNuclei.txt") +} + +func writeTempFile(list []string) { + //create the temporary file and read the domains provided + file, err := os.OpenFile("tempNuclei.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + + if err != nil { + panic(err) + } + + datawriter := bufio.NewWriter(file) + + for _, data := range list { + _, _ = datawriter.WriteString(data + "\n") + } + + datawriter.Flush() + file.Close() +} + +func processJson() { + // Check if jq is installed + utils.CheckJq() + + //process the json output to make lines unique using jq + cmd := exec.Command("jq", "-c", "--unbuffered", "-r", "del(.timestamp) | del(.\"curl-command\") | @json", "temp.json") + + // Create a file to hold the output + outputFile, err := os.Create("EasyEASM.json") + if err != nil { + panic(err) + } + defer outputFile.Close() + + // Set the output of the command to the created file + cmd.Stdout = outputFile + + // Run the command + err = cmd.Run() + if err != nil { + panic(err) + } +} diff --git a/pkg/passive/passive.go b/pkg/passive/passive.go index c429602..99b01d8 100644 --- a/pkg/passive/passive.go +++ b/pkg/passive/passive.go @@ -6,6 +6,7 @@ import ( "github.com/g0ldencybersec/EasyEASM/pkg/passive/amass" "github.com/g0ldencybersec/EasyEASM/pkg/passive/httpx" + "github.com/g0ldencybersec/EasyEASM/pkg/passive/nuclei" "github.com/g0ldencybersec/EasyEASM/pkg/passive/subfinder" ) @@ -58,3 +59,7 @@ func (r *PassiveRunner) RunPassiveEnum() []string { func (r *PassiveRunner) RunHttpx() { httpx.RunHttpx(r.Subdomains) } + +func (r *PassiveRunner) RunNuclei(flags string) { + nuclei.RunNuclei(r.Subdomains, flags) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a6f7e90..249f8cc 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "bufio" "bytes" "encoding/csv" "encoding/json" @@ -9,6 +10,8 @@ import ( "net/http" "os" "os/exec" + "regexp" + "strings" ) func RemoveDuplicates(slice []string) []string { @@ -192,6 +195,7 @@ func InstallTools() { "httpx": "github.com/projectdiscovery/httpx/cmd/httpx@latest", "oam_subs": "github.com/owasp-amass/oam-tools/cmd/oam_subs@master", "subfinder": "github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest", + "nuclei": "github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest", } { if !checkTool(name) { installGoTool(name, path) @@ -223,3 +227,136 @@ func installGoTool(name string, path string) { log.Printf("Successfully installed the package: %s", packagePath) } + +func GetInput(prompt string, r *bufio.Reader) (string, error) { + fmt.Print(prompt) + input, err := r.ReadString('\n') + if err != nil { + panic(err) + } + + return strings.TrimSpace(input), err +} + +func CheckJq() { + //check if jq is installed, if not, abort the scan + cmd := exec.Command("jq", "--version") + err := cmd.Run() + if err != nil { + print("Jq is not installed, nuclei scan can't be run.\n\n") + panic(err) + } else { + return + } +} + +func NotifyVulnDiscord(discordWebhook string) { + // Used to parse the nuclei file and notify about vuln + // notification contains: host, name of the vulnerability, severity + + // Open the JSON file + inputFile, err := os.Open("EasyEASM.json") + if err != nil { + fmt.Println("Error opening JSON file") + panic(err) + } + defer inputFile.Close() + + //structured json of the nuclei output, used only here so declared inside + type Info struct { + Name string `json:"name"` + Severity string `json:"severity"` + } + + type Data struct { + Host string `json:"host"` + Inform Info `json:"info"` + } + + var jsonPayload []Data + var vulnerability Data + decoder := json.NewDecoder(inputFile) + + //decode the json output from nuclei + for decoder.More() { + err := decoder.Decode(&vulnerability) + if err != nil { + panic(err) + } + + //append the parametres for each line of the JSON + jsonPayload = append(jsonPayload, vulnerability) + } + + //bulking toghether the different vuln to have a single notification + var message string + message = "List of discovered vulnerabilities:\n" + for _, v := range jsonPayload { + newMessage := fmt.Sprintf("Host: %v, Name: %v, Severity: %v\n", v.Host, v.Inform.Name, v.Inform.Severity) + message += newMessage + } + + //sending the message to the provided webhook + sendToDiscord(discordWebhook, message) +} + +func NotifyVulnSlack(slackWebhook string) { + // Used to parse the nuclei file and notify about vuln + // notification are based on: host, name of the vulnerability and severity + + // Open the JSON file + inputFile, err := os.Open("EasyEASM.json") + if err != nil { + fmt.Println("Error opening JSON file") + panic(err) + } + defer inputFile.Close() + + //structured json of the nuclei output, used only here so declared inside + type Info struct { + Name string `json:"name"` + Severity string `json:"severity"` + } + + type Data struct { + Host string `json:"host"` + Inform Info `json:"info"` + } + + var jsonPayload []Data + var vulnerability Data + decoder := json.NewDecoder(inputFile) + + //decode the json output from nuclei + for decoder.More() { + err := decoder.Decode(&vulnerability) + if err != nil { + panic(err) + } + + //append the parametres for each line of the JSON + jsonPayload = append(jsonPayload, vulnerability) + } + + //bulking toghether the different vuln to have a single notification + //notify the host, name and severity of the vulnerability + var message string + message = "List of discovered vulnerabilities:\n" + for _, v := range jsonPayload { + newMessage := fmt.Sprintf("Host: %v, Name: %v, Severity: %v\n", v.Host, v.Inform.Name, v.Inform.Severity) + message += newMessage + } + + //sending the message to the provided webhook + sendToDiscord(slackWebhook, message) +} + +func ValidDomain(domain string) bool { + //check if the string provided is a valid domain - pattern can be made modified to be more strict + pattern := `^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*(?:[A-Za-z0-9\-]+\.)+[A-Za-z]{2,}$` + regex := regexp.MustCompile(pattern) + + //retrun a boolean to make the check quick in the configparser + return regex.MatchString(domain) + +}