diff --git a/.gitignore b/.gitignore index a1987c9..47f9477 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ +.idea/ *.eml \ No newline at end of file diff --git a/cmd/b64d.go b/cmd/b64d.go new file mode 100644 index 0000000..df013da --- /dev/null +++ b/cmd/b64d.go @@ -0,0 +1,35 @@ +/* +Copyright © 2025 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package cmd + +import ( + "encoding/base64" + "fmt" + + "github.com/spf13/cobra" + "github.com/fatih/color" +) + +var b64dCmd = &cobra.Command{ + Use: "b64d ", + Short: "Decode a Base64 string", + Long: "Decode a Base64-encoded string and display the original content.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + base64Str := args[0] + decoded, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + color.Red("Error decoding Base64 string: %v", err) + } + fmt.Println(string(decoded)) + }, +} + +// Register the b64d subcommand under misc +func init() { + miscCmd.AddCommand(b64dCmd) +} diff --git a/cmd/b64e.go b/cmd/b64e.go new file mode 100644 index 0000000..1518878 --- /dev/null +++ b/cmd/b64e.go @@ -0,0 +1,30 @@ +/* +Copyright © 2025 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package cmd + +import ( + "encoding/base64" + "fmt" + "github.com/spf13/cobra" +) + +var b64eCmd = &cobra.Command{ + Use: "b64e ", + Short: "Encode a string to Base64", + Long: "Encode a string to its Base64 representation.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + input := args[0] + encoded := base64.StdEncoding.EncodeToString([]byte(input)) + fmt.Println(encoded) + }, +} + +// Register the b64e subcommand under misc +func init() { + miscCmd.AddCommand(b64eCmd) +} diff --git a/cmd/defang.go b/cmd/defang.go index f4cb458..c5d5bdd 100644 --- a/cmd/defang.go +++ b/cmd/defang.go @@ -7,61 +7,30 @@ See the LICENSE file for details. package cmd import ( - "bufio" "fmt" "github.com/spf13/cobra" - "log" - "os" - "runtime" "soc-cli/internal/logic" - "strings" + "soc-cli/internal/util" ) var defangCmd = &cobra.Command{ Use: "defang [input]", Short: "Defang a URL or email address to make it safe for sharing", Args: cobra.MaximumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - var input string - - if len(args) > 0 { - input = args[0] - } else { - if !isInputFromPipe() { - fmt.Print("Enter URL or email to defang: ") - } - - reader := bufio.NewReader(os.Stdin) - in, err := reader.ReadString('\n') - if err != nil { - log.Fatalf("Error reading input: %v", err) - } - input = strings.TrimSpace(in) - } + Run: executeDefang, +} - defanged := logic.Defang(input) - fmt.Println(defanged) - }, +func executeDefang(cmd *cobra.Command, args []string) { + var input string + if len(args) > 0 { + input = args[0] + } else { + input = util.GetPromptedInput("Enter URL or email to defang: ") + } + defanged := logic.Defang(input) + fmt.Println(defanged) } func init() { rootCmd.AddCommand(defangCmd) } - -// isInputFromPipe checks if the standard input is coming from a pipe -func isInputFromPipe() bool { - // Check if stdin is a terminal - return !isTerminal(os.Stdin.Fd()) -} - -// isTerminal checks if the given file descriptor is a terminal -func isTerminal(fd uintptr) bool { - return runtime.GOOS != "windows" && isatty(fd) -} - -// isatty checks if the file descriptor is a terminal (Unix-like systems) -func isatty(fd uintptr) bool { - // Use the syscall package to check if the file descriptor is a terminal - // This is a simplified version; you may need to import "golang.org/x/sys/unix" for a complete implementation - return false // Placeholder; implement actual check if needed -} diff --git a/cmd/email.go b/cmd/email.go index cf976a3..04847c9 100644 --- a/cmd/email.go +++ b/cmd/email.go @@ -13,12 +13,22 @@ import ( "io" "mime" "mime/multipart" + "mime/quotedprintable" "net/mail" "os" "soc-cli/internal/util" "strings" ) +const ( + emlExtension = ".eml" + contentTypeHeader = "Content-Type" + transferEncodingHeader = "Content-Transfer-Encoding" + receivedSPFHeader = "Received-SPF" + dkimSignatureHeader = "DKIM-Signature" + authenticationResultsHeader = "Authentication-Results" +) + var analyzeEmailCmd = &cobra.Command{ Use: "email [file]", Short: "Analyze an email in .eml format for attachments and links", @@ -35,6 +45,10 @@ func init() { // analyzeEmail processes the .eml file and extracts attachments and links func analyzeEmail(filePath string) { + if !isValidEmlFile(filePath) { + return + } + file, err := os.Open(filePath) if err != nil { fmt.Println("Error opening file:", err) @@ -48,57 +62,66 @@ func analyzeEmail(filePath string) { fmt.Println("Error parsing .eml file:", err) return } - color.Blue("Main information:") - fmt.Println("From:", msg.Header.Get("From")) - fmt.Println("To:", msg.Header.Get("To")) - fmt.Println("Subject:", msg.Header.Get("Subject")) - fmt.Println("Date:", msg.Header.Get("Date")) - fmt.Println("Return-Path:", msg.Header.Get("Return-Path")) + + printEmailHeaders(msg) // Check for SPF information - spfHeader := msg.Header.Get("Received-SPF") - if spfHeader != "" { - fmt.Println(color.BlueString("\nSPF Information:\n"), spfHeader) - } else { - fmt.Println(color.BlueString("\nSPF Information:\n") + "No Received-SPF header found.") - } + printHeaderInfo(msg.Header.Get(receivedSPFHeader), "SPF Information") // Extract DKIM Information - dkimHeader := msg.Header.Get("DKIM-Signature") - if dkimHeader != "" { - color.Blue("\nDKIM Information:") - fmt.Println(dkimHeader) - } else { - fmt.Println(color.BlueString("\nDKIM Information:\n") + "No DKIM-Signature header found.") - } + printHeaderInfo(msg.Header.Get(dkimSignatureHeader), "DKIM Information") // Extract DMARC Information from Authentication-Results header - authResults := msg.Header.Get("Authentication-Results") + authResults := msg.Header.Get(authenticationResultsHeader) if authResults != "" { extractDMARCDKIM(authResults) } else { fmt.Println("\nDMARC Information: No Authentication-Results header found.") } - mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type")) - if err != nil { - fmt.Println("Error parsing content type:", err) - return + processEmailBody(msg) +} + +// isValidEmlFile checks if the provided file path has a valid .eml extension +func isValidEmlFile(filePath string) bool { + if !strings.HasSuffix(strings.ToLower(filePath), emlExtension) { + color.Red("The provided file is not an .eml file.") + return false } + return true +} + +// processEmailBody processes the email body based on its content type +func processEmailBody(msg *mail.Message) { + mediaType, params, err := mime.ParseMediaType(msg.Header.Get(contentTypeHeader)) + handleError(err, "Error parsing content type:") if strings.HasPrefix(mediaType, "multipart/") { - // Handle multipart emails (usually contains attachments and text) mr := multipart.NewReader(msg.Body, params["boundary"]) processMultipart(mr) } else { - // Handle single-part emails (just extract links) - body, _ := io.ReadAll(msg.Body) + handleSinglePartEmail(msg) + } +} + +// handleSinglePartEmail handles single-part emails and extracts links +func handleSinglePartEmail(msg *mail.Message) { + body, _ := io.ReadAll(msg.Body) + encoding := msg.Header.Get(transferEncodingHeader) + + if strings.ToLower(encoding) == "quoted-printable" { + reader := quotedprintable.NewReader(strings.NewReader(string(body))) + decodedBody, err := io.ReadAll(reader) + handleError(err, "Error decoding quoted-printable content:") + extractLinks(string(decodedBody)) + } else { extractLinks(string(body)) } } // processMultipart processes multipart emails for attachments and links func processMultipart(mr *multipart.Reader) { + attachmentsFound := false for { part, err := mr.NextPart() if err == io.EOF { @@ -109,22 +132,25 @@ func processMultipart(mr *multipart.Reader) { return } - contentType := part.Header.Get("Content-Type") + contentType := part.Header.Get(contentTypeHeader) disposition := part.Header.Get("Content-Disposition") // If it's an attachment, list it if strings.Contains(disposition, "attachment") { - fileName := part.FileName() - if fileName == "" { - fileName = "unnamed attachment" + if !attachmentsFound { + color.Blue("\nAttachments:") + attachmentsFound = true } - fmt.Printf("Attachment: %s (MIME type: %s)\n", fileName, contentType) + handleAttachment(part, contentType) } else { // Otherwise, it's likely part of the email body (text or HTML) body, _ := io.ReadAll(part) extractLinks(string(body)) } } + if !attachmentsFound { + fmt.Println("\nNo attachments found.") + } } // extractDMARCDKIM extracts DMARC and DKIM results from the Authentication-Results header @@ -164,3 +190,44 @@ func extractLinks(body string) { color.Blue("\nNo links found in the email.") } } + +func handleAttachment(part *multipart.Part, contentType string) { + fileName := part.FileName() + if fileName == "" { + fileName = "unnamed attachment" + } + + fmt.Printf("Attachment: %s (MIME type: %s)\n", fileName, contentType) +} + +func handleError(err error, message string) { + if err != nil { + fmt.Println(message, err) + } +} + +func printHeader(headerName, headerValue string) { + if headerValue != "" { + fmt.Printf("%s: %s\n", color.CyanString(headerName), headerValue) + } +} + +func printEmailHeaders(msg *mail.Message) { + color.Blue("Main information:") + printHeader("From", msg.Header.Get("From")) + printHeader("To", msg.Header.Get("To")) + printHeader("Cc", msg.Header.Get("Cc")) + printHeader("Bcc", msg.Header.Get("Bcc")) + printHeader("Subject", msg.Header.Get("Subject")) + printHeader("Date", msg.Header.Get("Date")) + printHeader("Reply-To", msg.Header.Get("Reply-To")) + printHeader("Return-Path", msg.Header.Get("Return-Path")) +} + +func printHeaderInfo(headerValue, headerName string) { + if headerValue != "" { + fmt.Println(color.BlueString("\n%s:\n", headerName), headerValue) + } else { + fmt.Println(color.BlueString("\n%s:\n", headerName) + "No information found.") + } +} diff --git a/cmd/file_check.go b/cmd/file_check.go new file mode 100644 index 0000000..06af4d4 --- /dev/null +++ b/cmd/file_check.go @@ -0,0 +1,248 @@ +/* +Copyright © 2024 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package cmd + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "io" + "log" + "mime/multipart" + "net/http" + "os" + "soc-cli/internal/apis" + "soc-cli/internal/util" + "strings" + "time" +) + +const virusTotalBaseURL = "https://www.virustotal.com/api/v3" +const virusTotalFileGuiURL = "https://www.virustotal.com/gui/file/%s" +const virusTotalFileReportEndpoint = "/files/" +const virusTotalFileUploadEndpoint = "/files" + +// fileCheckCmd represents the file-check command +var fileCheckCmd = &cobra.Command{ + Use: "file-check [file]", + Short: "Check file for suspicious content and upload to VirusTotal if not present", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + filePath := args[0] + checkFileOnVirusTotal(filePath) + }, +} + +func init() { + rootCmd.AddCommand(fileCheckCmd) +} + +func checkFileOnVirusTotal(filePath string) { + apiKey := viper.GetString("api_keys.virustotal.api_key") + if apiKey == "" { + color.Red("VirusTotal API key missing! Please set it in the config file.") + os.Exit(1) + } + + hash, err := calculateSHA256(filePath) + if err != nil { + color.Red("Error calculating file hash: %v", err) + os.Exit(1) + } + + fmt.Printf("File SHA256: %s\n", hash) + + // Check if file already exists in VirusTotal + if exist, report := fileExistsInVirusTotal(apiKey, hash); exist { + color.Green("File already analyzed on VirusTotal.") + displayVirusTotalReport(report) + + } else { + // Ask for confirmation before uploading + if confirmUpload() { + fmt.Println("Uploading file to VirusTotal for analysis...") + uploadFileToVirusTotal(apiKey, filePath) + } else { + fmt.Println("Upload canceled.") + } + } +} + +func confirmUpload() bool { + reader := bufio.NewReader(os.Stdin) + fmt.Print("File not found on VirusTotal. Do you want to upload it for analysis? (y/N): ") + response, err := reader.ReadString('\n') + if err != nil { + log.Fatalf("Error reading input: %v", err) + } + response = strings.TrimSpace(strings.ToLower(response)) + return response == "y" || response == "yes" +} + +func calculateSHA256(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("could not open file: %w", err) + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "", fmt.Errorf("could not hash file: %w", err) + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} + +func fileExistsInVirusTotal(apiKey, hash string) (exist bool, report apis.VTResponse) { + + headers := map[string]string{ + "X-Apikey": apiKey, + "Accept": "application/json", + } + + statusCode, err := util.HTTPGetJSON(virusTotalBaseURL+virusTotalFileReportEndpoint+hash, headers, &report) + if err != nil { + log.Fatalf("Error fetching VirusTotal: %v", err) + } + + if statusCode == http.StatusNotFound { + return false, apis.VTResponse{} // File not found on VirusTotal + } + + return true, report +} + +func uploadFileToVirusTotal(apiKey, filePath string) { + + // Create a buffer to hold the multipart form data + var b bytes.Buffer + writer := multipart.NewWriter(&b) + + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("Could not open file for upload: %v", err) + } + defer file.Close() + + part, err := writer.CreateFormFile("file", filePath) + if err != nil { + log.Fatalf("CreateFormFile: %v", err) + } + + // Copy the file content to the form file field + _, err = io.Copy(part, file) + if err != nil { + log.Fatalf("io.Copy: %v", err) + } + + // Close the writer to finalize the multipart form + err = writer.Close() + if err != nil { + log.Fatalf("writer.Close: %v", err) + } + + req, err := http.NewRequest("POST", virusTotalBaseURL+virusTotalFileUploadEndpoint, &b) + if err != nil { + log.Fatalf("Error creating upload request: %v", err) + } + req.Header.Set("x-apikey", apiKey) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error uploading file: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Fatalf("Unexpected response from VirusTotal upload: %v", req) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading upload response body: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + log.Fatalf("Error parsing upload response JSON: %v", err) + } + + // Extract the File ID for querying the report + fileID, ok := result["data"].(map[string]interface{})["id"].(string) + if !ok { + log.Fatalf("File ID not found in response") + } + fmt.Println("File uploaded successfully. Fetching scan report...") + fetchVirusTotalReport(apiKey, fileID) +} + +func fetchVirusTotalReport(apiKey, fileID string) { + client := &http.Client{} + url := fmt.Sprintf("%s%s%s", virusTotalBaseURL, virusTotalFileReportEndpoint, fileID) + + for attempts := 0; attempts < 10; attempts++ { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + req.Header.Set("x-apikey", apiKey) + req.Header.Set("accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + fmt.Println("Report not ready. Retrying in 10 seconds...") + time.Sleep(10 * time.Second) + continue + } + + if resp.StatusCode != http.StatusOK { + log.Fatalf("Unexpected response from VirusTotal report API: %v", resp) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + var result apis.VTResponse + if err := json.Unmarshal(body, &result); err != nil { + log.Fatalf("Error parsing JSON response: %v", err) + } + displayVirusTotalReport(result) + fmt.Println("VirusTotal Scan Report:") + return + } + + color.Red("Report could not be retrieved within the timeout period.") +} + +func displayVirusTotalReport(report apis.VTResponse) { + scanUrl := fmt.Sprintf(virusTotalFileGuiURL, report.Data.Attributes.Sha256) + + color.Blue("VirusTotal Scan Report:") + + fmt.Printf("\nType: %s\n", report.Data.Type) + fmt.Printf("Meaningful Name: %s\n", report.Data.Attributes.MeaningfulName) + fmt.Printf("Magic: %v\n", report.Data.Attributes.Magic) + fmt.Printf("Reputation: %d\n", report.Data.Attributes.Reputation) + fmt.Printf("Link: %s\n", scanUrl) + fmt.Printf("Analysis result: malicious %v, undetected %v, harmless %v\n", report.Data.Attributes.LastAnalysisStats.Malicious, report.Data.Attributes.LastAnalysisStats.Suspicious, report.Data.Attributes.LastAnalysisStats.Harmless) +} diff --git a/cmd/hash.go b/cmd/hash.go index bb2dada..06e2e92 100644 --- a/cmd/hash.go +++ b/cmd/hash.go @@ -40,12 +40,9 @@ func showHashes(filePath string, asJson bool) { defer file.Close() - md5Digest := logic.ComputeMd5(file) - // Reset the file pointer to the beginning - file.Seek(0, 0) - sha1Digest := logic.ComputeSha1(file) - file.Seek(0, 0) - sha256Digest := logic.ComputeSha256(file) + md5Digest := logic.ComputeFileMd5(file) + sha1Digest := logic.ComputeFileSha1(file) + sha256Digest := logic.ComputeFileSha256(file) if asJson { diff --git a/cmd/ip.go b/cmd/ip.go index 0c73bab..c5a9f6b 100644 --- a/cmd/ip.go +++ b/cmd/ip.go @@ -8,79 +8,128 @@ package cmd import ( "fmt" - "log" + "github.com/fatih/color" + "github.com/rodaine/table" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "net" "os" "soc-cli/internal/apis" "soc-cli/internal/util" "strings" "time" +) - "github.com/fatih/color" - "github.com/rodaine/table" - "github.com/spf13/cobra" - "github.com/spf13/viper" +const ( + defaultReportEntries = 3 + defaultReportMaxLen = 100 + greyNoiseAPIKeyMsg = "GreyNoise API key is missing! Please set the greynoise api_key in config.yaml file." + ipInfoAPIKeyMsg = "IPInfo API key is missing! Please set the ipinfo api_key in config.yaml file." + abuseIPDBAPIKeyMsg = "AbuseIPDB API key is missing! Please set the abuseipdb api_key in config.yaml file." ) -var reportLimit = 3 var reportMaxLen int +var reportEntries int -func analyzeIP(ip string) { +func checkInput(input string) error { + // Handle defanged IP + input = strings.ReplaceAll(input, "[.]", ".") + + ip := net.ParseIP(input) + if ip == nil { + color.Red("Invalid IP address.") + os.Exit(1) + } // Validate provided IP address - if util.IPRegex.MatchString(ip) { - if util.RFC1918Regex.MatchString(ip) { - fmt.Printf("The IP provided %s is a RFC1918 bogus IP address.\n", ip) - os.Exit(0) - } else if ip == "127.0.0.1" { - fmt.Printf("The IP provided %s is a loopback IP address.\n", ip) - os.Exit(0) - } - } else { - log.Fatalf("The IP provided %s is not a valid IPv4 address.\n", ip) + switch { + case ip.IsPrivate(): + return fmt.Errorf("the IP %s is a RFC1918 bogus IP address", ip) + case ip.IsLoopback(): + return fmt.Errorf("the IP %s is a loopback IP address", ip) + case ip.IsMulticast(): + return fmt.Errorf("the IP %s is a multicast IP address", ip) + case ip.To16() != nil && ip.To4() == nil: + return fmt.Errorf("IPv6 addresses are not supported yet") } - greyNoiseApiKey := viper.GetString("api_keys.greynoise.api_key") - if greyNoiseApiKey == "" { - log.Println("GreyNoise API key is missing! Please set the greynoise api_key in config.yaml file") + analyzeIP(ip) + return nil +} + +func checkAPIKeys() []string { + var missingKeys []string + if viper.GetString("api_keys.greynoise.api_key") == "" { + missingKeys = append(missingKeys, greyNoiseAPIKeyMsg) + } + if viper.GetString("api_keys.ipinfo.api_key") == "" { + missingKeys = append(missingKeys, ipInfoAPIKeyMsg) } + if viper.GetString("api_keys.abuseipdb.api_key") == "" { + missingKeys = append(missingKeys, abuseIPDBAPIKeyMsg) + } + return missingKeys +} - ipInfoApiKey := viper.GetString("api_keys.ipinfo.api_key") - if ipInfoApiKey == "" { - log.Println("API key is missing! Please set the ipinfo api_key in config.yaml file") +func analyzeIP(ip net.IP) { + missingKeys := checkAPIKeys() + if len(missingKeys) > 0 { + for _, msg := range missingKeys { + color.Yellow(msg) + } } - abuseIPDBApiKey := viper.GetString("api_keys.abuseipdb.api_key") - if abuseIPDBApiKey == "" { - log.Println("API key is missing! Please set the abuseipdb api_key in config.yaml file") + // Fetch IP information + if viper.GetString("api_keys.ipinfo.api_key") != "" { + ipInfoData := apis.GetIPInfo(ip, viper.GetString("api_keys.ipinfo.api_key")) + printIPInfo(ipInfoData) } - // Fetch IpInfo api - ipInfoData := apis.GetIPInfo(ip, ipInfoApiKey) + if viper.GetString("api_keys.greynoise.api_key") != "" { + greyNoiseData := apis.GetGreyNoiseData(ip, viper.GetString("api_keys.greynoise.api_key")) + printGreyNoiseData(greyNoiseData) + } - // Fetch GreyNoise threat intelligence - greyNoiseData := apis.GetGreyNoiseData(ip, greyNoiseApiKey) + if viper.GetString("api_keys.abuseipdb.api_key") != "" { + abuseIPDBData := apis.GetAbuseIPDBInfo(ip, viper.GetString("api_keys.abuseipdb.api_key")) + printAbuseIPDBData(abuseIPDBData) + } - abuseIPDBData := apis.GetAbuseIPDBInfo(ip, abuseIPDBApiKey) +} - // Print the IP information +func printIPInfo(ipInfoData *apis.IPInfo) { color.Blue("IP information from IPInfo") - fmt.Printf("IP: %s\nHostname: %s\nOrg: %s\nCountry: %s\n", - ipInfoData.IP, ipInfoData.Hostname, ipInfoData.Org, ipInfoData.Country) + util.PrintEntry("IP", ipInfoData.IP) + util.PrintEntry("Hostname", ipInfoData.Hostname) + util.PrintEntry("Org", ipInfoData.Org) + util.PrintEntry("Country", ipInfoData.Country) + +} +func printGreyNoiseData(greyNoiseData *apis.GreyNoiseInfo) { if greyNoiseData != nil { color.Blue("\nGreyNoise Threat Intelligence") - classification := greyNoiseData.Classification - if classification == "malicious" { - classification = color.RedString(strings.ToUpper(classification)) - } else if classification == "benign" { - classification = color.GreenString(strings.ToUpper(classification)) + classification := strings.ToUpper(greyNoiseData.Classification) + switch classification { + case "BENIGN": + classification = color.GreenString(classification) + case "MALICIOUS": + classification = color.RedString(classification) + case "SUSPICIOUS": + classification = color.YellowString(classification) } - fmt.Printf("Noise: %v\nRiot: %v\nClassification: %s\nName: %s\nLink: %s\n", - greyNoiseData.Noise, greyNoiseData.Riot, classification, greyNoiseData.Name, greyNoiseData.Link) + util.PrintEntry("Noise", util.PrintYesNo(greyNoiseData.Noise)) + util.PrintEntry("Riot", util.PrintYesNo(greyNoiseData.Riot)) + util.PrintEntry("Classification", classification) + util.PrintEntry("Message", greyNoiseData.Message) + util.PrintEntry("Last seen", greyNoiseData.LastSeen) + util.PrintEntry("Link", greyNoiseData.Link) } +} +func printAbuseIPDBData(abuseIPDBData *apis.AbuseIPDBResponse) { if abuseIPDBData != nil { color.Blue("\nAbuseIPDB report") if abuseIPDBData.Data.TotalReports == 0 { @@ -91,9 +140,9 @@ func analyzeIP(ip string) { lastReportDate, _ := time.Parse(time.RFC3339, abuseIPDBData.Data.LastReportedAt) // Print AbuseIPDB info - fmt.Printf("Abuse Confidence Score: %d\n", abuseIPDBData.Data.AbuseConfidenceScore) - fmt.Printf("Total Reports: %d\n", abuseIPDBData.Data.TotalReports) - fmt.Printf("Last Reported At: %s\n", lastReportDate.Format("Monday, January 2, 2006")) + util.PrintEntry("Abuse Confidence Score", abuseIPDBData.Data.AbuseConfidenceScore) + util.PrintEntry("Total Reports", abuseIPDBData.Data.TotalReports) + util.PrintEntry("Last Reported At", lastReportDate.Format("Monday, January 2, 2006")) // Print the individual reports if available if len(abuseIPDBData.Data.Reports) > 0 { @@ -104,21 +153,18 @@ func analyzeIP(ip string) { tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) for index, report := range abuseIPDBData.Data.Reports { - if index > reportLimit { + if index >= reportEntries { break } humanTime, _ := util.HumanReadableDate(report.ReportedAt) tbl.AddRow(humanTime, report.ReporterCountry, util.ShortStr(report.Comment, reportMaxLen)) } + fmt.Println() tbl.Print() } - } else { - color.Red("An error has occured.") - os.Exit(1) } - } var ipCmd = &cobra.Command{ @@ -126,12 +172,16 @@ var ipCmd = &cobra.Command{ Short: "Analyze an IP address for geolocation, ASN, and threat status", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - ip := args[0] - analyzeIP(ip) + input := args[0] + if err := checkInput(input); err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } }, } func init() { - ipCmd.Flags().IntVarP(&reportMaxLen, "length", "l", 50, "AbuseIPDB report max length") + ipCmd.Flags().IntVarP(&reportMaxLen, "length", "l", defaultReportMaxLen, "AbuseIPDB report max length") + ipCmd.Flags().IntVarP(&reportEntries, "reports", "r", defaultReportEntries, "AbuseIPDB reports to show") rootCmd.AddCommand(ipCmd) } diff --git a/cmd/misc.go b/cmd/misc.go new file mode 100644 index 0000000..f2fa249 --- /dev/null +++ b/cmd/misc.go @@ -0,0 +1,21 @@ +/* +Copyright © 2025 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var miscCmd = &cobra.Command{ + Use: "misc", + Short: "Miscellaneous utilities", + Long: "A collection of miscellaneous utilities for various tasks.", +} + +func init() { + rootCmd.AddCommand(miscCmd) +} diff --git a/cmd/myip.go b/cmd/myip.go new file mode 100644 index 0000000..48579ae --- /dev/null +++ b/cmd/myip.go @@ -0,0 +1,47 @@ +/* +Copyright © 2025 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "net" + "soc-cli/internal/util" + "github.com/fatih/color" + "strings" +) + +func getMyIP() net.IP { + + headers := map[string]string{ + "User-Agent": "curl/8.9.1", + } + + url := "https://ip.me" + + body, err := util.GetRaw(url, headers) + if err != nil { + color.Red("Error fetching API: %v", err) + } + + ip := strings.TrimSpace(string(body)) + return net.ParseIP(ip) +} + +var myipCmd = &cobra.Command{ + Use: "myip", + Short: "Get your ip address", + Long: "Get your ip address using ip.me API", + Run: func(cmd *cobra.Command, args []string) { + ip := getMyIP() + fmt.Println(ip) + }, +} + +func init() { + miscCmd.AddCommand(myipCmd) +} diff --git a/cmd/urlscan.go b/cmd/urlscan.go index a590f02..19cff28 100644 --- a/cmd/urlscan.go +++ b/cmd/urlscan.go @@ -14,11 +14,13 @@ import ( "github.com/spf13/viper" "log" "net/http" + "soc-cli/internal/logic" "soc-cli/internal/util" "time" ) var defautVisibility = "private" // public, unlisted or private +var defangFlag bool const ( urlscanScanApi = "https://urlscan.io/api/v1/scan/" @@ -31,6 +33,7 @@ type urlScanResult struct { Domain string `json:"domain"` Country string `json:"country"` IP string `json:"ip"` + Title string `json:"title"` } `json:"page"` Task struct { ReportURL string `json:"reportURL"` @@ -51,7 +54,7 @@ func submitURLScan(url string) (string, error) { var result map[string]interface{} - err := util.MakePOSTRequest(urlscanScanApi, map[string]string{"API-Key": apiKey}, requestBody, &result) + err := util.HTTPPostJSON(urlscanScanApi, map[string]string{"API-Key": apiKey}, requestBody, &result) if err != nil { return "", fmt.Errorf("failed to submit URL scan request: %v", err) @@ -100,15 +103,28 @@ func fetchURLScanResult(scanID string) (*urlScanResult, error) { } func displayResults(scanResult urlScanResult) { - fmt.Printf("Scan Results for URL: %s\n", scanResult.Page.URL) - fmt.Printf("Domain: %s\n", scanResult.Page.Domain) - fmt.Printf("Country: %s\n", scanResult.Page.Country) - fmt.Printf("IP: %s\n", scanResult.Page.IP) - fmt.Printf("Link: %s\n", scanResult.Task.ReportURL) - if scanResult.Verdict.Malicious { - fmt.Println("Verdict: " + color.RedString("MALICIOUS")) + isMalicious := scanResult.Verdict.Malicious + domain := scanResult.Page.Domain + scannedUrl := scanResult.Page.URL + + if isMalicious || defangFlag { + + scannedUrl = logic.DefangURL(scannedUrl) + domain = logic.DefangURL(domain) + } + + util.PrintEntry("Scan Results for URL", scannedUrl) + util.PrintEntry("Domain", domain) + + util.PrintEntry("Title", scanResult.Page.Title) + + util.PrintEntry("IP", scanResult.Page.IP) + util.PrintEntry("Country", scanResult.Page.Country) + util.PrintEntry("Link", scanResult.Task.ReportURL) + if isMalicious { + util.PrintEntry("Verdict", color.RedString("MALICIOUS")) } else { - fmt.Println("Verdict: " + color.GreenString("SAFE")) + util.PrintEntry("Verdict", color.GreenString("SAFE")) } } @@ -126,7 +142,8 @@ var urlScanCmd = &cobra.Command{ log.Fatalf("Error submitting URL for scan: %v", err) } - color.Blue("URL submitted successfully. Awaiting results...") + color.Green("URL submitted successfully.") + color.Blue("Awaiting results...") // Fetch the scan results scanResult, err := fetchURLScanResult(scanID) @@ -139,5 +156,6 @@ var urlScanCmd = &cobra.Command{ } func init() { + urlScanCmd.Flags().BoolVar(&defangFlag, "defang", false, "Defang the URL") rootCmd.AddCommand(urlScanCmd) } diff --git a/cmd/version.go b/cmd/version.go index b86f555..9b0b727 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -13,7 +13,7 @@ import ( "log" ) -var Version = "dev" +var Version = "v01.0" type verOutput struct { Version string `json:"version"` diff --git a/config/config.go b/config/config.go deleted file mode 100644 index b97193d..0000000 --- a/config/config.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright © 2024 Alessandro Riva - -Licensed under the MIT License. -See the LICENSE file for details. -*/ -package config - -import ( - "fmt" - "github.com/spf13/viper" - "os" - "path/filepath" -) - -const configTemplate = `api_keys: - urlscan: - api_key: your-urlscan-api-key - - ipinfo: - api_key: your-ipinfo-api-key -` - -func EnsureConfigExists() error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("could not find home directory: %v", err) - } - - configDir := filepath.Join(home, ".config", "soc-cli") - configFile := filepath.Join(configDir, "config.yaml") - - // Create the directory if it doesn't exist - if _, err := os.Stat(configDir); os.IsNotExist(err) { - if err := os.MkdirAll(configDir, os.ModePerm); err != nil { - return fmt.Errorf("could not create config directory: %v", err) - } - } - - // Create file with default config if doesn't exist - if _, err := os.Stat(configFile); os.IsNotExist(err) { - defaultConfig := []byte(configTemplate) - if err := os.WriteFile(configFile, defaultConfig, 0644); err != nil { - return fmt.Errorf("could not create config file: %v", err) - } - fmt.Println("A new configuration file was created at:", configFile) - } - - return nil -} - -func LoadConfig() error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("could not find home directory: %v", err) - } - - configPath := filepath.Join(home, ".config", "soc-cli") - - viper.SetConfigName("config") - viper.SetConfigType("yaml") - viper.AddConfigPath(configPath) - - if err := viper.ReadInConfig(); err != nil { - return fmt.Errorf("error reading config file: %v", err) - } - - return nil -} diff --git a/go.mod b/go.mod index fdd9e77..d2a66fc 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,30 @@ module soc-cli -go 1.22.7 +go 1.24 require ( - github.com/fatih/color v1.17.0 - github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 + github.com/fatih/color v1.18.0 + github.com/rodaine/table v1.3.0 + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.20.1 + golang.org/x/term v0.31.0 ) require ( - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/rodaine/table v1.3.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e1cb65a..d403307 100644 --- a/go.sum +++ b/go.sum @@ -1,91 +1,78 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/internal/apis/abuseipdb.go b/internal/apis/abuseipdb.go index 5f07f6a..978aaf7 100644 --- a/internal/apis/abuseipdb.go +++ b/internal/apis/abuseipdb.go @@ -9,12 +9,13 @@ package apis import ( "fmt" "log" + "net" "soc-cli/internal/util" ) const abuseAPIURL = "https://api.abuseipdb.com/api/v2/check?ipAddress=%s&maxAgeInDays=90&verbose" -type abuseIPDBResponse struct { +type AbuseIPDBResponse struct { Data struct { IPAddress string `json:"ipAddress"` IsPublic bool `json:"isPublic"` @@ -38,17 +39,17 @@ type abuseIPDBResponse struct { } // getAbuseIPDBInfo fetches data from AbuseIPDB for a specific IP address -func GetAbuseIPDBInfo(ip string, apiKey string) *abuseIPDBResponse { - apiUrl := fmt.Sprintf(abuseAPIURL, ip) +func GetAbuseIPDBInfo(ip net.IP, apiKey string) *AbuseIPDBResponse { + apiUrl := fmt.Sprintf(abuseAPIURL, ip.String()) headers := map[string]string{ "Key": apiKey, "Accept": "application/json", } - var data abuseIPDBResponse + var data AbuseIPDBResponse - err := util.MakeGETRequest(apiUrl, headers, &data) + _, err := util.HTTPGetJSON(apiUrl, headers, &data) if err != nil { log.Fatalf("Error fetching AbuseIPDB info: %v", err) } diff --git a/internal/apis/greynoise.go b/internal/apis/greynoise.go index 7735c52..68d9887 100644 --- a/internal/apis/greynoise.go +++ b/internal/apis/greynoise.go @@ -9,31 +9,33 @@ package apis import ( "fmt" "log" + "net" "soc-cli/internal/util" ) const greyNoiseAPIURL = "https://api.greynoise.io/v3/community/%s" -type greyNoiseInfo struct { +type GreyNoiseInfo struct { IP string `json:"ip"` Noise bool `json:"noise"` Riot bool `json:"riot"` Classification string `json:"classification"` - Name string `json:"name"` Link string `json:"link"` + LastSeen string `json:"last_seen"` + Message string `json:"message"` } // Get threat intelligence from GreyNoise API -func GetGreyNoiseData(ip string, apiKey string) *greyNoiseInfo { - apiUrl := fmt.Sprintf(greyNoiseAPIURL, ip) +func GetGreyNoiseData(ip net.IP, apiKey string) *GreyNoiseInfo { + apiUrl := fmt.Sprintf(greyNoiseAPIURL, ip.String()) headers := map[string]string{ "key": apiKey, } - var greyNoiseData greyNoiseInfo + var greyNoiseData GreyNoiseInfo - err := util.MakeGETRequest(apiUrl, headers, &greyNoiseData) + _, err := util.HTTPGetJSON(apiUrl, headers, &greyNoiseData) if err != nil { log.Fatalf("Error fetching AbuseIPDB info: %v", err) } diff --git a/internal/apis/ipinfo.go b/internal/apis/ipinfo.go index 19a7dea..0b5c7be 100644 --- a/internal/apis/ipinfo.go +++ b/internal/apis/ipinfo.go @@ -9,24 +9,25 @@ package apis import ( "fmt" "log" + "net" "soc-cli/internal/util" ) const ipInfoAPIURL = "https://ipinfo.io/%s?token=%s" -type ipInfo struct { +type IPInfo struct { IP string `json:"ip"` Country string `json:"country"` Hostname string `json:"hostname"` Org string `json:"org"` } -func GetIPInfo(ip string, apiKey string) *ipInfo { - apiUrl := fmt.Sprintf(ipInfoAPIURL, ip, apiKey) +func GetIPInfo(ip net.IP, apiKey string) *IPInfo { + apiUrl := fmt.Sprintf(ipInfoAPIURL, ip.String(), apiKey) - var info ipInfo + var info IPInfo - err := util.MakeGETRequest(apiUrl, nil, &info) + _, err := util.HTTPGetJSON(apiUrl, nil, &info) if err != nil { log.Fatalf("Error fetching IP info: %v", err) } diff --git a/internal/apis/virustotal.go b/internal/apis/virustotal.go new file mode 100644 index 0000000..f5c7a73 --- /dev/null +++ b/internal/apis/virustotal.go @@ -0,0 +1,52 @@ +/* +Copyright © 2025 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package apis + +// Define the structs to match the JSON structure +type VTResponse struct { + Data Data `json:"data"` +} + +type Data struct { + ID string `json:"id"` + Type string `json:"type"` + Links Links `json:"links"` + Attributes Attributes `json:"attributes"` +} + +type Links struct { + Self string `json:"self"` +} + +type Attributes struct { + Reputation int `json:"reputation"` + LastModificationDate int64 `json:"last_modification_date"` + Magic string `json:"magic"` + Ssdeep string `json:"ssdeep"` + MeaningfulName string `json:"meaningful_name"` + TypeDescription string `json:"type_description"` + Vhash string `json:"vhash"` + Sha256 string `json:"sha256"` + TypeTags []string `json:"type_tags"` + TimesSubmitted int `json:"times_submitted"` + LastAnalysisStats LastAnalysisStats `json:"last_analysis_stats"` + TypeExtension string `json:"type_extension"` + TypeTag string `json:"type_tag"` + LastSubmissionDate int64 `json:"last_submission_date"` + Sha1 string `json:"sha1"` +} + +type LastAnalysisStats struct { + Malicious int `json:"malicious"` + Suspicious int `json:"suspicious"` + Undetected int `json:"undetected"` + Harmless int `json:"harmless"` + Timeout int `json:"timeout"` + ConfirmedTimeout int `json:"confirmed-timeout"` + Failure int `json:"failure"` + TypeUnsupported int `json:"type-unsupported"` +} diff --git a/internal/apis/whodat.go b/internal/apis/whodat.go index 237f873..b5ed6e8 100644 --- a/internal/apis/whodat.go +++ b/internal/apis/whodat.go @@ -68,7 +68,7 @@ func GetWhoisData(domain string) (*DomainInfo, error) { var whois DomainInfo - err := util.MakeGETRequest(apiUrl, nil, &whois) + _, err := util.HTTPGetJSON(apiUrl, nil, &whois) return &whois, err } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..a5e9b7d --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,62 @@ +/* +Copyright © 2024 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "log/slog" + + "github.com/fatih/color" + + "github.com/spf13/viper" +) + +func InitConfig() error { + + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("could not find home directory: %v", err) + } + + configPath := filepath.Join(home, ".config", "soc-cli") + + // Create the directory if it doesn't exist + if _, err := os.Stat(configPath); os.IsNotExist(err) { + if err := os.MkdirAll(configPath, os.ModePerm); err != nil { + return fmt.Errorf("could not create config directory: %v", err) + } + } + + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(configPath) + + viper.SetDefault("api_keys.urlscan.api_key", "") + viper.SetDefault("api_keys.ipinfo.api_key", "") + viper.SetDefault("api_keys.greynoise.api_key", "") + viper.SetDefault("api_keys.abuseipdb.api_key", "") + viper.SetDefault("api_keys.virustotal.api_key", "") + + if err := viper.SafeWriteConfig(); err != nil { + if _, ok := err.(viper.ConfigFileAlreadyExistsError); ok { + slog.Debug("Config file already exists, reading existing config") + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("error reading config file: %v", err) + } + } else { + return fmt.Errorf("error writing config file: %v", err) + } + } else { + color.Green("First execution, config file created") + os.Exit(0) + } + + return nil +} diff --git a/internal/logic/hashing.go b/internal/logic/hashing.go index 9bb85a8..ab2a173 100644 --- a/internal/logic/hashing.go +++ b/internal/logic/hashing.go @@ -16,8 +16,9 @@ import ( "os" ) -func ComputeMd5(file *os.File) string { +func ComputeFileMd5(file *os.File) string { hmd5 := md5.New() + file.Seek(0, 0) if _, err := io.Copy(hmd5, file); err != nil { log.Fatal("failed to calculate MD5 of file: %w", err) } @@ -26,8 +27,9 @@ func ComputeMd5(file *os.File) string { return hexmd5 } -func ComputeSha1(file *os.File) string { +func ComputeFileSha1(file *os.File) string { h1 := sha1.New() + file.Seek(0, 0) if _, err := io.Copy(h1, file); err != nil { log.Fatal("failed to calculate SHA1 of file: %w", err) } @@ -36,8 +38,9 @@ func ComputeSha1(file *os.File) string { return hex1 } -func ComputeSha256(file *os.File) string { +func ComputeFileSha256(file *os.File) string { h256 := sha256.New() + file.Seek(0, 0) if _, err := io.Copy(h256, file); err != nil { log.Fatal("failed to calculate SHA256 of file: %w", err) } diff --git a/internal/util/api.go b/internal/util/api.go index 47639eb..9ea941b 100644 --- a/internal/util/api.go +++ b/internal/util/api.go @@ -11,64 +11,65 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" - "os" - "strconv" ) -var debug bool +func GetRaw(url string, headers map[string]string) (body []byte, err error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + for key, value := range headers { + req.Header.Set(key, value) + } -func init() { - // Check if SOC_DEBUG is set and enable debug mode if it is - if val, exists := os.LookupEnv("SOC_DEBUG"); exists { - debug, _ = strconv.ParseBool(val) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err = io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) } + + return body, nil } -func MakeGETRequest(url string, headers map[string]string, target interface{}) error { +func HTTPGetJSON(url string, headers map[string]string, target any) (sc int, err error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - return fmt.Errorf("error creating request: %w", err) + return 0, fmt.Errorf("error creating request: %w", err) } for key, value := range headers { req.Header.Set(key, value) } - // Log request details if debug is enabled - if debug { - log.Printf("Making API request to URL: %s", url) - for key, value := range headers { - log.Printf("Header: %s = %s", key, value) - } - } - client := &http.Client{} resp, err := client.Do(req) if err != nil { - return fmt.Errorf("error making request: %w", err) + return resp.StatusCode, fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("error reading response body: %w", err) - } - - if debug { - log.Printf("Response body: %s", string(body)) + return resp.StatusCode, fmt.Errorf("error reading response body: %w", err) } err = json.Unmarshal(body, target) if err != nil { - return fmt.Errorf("error unmarshalling JSON response: %w", err) + return resp.StatusCode, fmt.Errorf("error unmarshalling JSON response: %w", err) } - return nil + return resp.StatusCode, nil } -func MakePOSTRequest(url string, headers map[string]string, body interface{}, target interface{}) error { +func HTTPPostJSON(url string, headers map[string]string, body any, target interface{}) error { // Marshal the body into JSON jsonBody, err := json.Marshal(body) if err != nil { @@ -87,15 +88,6 @@ func MakePOSTRequest(url string, headers map[string]string, body interface{}, ta // Set Content-Type header to application/json req.Header.Set("Content-Type", "application/json") - // Log request details if debug is enabled - if debug { - log.Printf("Making API request to URL: %s", url) - for key, value := range headers { - log.Printf("Header: %s = %s", key, value) - } - log.Printf("Request body: %s", string(jsonBody)) - } - client := &http.Client{} resp, err := client.Do(req) if err != nil { @@ -108,10 +100,6 @@ func MakePOSTRequest(url string, headers map[string]string, body interface{}, ta return fmt.Errorf("error reading response body: %w", err) } - if debug { - log.Printf("Response body: %s", string(bodyResp)) - } - err = json.Unmarshal(bodyResp, target) if err != nil { return fmt.Errorf("error unmarshalling JSON response: %w", err) diff --git a/internal/util/printing.go b/internal/util/printing.go new file mode 100644 index 0000000..b83f5e0 --- /dev/null +++ b/internal/util/printing.go @@ -0,0 +1,59 @@ +/* +Copyright © 2024 Alessandro Riva + +Licensed under the MIT License. +See the LICENSE file for details. +*/ +package util + +import ( + "bufio" + "fmt" + "github.com/fatih/color" + "golang.org/x/term" + "os" + "strings" +) + +func PrintEntry(entryName string, entryValue interface{}) { + if entryValue != nil { + switch v := entryValue.(type) { + case string: + if v != "" { + fmt.Printf("%s: %s\n", color.CyanString(entryName), v) + } + case bool: + fmt.Printf("%s: %t\n", color.CyanString(entryName), v) + case int: + fmt.Printf("%s: %d\n", color.CyanString(entryName), v) + default: + fmt.Printf("%s: %v\n", color.CyanString(entryName), v) + } + } +} + +func PrintYesNo(val bool) string { + if val { + return color.GreenString("YES") + } + return color.RedString("NO") +} + +// getPromptedInput prompts the user for input if the standard input is not a pipe +func GetPromptedInput(prompt string) string { + if !isInputFromPipe() { + fmt.Print(prompt) + } + + reader := bufio.NewReader(os.Stdin) + in, err := reader.ReadString('\n') + if err != nil { + color.Red("Error reading input: %v", err) + } + return strings.TrimSpace(in) +} + +// isInputFromPipe checks if the standard input is coming from a pipe +func isInputFromPipe() bool { + return !term.IsTerminal(int(os.Stdin.Fd())) +} diff --git a/internal/util/util.go b/internal/util/util.go index abe2f86..58f5aad 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -64,3 +64,7 @@ func formatDuration(d time.Duration) string { } return "just now" } + +func IsValidDomain(domain string) bool { + return DomainRegex.MatchString(domain) +} diff --git a/main.go b/main.go index 05872af..b9881f3 100644 --- a/main.go +++ b/main.go @@ -9,16 +9,13 @@ package main import ( "log" "soc-cli/cmd" - "soc-cli/config" + "soc-cli/internal/config" ) func main() { - if err := config.EnsureConfigExists(); err != nil { - log.Fatalf("Error ensuring config exists: %v", err) + if err := config.InitConfig(); err != nil { + log.Fatalf("Error initializing config: %v", err) } - if err := config.LoadConfig(); err != nil { - log.Fatalf("Failed to load config: %v", err) - } cmd.Execute() }