Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ff96812
Add file-check
alex27riva Dec 29, 2024
b6daedf
Fix file check
alex27riva Jan 2, 2025
b06f3e0
Refactor check IP
alex27riva Jan 2, 2025
cde5083
Smaller function
alex27riva Jan 2, 2025
cccb1c4
Fix
alex27riva Jan 2, 2025
555d366
Red color for errors
alex27riva Jan 3, 2025
d94b9b4
Base64 encode and decode
alex27riva Jan 3, 2025
97d42dc
Parsed VT response
alex27riva Jan 4, 2025
9dcab4e
Fixes
alex27riva Jan 4, 2025
c91c052
Add statuscode and remove redundant code
alex27riva Jan 4, 2025
c513ad5
Fix links in email
alex27riva Jan 4, 2025
4d8bf2d
Better email code
alex27riva Jan 4, 2025
372551b
Better IP code
alex27riva Jan 4, 2025
02df83e
Bump Go version
alex27riva Jan 5, 2025
8414727
Warn missing API keys
alex27riva Jan 12, 2025
f2ec122
Updated .gitignore
alex27riva Jan 12, 2025
7d5ce48
Better email code
alex27riva Jan 12, 2025
9269b03
Fix
alex27riva Jan 15, 2025
7ffe95d
Add title in urlscan
alex27riva Jan 24, 2025
69d8199
Fix scan link
alex27riva Jan 26, 2025
c6fc19a
Urlscan fixes
alex27riva Jan 31, 2025
63bc002
Better email header printing
alex27riva Jan 31, 2025
85c4928
Better defang code
alex27riva Jan 31, 2025
e04f3e1
Defang URL if malicious or flag is set
alex27riva Jan 31, 2025
1dbcd7e
Small fix
alex27riva Feb 1, 2025
5415f56
Better ip output
alex27riva Feb 1, 2025
c4cf243
Moved PrintEntry func
alex27riva Feb 1, 2025
f5a7b6b
Add reports flag
alex27riva Feb 1, 2025
9f9d16b
Colored urlscan output
alex27riva Feb 1, 2025
424f770
Fix
alex27riva Feb 3, 2025
fb2c952
Renamed functions
alex27riva Feb 6, 2025
1b8814f
Handle defanged IP
alex27riva Feb 6, 2025
319797f
Myip command
alex27riva Feb 6, 2025
a4f827b
Better defang code
alex27riva Feb 11, 2025
e777b3f
Add color in IP command
alex27riva Feb 19, 2025
b3ebe29
Remove debug in api.go
alex27riva Mar 11, 2025
c6d6f82
Better config
alex27riva Mar 15, 2025
f85bb01
Moved config
alex27riva Apr 20, 2025
42b9994
Update go.mod
alex27riva Apr 20, 2025
23d8fbd
chore: cleaner hashing code
alex27riva May 20, 2025
87c5e7f
feat: first version
alex27riva May 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build/
.idea/
*.eml
35 changes: 35 additions & 0 deletions cmd/b64d.go
Original file line number Diff line number Diff line change
@@ -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 <base64-string>",
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)
}
30 changes: 30 additions & 0 deletions cmd/b64e.go
Original file line number Diff line number Diff line change
@@ -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 <string-to-encode>",
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)
}
55 changes: 12 additions & 43 deletions cmd/defang.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
131 changes: 99 additions & 32 deletions cmd/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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.")
}
}
Loading