16 Commits

Author SHA1 Message Date
Evan Hosinski e9dc836c4e Remove SQLite dependencies from go.mod and go.sum 2026-06-04 12:46:11 -04:00
Evan Hosinski daf54bb8c1 Update import paths to use new module path hub.krkn.tech/KrakenTech/crowsnest across the project 2026-06-04 12:45:39 -04:00
Evan Hosinski f23bd04114 Fixed issue where query only exported to file 2026-04-07 09:37:41 -04:00
Evan Hosinski 5905b3478d Altered makefile to add new ldflags to remove debug info 2026-04-07 09:10:49 -04:00
Evan Hosinski 5c36b034b6 - Added datawells subcommand
- Altered the request format to match the new api request structure
- Altered max results per page to reflect updated Dehashed API max (10000)
2026-04-07 09:09:12 -04:00
Evan Hosinski da53a787fe Altered the badger db to derive the HWID from more static sources and to have a fallback in the event of a failure 2026-04-07 08:52:11 -04:00
Evan Hosinski d80ac68201 Merge remote-tracking branch 'origin/main' 2025-06-03 20:09:38 -04:00
Evan Hosinski 508d7d720e Add mutually exclusive flags to targets command 2025-06-03 20:09:34 -04:00
KrakenTech 98973d46ec Merge pull request #10 from Kraken-OffSec/reduce-default-results-columns
Refactor user and credential handling: rename Creds to User, update d…
2025-06-03 19:43:38 -04:00
Evan Hosinski 22beaf2310 Refactor user and credential handling: rename Creds to User, update database migrations, and add targets subcommand for exporting users and subdomains 2025-06-03 19:42:40 -04:00
KrakenTech 7cff4e70b8 Merge pull request #9 from Kraken-OffSec/add-targets-subcommand-option
Refactor user and credential handling: rename Creds to User, update d…
2025-06-03 19:29:44 -04:00
Evan Hosinski 0bd9347074 Refactor user and credential handling: rename Creds to User, update database migrations, and add targets subcommand for exporting users and subdomains 2025-06-03 19:29:10 -04:00
KrakenTech 9a22445e55 Merge pull request #8 from Kraken-OffSec/fixed-local-db-output-file-location
Fixed error where the local db instance would be written to crowsnest…
2025-06-03 18:10:18 -04:00
Evan Hosinski e167a10fcc Fixed error where the local db instance would be written to crowsnest.sql/crowsnest.sql 2025-06-03 18:08:57 -04:00
Evan Hosinski 84f3becdf2 Fixed root.go branding 2025-05-22 10:47:10 -04:00
KrakenTech a2358d0714 Update Makefile 2025-05-21 09:26:35 -04:00
36 changed files with 1697 additions and 695 deletions
+3 -3
View File
@@ -16,7 +16,7 @@ PLATFORMS=linux darwin windows
ARCHS=amd64 arm64 ARCHS=amd64 arm64
# Version info from git tag or default # Version info from git tag or default
VERSION=$(shell git describe --tags 2>/dev/null || echo "v1.3.1") VERSION=$(shell git describe --tags 2>/dev/null || echo "v1.3.3")
.PHONY: all clean build build-all .PHONY: all clean build build-all
@@ -30,14 +30,14 @@ clean:
# Build for current platform # Build for current platform
build: build:
CGO_ENABLED=0 $(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) -ldflags "-X main.version=$(VERSION)" crowsnest.go CGO_ENABLED=0 $(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) -ldflags "-X main.version=$(VERSION) -s -w" crowsnest.go
# Build for all platforms # Build for all platforms
build-all: clean build-all: clean
@for platform in $(PLATFORMS); do \ @for platform in $(PLATFORMS); do \
for arch in $(ARCHS); do \ for arch in $(ARCHS); do \
echo "Building for $$platform/$$arch..."; \ echo "Building for $$platform/$$arch..."; \
GOOS=$$platform GOARCH=$$arch CGO_ENABLED=0 $(GO) build -o $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch -ldflags "-X main.version=$(VERSION)" crowsnest.go; \ GOOS=$$platform GOARCH=$$arch CGO_ENABLED=0 $(GO) build -o $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch -ldflags "-X main.version=$(VERSION) -s -w" crowsnest.go; \
if [ "$$platform" = "windows" ]; then \ if [ "$$platform" = "windows" ]; then \
mv $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch.exe; \ mv $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch.exe; \
fi; \ fi; \
+17 -4
View File
@@ -63,7 +63,7 @@ To configure the database location:
CrowsNest requires an API key from Dehashed. Set it up with: CrowsNest requires an API key from Dehashed. Set it up with:
![Alt text](.img/set-dehashed.png "Set Dehashed Key") ![Alt text](.img/set-dehashed.png "Set Dehashed Key")
```bash ```bash
ar1ste1a@kali:~$ crowsnest set-dehashed <redacted> ar1ste1a@kali:~$ crowsnest set dehashed <redacted>
``` ```
### Simple Query ### Simple Query
@@ -134,10 +134,23 @@ crowsnest dehashed -R -E 'joh?n(ath[oa]n)' -D hotmail.com'
CrowsNest is capable of handling output formats. CrowsNest is capable of handling output formats.
The default output format is JSON. The default output format is JSON.
To change the output format, use the `-f` flag. To change the output format, use the `-f` flag.
CrowsNest currently supports JSON, YAML, XML, and TEXT output formats. CrowsNest currently supports JSON, YAML, XML, TEXT, and GREP output formats.
``` go ``` go
# Return matches for usernames exactly matching "admin" and write to text file 'admins_file.txt' # Return matches for usernames exactly matching "admin" and write to text file 'admins_file.txt'
crowsnest dehashed -U admin -o admins_file -f txt crowsnest dehashed -U admin -o admins_file -f txt
# Return one key=value record per line in a greppable file 'admins_file.grep'
crowsnest dehashed -U admin -o admins_file -f grep
```
### Data Wells
DeHashed data wells are free to query and do not require a paid API account.
``` go
# List the first page of data wells and write 'data_wells.json'
crowsnest dehashed data-wells
# Sort by record count and write one key=value record per line
crowsnest dehashed data-wells --sort records-DESC --count 50 -f grep -o data_wells
``` ```
--- ---
@@ -216,11 +229,11 @@ crowsnest whois -n google.com
## 🌐 Hunter.io ## 🌐 Hunter.io
CrowsNest supports Hunter.io lookups. CrowsNest supports Hunter.io lookups.
Hunter.io lookups require a separate API Key from the Dehashed API. Hunter.io lookups require a separate API Key from the Dehashed API.
This can be set using the `set-hunter` command. This can be set using the `set hunter` command.
![Alt text](.img/set-hunter.png "Set Dehashed Key") ![Alt text](.img/set-hunter.png "Set Dehashed Key")
```bash ```bash
# Set the Hunter.io API key # Set the Hunter.io API key
crowsnest set-hunter <redacted> crowsnest set hunter <redacted>
``` ```
### Domain Search ### Domain Search
+70 -9
View File
@@ -1,31 +1,35 @@
package cmd package cmd
import ( import (
"crowsnest/internal/badger"
"crowsnest/internal/debug"
"crowsnest/internal/dehashed"
"crowsnest/internal/sqlite"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/badger"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/dehashed"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
func init() { func init() {
// Add api command to root command // Add api command to root command
rootCmd.AddCommand(dehashedCmd) rootCmd.AddCommand(dehashedCmd)
dehashedCmd.AddCommand(dehashedDataWellsCmd)
// Add flags specific to api command // Add flags specific to api command
dehashedCmd.Flags().IntVarP(&maxRecords, "max-records", "m", 30000, "Maximum amount of records to return") dehashedCmd.Flags().IntVarP(&maxRecords, "max-records", "m", 50000, "Maximum total records to return (max 50000)")
dehashedCmd.Flags().IntVarP(&maxRequests, "max-requests", "r", -1, "Maximum number of requests to make") dehashedCmd.Flags().IntVarP(&maxRequests, "max-requests", "r", -1, "Maximum number of requests to make")
dehashedCmd.Flags().IntVarP(&startingPage, "starting-page", "s", 1, "Starting page for requests") dehashedCmd.Flags().IntVarP(&startingPage, "starting-page", "s", 1, "Starting page for requests")
dehashedCmd.Flags().BoolVarP(&printBalance, "print-balance", "b", false, "Print remaining balance after requests") dehashedCmd.Flags().BoolVarP(&printBalance, "print-balance", "b", false, "Print remaining balance after requests")
dehashedCmd.Flags().BoolVarP(&regexMatch, "regex-match", "R", false, "Use regex matching on query fields") dehashedCmd.Flags().BoolVarP(&regexMatch, "regex-match", "R", false, "Use regex matching on query fields")
dehashedCmd.Flags().BoolVarP(&wildcardMatch, "wildcard-match", "W", false, "Use wildcard matching on query fields (Use ? to replace a single character, and * for multiple characters)") dehashedCmd.Flags().BoolVarP(&wildcardMatch, "wildcard-match", "W", false, "Use wildcard matching on query fields (Use ? to replace a single character, and * for multiple characters)")
dehashedCmd.Flags().BoolVarP(&credsOnly, "creds-only", "C", false, "Return credentials only") dehashedCmd.Flags().BoolVarP(&credsOnly, "creds-only", "C", false, "Return credentials only")
dehashedCmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json, yaml, xml, txt)") dehashedCmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json, yaml, xml, txt, grep)")
dehashedCmd.Flags().StringVarP(&outputFile, "output", "o", "query", "File to output results to including extension") dehashedCmd.Flags().StringVarP(&outputFile, "output", "o", "query", "File to output results to without extension")
dehashedCmd.Flags().StringVarP(&usernameQuery, "username", "U", "", "Username query") dehashedCmd.Flags().StringVarP(&usernameQuery, "username", "U", "", "Username query")
dehashedCmd.Flags().StringVarP(&emailQuery, "email-query", "E", "", "HunterEmail query") dehashedCmd.Flags().StringVarP(&emailQuery, "email-query", "E", "", "Email query")
dehashedCmd.Flags().StringVarP(&ipQuery, "ip", "I", "", "IP address query") dehashedCmd.Flags().StringVarP(&ipQuery, "ip", "I", "", "IP address query")
dehashedCmd.Flags().StringVarP(&domainQuery, "domain", "D", "", "Domain query") dehashedCmd.Flags().StringVarP(&domainQuery, "domain", "D", "", "Domain query")
dehashedCmd.Flags().StringVarP(&passwordQuery, "password", "P", "", "Password query") dehashedCmd.Flags().StringVarP(&passwordQuery, "password", "P", "", "Password query")
@@ -40,6 +44,12 @@ func init() {
// Add mutually exclusive flags to wildcard match and regex match // Add mutually exclusive flags to wildcard match and regex match
dehashedCmd.MarkFlagsMutuallyExclusive("regex-match", "wildcard-match") dehashedCmd.MarkFlagsMutuallyExclusive("regex-match", "wildcard-match")
dehashedDataWellsCmd.Flags().IntVar(&dataWellsCount, "count", 20, "Number of data wells to return (20 or 50)")
dehashedDataWellsCmd.Flags().IntVarP(&dataWellsPage, "page", "p", 1, "Data wells page to request")
dehashedDataWellsCmd.Flags().StringVar(&dataWellsSort, "sort", "", "Sort data wells by added, name, date, or records; optionally suffix -ASC or -DESC")
dehashedDataWellsCmd.Flags().StringVarP(&dataWellsOutputFormat, "format", "f", "json", "Output format (json, yaml, xml, txt, grep)")
dehashedDataWellsCmd.Flags().StringVarP(&dataWellsOutputFile, "output", "o", "data_wells", "File to output data wells to without extension")
} }
var ( var (
@@ -66,6 +76,11 @@ var (
phoneQuery string phoneQuery string
socialQuery string socialQuery string
cryptoCurrencyAddressQuery string cryptoCurrencyAddressQuery string
dataWellsCount int
dataWellsPage int
dataWellsSort string
dataWellsOutputFormat string
dataWellsOutputFile string
// Query command // Query command
dehashedCmd = &cobra.Command{ dehashedCmd = &cobra.Command{
@@ -77,7 +92,7 @@ var (
// Validate credentials // Validate credentials
if key == "" { if key == "" {
fmt.Println("API key is required. Set the key with the \"set-key\" command. [crowsnest set-key <api_key>]") fmt.Println("API key is required. Set the key with the \"set dehashed\" command. [crowsnest set dehashed <api_key>]")
return return
} }
@@ -118,6 +133,7 @@ var (
dehasher.Start() dehasher.Start()
fmt.Println("\n[*] Completing Process") fmt.Println("\n[*] Completing Process")
// Store query options
err := sqlite.StoreDehashedQueryOptions(queryOptions) err := sqlite.StoreDehashedQueryOptions(queryOptions)
if err != nil { if err != nil {
if debugGlobal { if debugGlobal {
@@ -132,9 +148,54 @@ var (
} }
}, },
} }
dehashedDataWellsCmd = &cobra.Command{
Use: "data-wells",
Short: "List DeHashed data wells",
Long: `List DeHashed data wells. This endpoint is free and does not require a DeHashed API key or subscription.`,
Run: func(cmd *cobra.Command, args []string) {
client := dehashed.NewDehashedClientV2("", debugGlobal)
response, err := client.DataWells(dehashed.DataWellsRequest{
Count: dataWellsCount,
Page: dataWellsPage,
Sort: dataWellsSort,
})
if err != nil {
fmt.Printf("[!] Error querying data wells: %v\n", err)
return
}
fType := files.GetFileType(dataWellsOutputFormat)
if dataWellsOutputFile != "" {
fmt.Printf("[*] Writing data wells to file: %s%s\n", dataWellsOutputFile, fType.Extension())
if err := dehashed.WriteDataWellsToFile(response, dataWellsOutputFile, fType); err != nil {
fmt.Printf("[!] Error writing data wells to file: %v\n", err)
return
}
}
fmt.Printf("[+] Retrieved %d data wells (total: %d, next page: %t)\n", len(response.DataWells), response.Total, response.NextPage)
printDataWellsTable(response.DataWells)
},
}
) )
// Helper functions to get stored API credentials // Helper functions to get stored API credentials
func getDehashedApiKey() string { func getDehashedApiKey() string {
return badger.GetDehashedKey() return badger.GetDehashedKey()
} }
func printDataWellsTable(dataWells []dehashed.DataWell) {
headers := []string{"Name", "Date", "Records", "Sensitive", "Data"}
rows := make([][]string, 0, len(dataWells))
for _, well := range dataWells {
rows = append(rows, []string{
well.Name,
well.Date,
fmt.Sprintf("%d", well.Records),
fmt.Sprintf("%t", well.IsSensitive),
well.Data,
})
}
pretty.Table(headers, rows)
}
-311
View File
@@ -1,311 +0,0 @@
package cmd
import (
"crowsnest/internal/export"
"crowsnest/internal/files"
"crowsnest/internal/sqlite"
"fmt"
"github.com/spf13/cobra"
"go.uber.org/zap"
"strings"
)
func init() {
// Add Subcommand to db command
rootCmd.AddCommand(exportCmd)
// Add flags specific to export command
exportCmd.Flags().IntVarP(&exportLimitRows, "limit", "l", 100, "Limit number of results")
exportCmd.Flags().BoolVarP(&exportListAll, "list-all", "a", false, "List all tables and their columns")
exportCmd.Flags().StringVarP(&exportTableName, "table", "t", "", "Table to export (results, creds, whois, subdomains, history, runs)")
exportCmd.Flags().StringVarP(&exportNotNull, "not-null", "n", "", "Filter for non-null values (comma-separated list, e.g., 'password,email')")
exportCmd.Flags().StringVarP(&exportColumns, "columns", "c", "", "Columns to display in output (comma-separated list, e.g., 'username,email,password')")
exportCmd.Flags().StringVarP(&exportUserQuery, "user-query", "q", "", "User query to execute")
exportCmd.Flags().StringVarP(&exportRawQuery, "raw-query", "r", "", "Raw SQL query to execute")
exportCmd.Flags().StringVarP(&exportFormat, "format", "f", "json", "Output format (json, yaml, xml, txt)")
exportCmd.Flags().StringVarP(&exportFile, "file", "o", "export", "File to output results to including extension")
// Add mutually exclusive flags to query and raw-query
// Cannot use query and raw-query at the same time
exportCmd.MarkFlagsMutuallyExclusive("user-query", "raw-query")
// Raw query does not require a table
exportCmd.MarkFlagsMutuallyExclusive("user-query", "table")
// List all columns does not require a query or raw-query
exportCmd.MarkFlagsMutuallyExclusive("raw-query", "list-all")
}
// DB export command
var (
exportLimitRows int
exportListAll bool
exportTableName string
exportNotNull string
exportColumns string
exportUserQuery string
exportRawQuery string
exportFormat string
exportFile string
exportCmd = &cobra.Command{
Use: "export",
Short: "Export database to file",
Run: func(cmd *cobra.Command, args []string) {
// If list-all flag is set, list all tables and columns
if exportListAll {
listAvailableTables()
return
}
fmt.Println("[*] Exporting database...")
// If Raw Query is set, execute it and export
if exportRawQuery != "" {
fmt.Println("[*] Executing Raw Query...")
exportRawDBQuery()
return
}
// Validate table name
if exportTableName == "" {
fmt.Println("[!] Error: Table name is required. Use -t or --table to specify a table.")
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.")
return
}
if !isValidTable(exportTableName) {
fmt.Printf("[!] Error: Unknown table '%s'.\n", exportTableName)
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.")
return
}
// Validate columns if specified
if exportColumns != "" {
columns := strings.Split(exportColumns, ",")
invalidColumns := validateColumns(exportTableName, columns)
if len(invalidColumns) > 0 {
fmt.Printf("[!] Error: Invalid column(s) for table '%s': %s\n",
exportTableName, strings.Join(invalidColumns, ", "))
fmt.Println("[*] Available columns for this table:")
for i := 0; i < len(availableTables[exportTableName]); i += 5 {
end := i + 5
if end > len(availableTables[exportTableName]) {
end = len(availableTables[exportTableName])
}
fmt.Printf(" %s\n", strings.Join(availableTables[exportTableName][i:end], ", "))
}
return
}
}
// Validate not-null fields if specified
if exportNotNull != "" {
notNullFields := strings.Split(exportNotNull, ",")
invalidFields := validateColumns(exportTableName, notNullFields)
if len(invalidFields) > 0 {
fmt.Printf("[!] Error: Invalid not-null field(s) for table '%s': %s\n",
exportTableName, strings.Join(invalidFields, ", "))
fmt.Println("[*] Available columns for this table:")
for i := 0; i < len(availableTables[exportTableName]); i += 5 {
end := i + 5
if end > len(availableTables[exportTableName]) {
end = len(availableTables[exportTableName])
}
fmt.Printf(" %s\n", strings.Join(availableTables[exportTableName][i:end], ", "))
}
return
}
}
// Determine which table to query based on the tableTypeDBQuery parameter
table := sqlite.GetTable(exportTableName)
if table == sqlite.UnknownTable {
fmt.Printf("[!] Error: Unknown table type '%s'.\n", exportTableName)
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.")
return
}
fmt.Println("[*] Querying Database...")
exportTableQuery(table)
},
}
)
// exportTableQuery queries a table and exports the results
func exportTableQuery(table sqlite.Table) {
// Get the columns to query
columns := []string{"*"}
if exportColumns != "" {
columns = strings.Split(exportColumns, ",")
}
// Get the not null fields
notNullFields := []string{}
if exportNotNull != "" {
notNullFields = strings.Split(exportNotNull, ",")
}
// Get the user query
userQuery := ""
if exportUserQuery != "" {
userQuery = exportUserQuery
}
// Get the limit
limit := exportLimitRows
// Get the object for the table
object := table.Object()
// Check if object is nil (invalid table)
if object == nil {
fmt.Printf("[!] Error: Table '%s' is not valid or does not exist.\n", exportTableName)
return
}
// Query the database
db := sqlite.GetDB()
query := db.Model(object).Select(columns)
if len(notNullFields) > 0 {
for _, field := range notNullFields {
query = query.Where(fmt.Sprintf("%s IS NOT NULL", field))
}
}
if userQuery != "" {
query = query.Where(userQuery)
}
if limit > 0 {
query = query.Limit(limit)
}
rows, err := query.Rows()
if err != nil {
zap.L().Error("export_query",
zap.String("message", "failed to execute query"),
zap.Error(err),
)
fmt.Printf("[!] Error executing query: %v\n", err)
return
}
defer rows.Close()
// Get the columns
cols, err := rows.Columns()
if err != nil {
zap.L().Error("export_query",
zap.String("message", "failed to get columns from query"),
zap.Error(err),
)
fmt.Printf("[!] Error getting columns from query: %v\n", err)
return
}
// Prepare data for export
var results []map[string]interface{}
// Process the rows
for rows.Next() {
values := make([]interface{}, len(cols))
pointers := make([]interface{}, len(cols))
for i := range values {
pointers[i] = &values[i]
}
if err := rows.Scan(pointers...); err != nil {
zap.L().Error("export_query",
zap.String("message", "failed to scan row from query"),
zap.Error(err),
)
fmt.Printf("[!] Error scanning row from query: %v\n", err)
return
}
// Create a map for this row
rowMap := make(map[string]interface{})
for i, col := range cols {
val := values[i]
rowMap[col] = val
}
results = append(results, rowMap)
}
// Export the results
exportResults(results)
}
// exportRawDBQuery executes a raw query and exports the results
func exportRawDBQuery() {
db := sqlite.GetDB()
rows, err := db.Raw(exportRawQuery).Rows()
if err != nil {
zap.L().Error("export_raw_query",
zap.String("message", "failed to execute raw query"),
zap.Error(err),
)
fmt.Printf("[!] Error executing raw query: %v\n", err)
return
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
zap.L().Error("export_raw_query",
zap.String("message", "failed to get columns from raw query"),
zap.Error(err),
)
fmt.Printf("[!] Error getting columns from raw query: %v\n", err)
return
}
// Prepare data for export
var results []map[string]interface{}
// Process the rows
for rows.Next() {
values := make([]interface{}, len(columns))
pointers := make([]interface{}, len(columns))
for i := range values {
pointers[i] = &values[i]
}
if err := rows.Scan(pointers...); err != nil {
zap.L().Error("export_raw_query",
zap.String("message", "failed to scan row from raw query"),
zap.Error(err),
)
fmt.Printf("[!] Error scanning row from raw query: %v\n", err)
return
}
// Create a map for this row
rowMap := make(map[string]interface{})
for i, col := range columns {
val := values[i]
rowMap[col] = val
}
results = append(results, rowMap)
}
// Export the results
exportResults(results)
}
// exportResults exports the results to a file
func exportResults(results []map[string]interface{}) {
// Get file type
fileType := files.GetFileType(exportFormat)
// Export results
err := export.WriteQueryResultsToFile(results, exportFile, fileType)
if err != nil {
zap.L().Error("export_results",
zap.String("message", "failed to write to file"),
zap.Error(err),
)
fmt.Printf("[!] Error writing to file: %v\n", err)
return
}
fmt.Printf("[+] Exported %d records to file: %s%s\n", len(results), exportFile, fileType.Extension())
}
+28 -15
View File
@@ -1,16 +1,18 @@
package cmd package cmd
import ( import (
"crowsnest/internal/badger"
"crowsnest/internal/debug"
"crowsnest/internal/export"
"crowsnest/internal/files"
hunter "crowsnest/internal/hunter.io"
"crowsnest/internal/pretty"
"fmt" "fmt"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
"time" "hub.krkn.tech/KrakenTech/crowsnest/internal/badger"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/export"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
hunter "hub.krkn.tech/KrakenTech/crowsnest/internal/hunter.io"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
func init() { func init() {
@@ -111,6 +113,24 @@ var (
return return
} }
// Store the users discovered
var creds []sqlite.User
for _, email := range result.Emails {
creds = append(creds, sqlite.User{Email: email.Value})
}
err = sqlite.StoreUsers(creds)
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to store hunter domain search")
debug.PrintError(err)
}
zap.L().Error("store_hunter_domain_search",
zap.String("message", "failed to store hunter domain search"),
zap.Error(err),
)
fmt.Printf("Error storing Hunter.io Domain Search Result: %v\n", err)
}
// Write Hunter.io Domain Search Result to file // Write Hunter.io Domain Search Result to file
fmt.Printf("[*] Writing Hunter.io Domain Search Result to file: %s%s\n", hunterOutputFile, fType.Extension()) fmt.Printf("[*] Writing Hunter.io Domain Search Result to file: %s%s\n", hunterOutputFile, fType.Extension())
err = export.WriteIStringToFile(result, hunterOutputFile, fType) err = export.WriteIStringToFile(result, hunterOutputFile, fType)
@@ -220,24 +240,17 @@ var (
// Pretty Print Hunter.io Email Verification Result // Pretty Print Hunter.io Email Verification Result
var ( var (
headers = []string{"Email", "Status", "Result", "Score", "Regexp", "Gibberish", "Disposable", "Webmail", "MX Records", "SMTP Server", "SMTP Check", "Accept All", "Block", "Sources"} headers = []string{"Email", "Result", "Score", "Disposable", "MX Records", "SMTP Server", "SMTP Check"}
rows [][]string rows [][]string
) )
rows = append(rows, []string{ rows = append(rows, []string{
result.Email, result.Email,
result.Status,
result.Result, result.Result,
fmt.Sprintf("%d", result.Score), fmt.Sprintf("%d", result.Score),
fmt.Sprintf("%t", result.Regexp),
fmt.Sprintf("%t", result.Gibberish),
fmt.Sprintf("%t", result.Disposable), fmt.Sprintf("%t", result.Disposable),
fmt.Sprintf("%t", result.Webmail),
fmt.Sprintf("%t", result.MXRecords), fmt.Sprintf("%t", result.MXRecords),
fmt.Sprintf("%t", result.SMTPServer), fmt.Sprintf("%t", result.SMTPServer),
fmt.Sprintf("%t", result.SMTPCheck), fmt.Sprintf("%t", result.SMTPCheck),
fmt.Sprintf("%t", result.AcceptAll),
fmt.Sprintf("%t", result.Block),
fmt.Sprintf("%v", result.Sources),
}) })
fmt.Println("Email Verification Result:") fmt.Println("Email Verification Result:")
+4 -3
View File
@@ -1,15 +1,16 @@
package cmd package cmd
import ( import (
"crowsnest/internal/easyTime"
"crowsnest/internal/pretty"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/spf13/cobra"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/spf13/cobra"
"hub.krkn.tech/KrakenTech/crowsnest/internal/easyTime"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
) )
func init() { func init() {
+119 -134
View File
@@ -1,153 +1,34 @@
package cmd package cmd
import ( import (
"crowsnest/internal/pretty"
"crowsnest/internal/sqlite"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
"strings" "hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/export"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
// Map of available tables and their columns
var availableTables = map[string][]string{
"creds": {
"id", "created_at", "updated_at", "deleted_at", "email", "username", "password",
},
//"history": {
// "id", "created_at", "updated_at", "deleted_at", "domain_name", "domain_type",
// "registrar_name", "whois_server", "created_date_iso8601", "updated_date_iso8601", "expires_date_iso8601",
//},
"lookup": {
"id", "created_at", "updated_at", "deleted_at", "search_term", "type", "first_seen", "last_visit",
"name",
},
// Query Options
"runs": {
"id", "created_at", "updated_at", "deleted_at", "max_records", "max_requests", "starting_page",
"output_format", "output_file", "regex_match", "wildcard_match", "username_query", "email_query",
"ip_query", "pass_query", "hash_query", "name_query", "domain_query", "vin_query", "license_plate_query",
"address_query", "phone_query", "social_query", "crypto_address_query", "print_balance", "creds_only",
},
"results": {
"id", "created_at", "updated_at", "deleted_at", "dehashed_id", "email", "ip_address", "username",
"password", "hashed_password", "hash_type", "name", "vin", "license_plate", "url", "social",
"cryptocurrency_address", "address", "phone", "company", "database_name",
},
"subdomains": {
"id", "created_at", "updated_at", "deleted_at", "domain", "first_seen", "last_seen",
},
"whois": {
"id", "created_at", "updated_at", "deleted_at", "audit", "contact_email", "created_date", "created_date_normalized",
"domain_name", "domain_name_ext", "estimated_domain_age", "expires_date", "expires_date_normalized", "footer", "header",
"name_servers", "parse_code", "raw_text", "registrant", "registrar_iana_id", "registrar_name", "registry_data",
"status", "stripped_text", "updated_date", "updated_date_normalized",
},
"hunter_domain": {
"id", "created_at", "updated_at", "deleted_at", "domain", "disposable", "webmail", "accept_all", "pattern",
"organization", "description", "industry", "twitter", "facebook", "linkedin", "instagram", "youtube",
"technologies", "country", "state", "city", "postal_code", "street", "headcount", "company_type", "emails", "linked_domains",
},
"hunter_email": {
"id", "created_at", "updated_at", "deleted_at", "value", "type", "confidence", "sources", "first_name", "last_name",
"position", "position_raw", "seniority", "department", "linkedin", "twitter", "phone_number", "verification_date", "verification_status",
},
}
// Function to list available tables and their columns
func listAvailableTables() {
fmt.Println("Available tables and columns:")
// Prepare data for pretty.Table
headers := []string{"Table", "Columns"}
var tableRows [][]string
// Sort tables alphabetically for consistent output
var tableNames []string
for tableName := range availableTables {
tableNames = append(tableNames, tableName)
}
// Simple bubble sort for table names
for i := 0; i < len(tableNames)-1; i++ {
for j := 0; j < len(tableNames)-i-1; j++ {
if tableNames[j] > tableNames[j+1] {
tableNames[j], tableNames[j+1] = tableNames[j+1], tableNames[j]
}
}
}
// Create rows for the table
for _, tableName := range tableNames {
columns := availableTables[tableName]
// Format columns with line breaks for better readability
var formattedColumns string
for i := 0; i < len(columns); i += 5 {
end := i + 5
if end > len(columns) {
end = len(columns)
}
if i > 0 {
formattedColumns += "\n"
}
formattedColumns += strings.Join(columns[i:end], ", ")
}
tableRows = append(tableRows, []string{tableName, formattedColumns})
}
// Display the table
pretty.Table(headers, tableRows)
}
// Function to validate table name
func isValidTable(tableName string) bool {
_, exists := availableTables[tableName]
return exists
}
// Function to validate column names for a specific table
func validateColumns(tableName string, columns []string) []string {
if tableName == "" || columns == nil || len(columns) == 0 || columns[0] == "*" {
return nil
}
tableColumns, exists := availableTables[tableName]
if !exists {
return []string{fmt.Sprintf("Table '%s' does not exist", tableName)}
}
var invalidColumns []string
for _, col := range columns {
valid := false
for _, tableCol := range tableColumns {
if col == tableCol {
valid = true
break
}
}
if !valid {
invalidColumns = append(invalidColumns, col)
}
}
return invalidColumns
}
func init() { func init() {
// Add whois command to root command // Add whois command to root command
rootCmd.AddCommand(queryCmd) rootCmd.AddCommand(queryCmd)
// Add flags specific to whois command // Add flags specific to whois command
queryCmd.Flags().StringVarP(&dbQueryTableName, "table", "t", "", "Table to query (results, creds, whois, subdomains, history, runs)") queryCmd.Flags().StringVarP(&dbQueryTableName, "table", "t", "", "Table to query (dehashed, users, whois, subdomains, lookup, runs)")
queryCmd.Flags().IntVarP(&dbQueryLimitRows, "limit", "l", 100, "Limit number of results") queryCmd.Flags().IntVarP(&dbQueryLimitRows, "limit", "l", 100, "Limit number of results")
queryCmd.Flags().StringVarP(&dbQueryNotNull, "not-null", "n", "", "Filter for non-null values (comma-separated list, e.g., 'password,email')") queryCmd.Flags().StringVarP(&dbQueryNotNull, "not-null", "n", "", "Filter for non-null values (comma-separated list, e.g., 'password,email')")
queryCmd.Flags().StringVarP(&dbQueryColumns, "columns", "c", "", "Columns to display in output (comma-separated list, e.g., 'username,email,password')") queryCmd.Flags().StringVarP(&dbQueryColumns, "columns", "c", "", "Columns to display in output (comma-separated list, e.g., 'username,email,password')")
queryCmd.Flags().StringVarP(&dbQueryUserQuery, "user-query", "q", "", "User query to execute") queryCmd.Flags().StringVarP(&dbQueryUserQuery, "user-query", "q", "", "User query to execute")
queryCmd.Flags().StringVarP(&dbQueryRawQuery, "raw-query", "r", "", "Raw SQL query to execute") queryCmd.Flags().StringVarP(&dbQueryRawQuery, "raw-query", "r", "", "Raw SQL query to execute")
queryCmd.Flags().BoolVarP(&dbQueryListAll, "list-all", "a", false, "List all tables and their columns") queryCmd.Flags().BoolVarP(&dbQueryListAll, "list-all", "a", false, "List all tables and their columns")
queryCmd.Flags().BoolVarP(&dbQueryExport, "export", "x", false, "Export results to file using --file and --format")
queryCmd.Flags().StringVarP(&dbQueryFormat, "format", "f", "json", "Output format (json, yaml, xml, txt, grep)")
queryCmd.Flags().StringVarP(&dbQueryFile, "file", "o", "query", "File to output results to when --export is set")
// Add mutually exclusive flags to query and raw-query // Add mutually exclusive flags to query and raw-query
// Cannot use query and raw-query at the same time // Cannot use query and raw-query at the same time
@@ -166,11 +47,15 @@ var (
dbQueryUserQuery string dbQueryUserQuery string
dbQueryRawQuery string dbQueryRawQuery string
dbQueryListAll bool dbQueryListAll bool
dbQueryExport bool
dbQueryFormat string
dbQueryFile string
queryCmd = &cobra.Command{ queryCmd = &cobra.Command{
Use: "query", Use: "query",
Short: "Query the database", Short: "Query the database",
Long: `Query the database for various information.`, Long: `Query the database for various information.
Use --export with --file and --format to write results to a file instead of displaying them.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// If list-all flag is set, list all tables and columns // If list-all flag is set, list all tables and columns
if dbQueryListAll { if dbQueryListAll {
@@ -188,14 +73,14 @@ var (
// Validate table name // Validate table name
if dbQueryTableName == "" { if dbQueryTableName == "" {
fmt.Println("[!] Error: Table name is required. Use -t or --table to specify a table.") fmt.Println("[!] Error: Table name is required. Use -t or --table to specify a table.")
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs") fmt.Println("[*] Available tables: dehashed, users, whois, subdomains, lookup, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.") fmt.Println("[*] Use --list-all to see all tables and their columns.")
return return
} }
if !isValidTable(dbQueryTableName) { if !isValidTable(dbQueryTableName) {
fmt.Printf("[!] Error: Unknown table '%s'.\n", dbQueryTableName) fmt.Printf("[!] Error: Unknown table '%s'.\n", dbQueryTableName)
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs") fmt.Println("[*] Available tables: dehashed, users, whois, subdomains, lookup, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.") fmt.Println("[*] Use --list-all to see all tables and their columns.")
return return
} }
@@ -242,7 +127,7 @@ var (
table := sqlite.GetTable(dbQueryTableName) table := sqlite.GetTable(dbQueryTableName)
if table == sqlite.UnknownTable { if table == sqlite.UnknownTable {
fmt.Printf("[!] Error: Unknown table type '%s'.\n", dbQueryTableName) fmt.Printf("[!] Error: Unknown table type '%s'.\n", dbQueryTableName)
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs") fmt.Println("[*] Available tables: dehashed, users, whois, subdomains, lookup, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.") fmt.Println("[*] Use --list-all to see all tables and their columns.")
return return
} }
@@ -321,6 +206,46 @@ func tableQuery(table sqlite.Table) {
return return
} }
if dbQueryExport {
fmt.Println("[*] Exporting results to file...")
if debugGlobal {
debug.PrintInfo("exporting results to file: " + dbQueryFile)
}
// Prepare data for export
var results []map[string]interface{}
// Process the rows
for rows.Next() {
values := make([]interface{}, len(cols))
pointers := make([]interface{}, len(cols))
for i := range values {
pointers[i] = &values[i]
}
if err := rows.Scan(pointers...); err != nil {
zap.L().Error("export_query",
zap.String("message", "failed to scan row from query"),
zap.Error(err),
)
fmt.Printf("[!] Error scanning row from query: %v\n", err)
return
}
// Create a map for this row
rowMap := make(map[string]interface{})
for i, col := range cols {
val := values[i]
rowMap[col] = val
}
results = append(results, rowMap)
}
// Export the results
exportQueryResults(results)
return
}
// Prepare data for pretty.Table // Prepare data for pretty.Table
headers := cols headers := cols
var tableRows [][]string var tableRows [][]string
@@ -411,6 +336,47 @@ func rawDBQuery() {
return return
} }
if dbQueryExport {
fmt.Println("[*] Exporting results to file...")
if debugGlobal {
debug.PrintInfo("exporting results to file: " + dbQueryFile)
}
// Prepare data for export
var results []map[string]interface{}
// Process the rows
for rows.Next() {
values := make([]interface{}, len(columns))
pointers := make([]interface{}, len(columns))
for i := range values {
pointers[i] = &values[i]
}
if err := rows.Scan(pointers...); err != nil {
zap.L().Error("export_raw_query",
zap.String("message", "failed to scan row from raw query"),
zap.Error(err),
)
fmt.Printf("[!] Error scanning row from raw query: %v\n", err)
return
}
// Create a map for this row
rowMap := make(map[string]interface{})
for i, col := range columns {
val := values[i]
rowMap[col] = val
}
results = append(results, rowMap)
}
// Export the results
exportQueryResults(results)
return
}
// Prepare data for pretty.Table // Prepare data for pretty.Table
headers := columns headers := columns
var tableRows [][]string var tableRows [][]string
@@ -477,3 +443,22 @@ func rawDBQuery() {
// Display the table // Display the table
pretty.Table(headers, tableRows) pretty.Table(headers, tableRows)
} }
// exportQueryResults exports the results to a file
func exportQueryResults(results []map[string]interface{}) {
// Get file type
fileType := files.GetFileType(dbQueryFormat)
// Export results
err := export.WriteQueryResultsToFile(results, dbQueryFile, fileType)
if err != nil {
zap.L().Error("export_results",
zap.String("message", "failed to write to file"),
zap.Error(err),
)
fmt.Printf("[!] Error writing to file: %v\n", err)
return
}
fmt.Printf("[+] Exported %d records to file: %s%s\n", len(results), dbQueryFile, fileType.Extension())
}
+19 -10
View File
@@ -1,13 +1,14 @@
package cmd package cmd
import ( import (
"crowsnest/internal/badger"
"fmt" "fmt"
"os"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
"os" "hub.krkn.tech/KrakenTech/crowsnest/internal/badger"
"strings"
) )
var ( var (
@@ -16,8 +17,8 @@ var (
// rootCmd is the base command for the CLI. // rootCmd is the base command for the CLI.
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "dehasher", Use: "crowsnest",
Short: `Dehasher is a cli tool for querying the dehashed api.`, Short: `CrowsNest is a cli tool for querying the common OSINT api's.`,
Long: fmt.Sprintf( Long: fmt.Sprintf(
"%s\n", "%s\n",
` `
@@ -53,15 +54,23 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&debugGlobal, "debug", false, "Show debug information") rootCmd.PersistentFlags().BoolVar(&debugGlobal, "debug", false, "Show debug information")
// Add subcommands // Add subcommands
rootCmd.AddCommand(setDehashedKeyCmd) rootCmd.AddCommand(setCmd)
rootCmd.AddCommand(setHunterKeyCmd)
rootCmd.AddCommand(setLocalDb) rootCmd.AddCommand(setLocalDb)
rootCmd.AddCommand(buyMeCoffeeCmd) rootCmd.AddCommand(buyMeCoffeeCmd)
setCmd.AddCommand(setDehashedKeyCmd)
setCmd.AddCommand(setHunterKeyCmd)
}
var setCmd = &cobra.Command{
Use: "set",
Short: "Set CrowsNest configuration values",
Long: "Set CrowsNest configuration values such as API keys.",
} }
// Command to set API key // Command to set API key
var setDehashedKeyCmd = &cobra.Command{ var setDehashedKeyCmd = &cobra.Command{
Use: "set-dehashed [key]", Use: "dehashed [key]",
Short: "Set and store Dehashed.com API key", Short: "Set and store Dehashed.com API key",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -77,7 +86,7 @@ var setDehashedKeyCmd = &cobra.Command{
} }
var setHunterKeyCmd = &cobra.Command{ var setHunterKeyCmd = &cobra.Command{
Use: "set-hunter [key]", Use: "hunter [key]",
Short: "Set and store Hunter.io API key", Short: "Set and store Hunter.io API key",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -94,7 +103,7 @@ var setHunterKeyCmd = &cobra.Command{
var setLocalDb = &cobra.Command{ var setLocalDb = &cobra.Command{
Use: "local-db [true|false]", Use: "local-db [true|false]",
Short: "Set dehasher to use a local database path instead of the default path", Short: "Set crowsnest to use a local database path instead of the default path",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var useLocalDatabase bool var useLocalDatabase bool
+134
View File
@@ -0,0 +1,134 @@
package cmd
import (
"fmt"
"strings"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
)
// Map of available tables and their columns
var availableTables = map[string][]string{
"users": {
"id", "created_at", "updated_at", "deleted_at", "email", "username", "password",
},
//"history": {
// "id", "created_at", "updated_at", "deleted_at", "domain_name", "domain_type",
// "registrar_name", "whois_server", "created_date_iso8601", "updated_date_iso8601", "expires_date_iso8601",
//},
"lookup": {
"id", "created_at", "updated_at", "deleted_at", "search_term", "type", "first_seen", "last_visit",
"name",
},
// Query Options
"runs": {
"id", "created_at", "updated_at", "deleted_at", "max_records", "max_requests", "starting_page",
"output_format", "output_file", "regex_match", "wildcard_match", "username_query", "email_query",
"ip_query", "pass_query", "hash_query", "name_query", "domain_query", "vin_query", "license_plate_query",
"address_query", "phone_query", "social_query", "crypto_address_query", "print_balance", "creds_only",
},
"dehashed": {
"id", "created_at", "updated_at", "deleted_at", "dehashed_id", "email", "ip_address", "username",
"password", "hashed_password", "hash_type", "name", "vin", "license_plate", "url", "social",
"cryptocurrency_address", "address", "phone", "company", "database_name",
},
"subdomains": {
"id", "created_at", "updated_at", "deleted_at", "domain", "subdomain",
},
"whois": {
"id", "created_at", "updated_at", "deleted_at", "audit", "contact_email", "created_date", "created_date_normalized",
"domain_name", "domain_name_ext", "estimated_domain_age", "expires_date", "expires_date_normalized", "footer", "header",
"name_servers", "parse_code", "raw_text", "registrant", "registrar_iana_id", "registrar_name", "registry_data",
"status", "stripped_text", "updated_date", "updated_date_normalized",
},
"hunter_domain": {
"id", "created_at", "updated_at", "deleted_at", "domain", "disposable", "webmail", "accept_all", "pattern",
"organization", "description", "industry", "twitter", "facebook", "linkedin", "instagram", "youtube",
"technologies", "country", "state", "city", "postal_code", "street", "headcount", "company_type", "emails", "linked_domains",
},
"hunter_email": {
"id", "created_at", "updated_at", "deleted_at", "value", "type", "confidence", "sources", "first_name", "last_name",
"position", "position_raw", "seniority", "department", "linkedin", "twitter", "phone_number", "verification_date", "verification_status",
},
}
// Function to list available tables and their columns
func listAvailableTables() {
fmt.Println("Available tables and columns:")
// Prepare data for pretty.Table
headers := []string{"Table", "Columns"}
var tableRows [][]string
// Sort tables alphabetically for consistent output
var tableNames []string
for tableName := range availableTables {
tableNames = append(tableNames, tableName)
}
// Simple bubble sort for table names
for i := 0; i < len(tableNames)-1; i++ {
for j := 0; j < len(tableNames)-i-1; j++ {
if tableNames[j] > tableNames[j+1] {
tableNames[j], tableNames[j+1] = tableNames[j+1], tableNames[j]
}
}
}
// Create rows for the table
for _, tableName := range tableNames {
columns := availableTables[tableName]
// Format columns with line breaks for better readability
var formattedColumns string
for i := 0; i < len(columns); i += 5 {
end := i + 5
if end > len(columns) {
end = len(columns)
}
if i > 0 {
formattedColumns += "\n"
}
formattedColumns += strings.Join(columns[i:end], ", ")
}
tableRows = append(tableRows, []string{tableName, formattedColumns})
}
// Display the table
pretty.Table(headers, tableRows)
}
// Function to validate table name
func isValidTable(tableName string) bool {
_, exists := availableTables[tableName]
return exists
}
// Function to validate column names for a specific table
func validateColumns(tableName string, columns []string) []string {
if tableName == "" || columns == nil || len(columns) == 0 || columns[0] == "*" {
return nil
}
tableColumns, exists := availableTables[tableName]
if !exists {
return []string{fmt.Sprintf("Table '%s' does not exist", tableName)}
}
var invalidColumns []string
for _, col := range columns {
valid := false
for _, tableCol := range tableColumns {
if col == tableCol {
valid = true
break
}
}
if !valid {
invalidColumns = append(invalidColumns, col)
}
}
return invalidColumns
}
+313
View File
@@ -0,0 +1,313 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
func init() {
// Add targets command to root command
rootCmd.AddCommand(targetsCmd)
// Add flags specific to targets command
targetsCmd.Flags().StringVarP(&targetsOutputFile, "output", "o", "targets", "Output file name (required)")
targetsCmd.Flags().BoolVarP(&targetsExternal, "external", "e", false, "Output external format (email:password)")
targetsCmd.Flags().BoolVarP(&targetsInternal, "internal", "i", false, "Output internal format (username:password)")
targetsCmd.Flags().BoolVarP(&targetsSubdomains, "subdomains", "s", false, "Output subdomains")
targetsCmd.Flags().BoolVarP(&targetsEmails, "emails", "E", false, "Output emails only (no passwords)")
targetsCmd.Flags().StringVarP(&targetsDomain, "domain", "d", "", "Filter by domain (for emails and subdomains)")
// Mark output flag as required
targetsCmd.MarkFlagsMutuallyExclusive("external", "internal", "subdomains", "emails")
}
var (
// Targets command flags
targetsOutputFile string
targetsExternal bool
targetsInternal bool
targetsSubdomains bool
targetsEmails bool
targetsDomain string
// Targets command
targetsCmd = &cobra.Command{
Use: "targets",
Short: "Export users and subdomains in formats suitable for external tools",
Long: `Export users and subdomains from the database in easily digestible formats for tools like sprays or other security testing tools.
Formats:
--external (-e): Output in email:password format
--internal (-i): Output in username:password format
--emails (-E): Output emails only (no passwords)
--subdomains (-s): Output subdomains only
Options:
--domain (-d): Filter results by domain (applies to emails and subdomains)
--output (-o): Specify output file name (required)
Examples:
# Export all external credentials (email:password)
crowsnest targets -e -o external_creds
# Export internal credentials for a specific domain
crowsnest targets -i -d example.com -o internal_creds
# Export all emails
crowsnest targets -E -o all_emails
# Export emails for a specific domain
crowsnest targets -E -d example.com -o domain_emails
# Export subdomains for a specific domain
crowsnest targets -s -d example.com -o subdomains
# Export all subdomains
crowsnest targets -s -o all_subdomains`,
Run: func(cmd *cobra.Command, args []string) {
// Validate that at least one format is specified
if !targetsExternal && !targetsInternal && !targetsSubdomains && !targetsEmails {
fmt.Println("[!] Error: You must specify at least one output format:")
fmt.Println(" --external (-e) for email:password format")
fmt.Println(" --internal (-i) for username:password format")
fmt.Println(" --emails (-E) for emails only")
fmt.Println(" --subdomains (-s) for subdomains")
return
}
if debugGlobal {
zap.L().Info("targets_debug",
zap.String("message", "targets command started"),
zap.Bool("external", targetsExternal),
zap.Bool("internal", targetsInternal),
zap.Bool("subdomains", targetsSubdomains),
zap.Bool("emails", targetsEmails),
zap.String("domain", targetsDomain),
zap.String("output_file", targetsOutputFile),
)
}
// Execute the targets export
err := executeTargetsExport()
if err != nil {
fmt.Printf("[!] Error: %v\n", err)
return
}
fmt.Printf("[+] Successfully exported targets to: %s\n", targetsOutputFile)
},
}
)
// executeTargetsExport performs the main logic for exporting targets
func executeTargetsExport() error {
var outputLines []string
// Export external credentials (email:password)
if targetsExternal {
if debugGlobal {
fmt.Println("[*] Exporting external credentials (email:password)...")
}
externalCreds, err := getExternalCredentials()
if err != nil {
return fmt.Errorf("failed to get external credentials: %v", err)
}
for _, cred := range externalCreds {
if cred.Email != "" && cred.Password != "" {
outputLines = append(outputLines, fmt.Sprintf("%s:%s", cred.Email, cred.Password))
}
}
if debugGlobal {
fmt.Printf("[*] Found %d external credentials\n", len(externalCreds))
}
}
// Export internal credentials (username:password)
if targetsInternal {
if debugGlobal {
fmt.Println("[*] Exporting internal credentials (username:password)...")
}
internalCreds, err := getInternalCredentials()
if err != nil {
return fmt.Errorf("failed to get internal credentials: %v", err)
}
for _, cred := range internalCreds {
if cred.Username != "" && cred.Password != "" {
outputLines = append(outputLines, fmt.Sprintf("%s:%s", cred.Username, cred.Password))
}
}
if debugGlobal {
fmt.Printf("[*] Found %d internal credentials\n", len(internalCreds))
}
}
// Export emails only
if targetsEmails {
if debugGlobal {
fmt.Println("[*] Exporting emails only...")
}
emails, err := getEmailsOnly()
if err != nil {
return fmt.Errorf("failed to get emails: %v", err)
}
for _, email := range emails {
if email.Email != "" {
outputLines = append(outputLines, email.Email)
}
}
if debugGlobal {
fmt.Printf("[*] Found %d emails\n", len(emails))
}
}
// Export subdomains
if targetsSubdomains {
if debugGlobal {
fmt.Println("[*] Exporting subdomains...")
}
subdomains, err := getSubdomains()
if err != nil {
return fmt.Errorf("failed to get subdomains: %v", err)
}
for _, subdomain := range subdomains {
if subdomain.Subdomain != "" {
outputLines = append(outputLines, subdomain.Subdomain)
}
}
if debugGlobal {
fmt.Printf("[*] Found %d subdomains\n", len(subdomains))
}
}
// Write to file
if len(outputLines) == 0 {
return fmt.Errorf("no data found to export")
}
// Join all lines with newlines and add a single newline at the end
content := strings.Join(outputLines, "\n") + "\n"
err := os.WriteFile(targetsOutputFile, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write to file: %v", err)
}
if debugGlobal {
fmt.Printf("[*] Wrote %d lines to %s\n", len(outputLines), targetsOutputFile)
}
return nil
}
// getExternalCredentials retrieves credentials for external format (email:password)
func getExternalCredentials() ([]sqlite.User, error) {
db := sqlite.GetDB()
var users []sqlite.User
query := db.Where("email IS NOT NULL AND email != '' AND password IS NOT NULL AND password != ''")
// Apply domain filter if specified
if targetsDomain != "" {
query = query.Where("email LIKE ?", "%@"+targetsDomain)
}
err := query.Find(&users).Error
if err != nil {
zap.L().Error("get_external_credentials",
zap.String("message", "failed to query external credentials"),
zap.Error(err),
)
return nil, err
}
return users, nil
}
// getInternalCredentials retrieves credentials for internal format (username:password)
func getInternalCredentials() ([]sqlite.User, error) {
db := sqlite.GetDB()
var users []sqlite.User
query := db.Where("username IS NOT NULL AND username != '' AND password IS NOT NULL AND password != ''")
// Apply domain filter if specified (filter usernames that might contain domain info)
if targetsDomain != "" {
query = query.Where("username LIKE ? OR email LIKE ?", "%"+targetsDomain+"%", "%@"+targetsDomain)
}
err := query.Find(&users).Error
if err != nil {
zap.L().Error("get_internal_credentials",
zap.String("message", "failed to query internal credentials"),
zap.Error(err),
)
return nil, err
}
return users, nil
}
// getEmailsOnly retrieves emails only (no passwords required)
func getEmailsOnly() ([]sqlite.User, error) {
db := sqlite.GetDB()
var users []sqlite.User
query := db.Where("email IS NOT NULL AND email != ''")
// Apply domain filter if specified
if targetsDomain != "" {
query = query.Where("email LIKE ?", "%@"+targetsDomain)
}
err := query.Find(&users).Error
if err != nil {
zap.L().Error("get_emails_only",
zap.String("message", "failed to query emails"),
zap.Error(err),
)
return nil, err
}
return users, nil
}
// getSubdomains retrieves subdomains from the database
func getSubdomains() ([]sqlite.Subdomain, error) {
db := sqlite.GetDB()
var subdomains []sqlite.Subdomain
query := db.Where("subdomain IS NOT NULL AND subdomain != ''")
// Apply domain filter if specified
if targetsDomain != "" {
query = query.Where("domain = ? OR subdomain LIKE ?", targetsDomain, "%."+targetsDomain)
}
err := query.Find(&subdomains).Error
if err != nil {
zap.L().Error("get_subdomains",
zap.String("message", "failed to query subdomains"),
zap.Error(err),
)
return nil, err
}
return subdomains, nil
}
+21 -13
View File
@@ -1,18 +1,19 @@
package cmd package cmd
import ( import (
"crowsnest/internal/debug"
"crowsnest/internal/export"
"crowsnest/internal/files"
"crowsnest/internal/pretty"
"crowsnest/internal/sqlite"
"crowsnest/internal/whois"
"fmt" "fmt"
"github.com/spf13/cobra"
"go.uber.org/zap"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/spf13/cobra"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/export"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
"hub.krkn.tech/KrakenTech/crowsnest/internal/whois"
) )
func init() { func init() {
@@ -59,7 +60,7 @@ var (
// Validate credentials // Validate credentials
if key == "" { if key == "" {
fmt.Println("API key is required. Set the key with the \"set-key\" command. [crowsnest set-key <api_key>]") fmt.Println("API key is required. Set the key with the \"set dehashed\" command. [crowsnest set dehashed <api_key>]")
return return
} }
@@ -195,8 +196,8 @@ var (
// Write history records to file if any // Write history records to file if any
if len(historyRecords) > 0 { if len(historyRecords) > 0 {
fmt.Println("[*] Records Found: %d\n", len(historyRecords)) fmt.Printf("[*] Records Found: %d\n", len(historyRecords))
fmt.Println("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension()) fmt.Printf("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension())
writeErr := export.WriteWhoIsHistoryToFile(historyRecords, filename, fType) writeErr := export.WriteWhoIsHistoryToFile(historyRecords, filename, fType)
if writeErr != nil { if writeErr != nil {
if debugGlobal { if debugGlobal {
@@ -240,6 +241,7 @@ var (
fmt.Println("[*] Performing WHOIS subdomain scan...") fmt.Println("[*] Performing WHOIS subdomain scan...")
subdomains, err := w.WhoisSubdomainScan(whoisDomain) subdomains, err := w.WhoisSubdomainScan(whoisDomain)
// Get credits
if whoisShowCredits { if whoisShowCredits {
checkBalance(w) checkBalance(w)
} }
@@ -255,7 +257,13 @@ var (
) )
fmt.Printf("Error performing subdomain scan: %v\n", err) fmt.Printf("Error performing subdomain scan: %v\n", err)
} else { } else {
err = sqlite.StoreWhoisSubdomainRecords(subdomains) // Store subdomains in subdomains table
var subs []sqlite.Subdomain
for _, s := range subdomains {
subs = append(subs, sqlite.Subdomain{Domain: whoisDomain, Subdomain: s.Domain})
}
err = sqlite.StoreSubdomains(subs)
if err != nil { if err != nil {
if debugGlobal { if debugGlobal {
debug.PrintInfo("failed to store subdomain record") debug.PrintInfo("failed to store subdomain record")
@@ -265,7 +273,7 @@ var (
zap.String("message", "failed to store subdomain record"), zap.String("message", "failed to store subdomain record"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error storing WHOIS subdomain record: %v\n", err) fmt.Printf("Error storing subdomain record: %v\n", err)
} }
// Write the subdomains to file if any // Write the subdomains to file if any
+7 -6
View File
@@ -1,16 +1,17 @@
package main package main
import ( import (
"crowsnest/cmd"
"crowsnest/internal/badger"
"crowsnest/internal/sqlite"
"fmt" "fmt"
"os"
"path/filepath"
"github.com/winking324/rzap" "github.com/winking324/rzap"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
"os" "hub.krkn.tech/KrakenTech/crowsnest/cmd"
"path/filepath" "hub.krkn.tech/KrakenTech/crowsnest/internal/badger"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
var ( var (
@@ -82,7 +83,7 @@ func main() {
useLocalDB := badger.GetUseLocalDB() useLocalDB := badger.GetUseLocalDB()
if useLocalDB { if useLocalDB {
// Use local database in current directory // Use local database in current directory
dbPath = "./crowsnest.sqlite" dbPath = "./"
zap.L().Info("Using local database", zap.String("path", dbPath)) zap.L().Info("Using local database", zap.String("path", dbPath))
} else { } else {
// Use default database path // Use default database path
+1 -3
View File
@@ -1,4 +1,4 @@
module crowsnest module hub.krkn.tech/KrakenTech/crowsnest
go 1.23.0 go 1.23.0
@@ -14,7 +14,6 @@ require (
go.uber.org/zap v1.20.0 go.uber.org/zap v1.20.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.26.1 gorm.io/gorm v1.26.1
) )
@@ -41,7 +40,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
-4
View File
@@ -75,8 +75,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -171,8 +169,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
+233 -10
View File
@@ -3,15 +3,19 @@ package badger
import ( import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"github.com/dgraph-io/badger/v4" "fmt"
"go.uber.org/zap"
"log" "log"
"os" "os"
"os/exec"
"os/user" "os/user"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"time"
"github.com/dgraph-io/badger/v4"
"go.uber.org/zap"
) )
var ( var (
@@ -21,13 +25,35 @@ var (
once sync.Once once sync.Once
) )
func GetHardwareEntropy() []byte { const fingerprintSalt = "CrowsNest-static-salt-value"
func GetHardwareEntropy() ([]byte, error) {
source, machineID, err := getMachineID()
if err != nil {
return nil, err
}
fingerprint := strings.Join([]string{
"v2",
runtime.GOOS,
source,
machineID,
fingerprintSalt,
}, ":")
return hashFingerprint(fingerprint), nil
}
func GetLegacyHardwareEntropy() []byte {
// Get hostname // Get hostname
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err != nil { if err != nil {
hostname = "unknown-host" hostname = "unknown-host"
log.Printf("Error getting hostname: %v", err) log.Printf("Error getting hostname: %v", err)
} }
if legacyHostname := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_HOSTNAME")); legacyHostname != "" {
hostname = legacyHostname
}
// Get username // Get username
currentUser, err := user.Current() currentUser, err := user.Current()
@@ -35,9 +61,15 @@ func GetHardwareEntropy() []byte {
if err == nil && currentUser != nil { if err == nil && currentUser != nil {
username = currentUser.Username username = currentUser.Username
} }
if legacyUsername := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_USERNAME")); legacyUsername != "" {
username = legacyUsername
}
// Get OS and architecture info // Get OS and architecture info
osInfo := runtime.GOOS + "-" + runtime.GOARCH osInfo := runtime.GOOS + "-" + runtime.GOARCH
if legacyOSInfo := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_OSINFO")); legacyOSInfo != "" {
osInfo = legacyOSInfo
}
// Combine all information for a unique but consistent fingerprint // Combine all information for a unique but consistent fingerprint
fingerprint := strings.Join([]string{ fingerprint := strings.Join([]string{
@@ -45,14 +77,93 @@ func GetHardwareEntropy() []byte {
username, username,
osInfo, osInfo,
// You could add a static salt here for additional security // You could add a static salt here for additional security
"CrowsNest-static-salt-value", fingerprintSalt,
}, ":") }, ":")
// Hash the fingerprint to get a 32-byte key // Hash the fingerprint to get a 32-byte key
return hashFingerprint(fingerprint)
}
func hashFingerprint(fingerprint string) []byte {
sum := sha256.Sum256([]byte(fingerprint)) sum := sha256.Sum256([]byte(fingerprint))
return sum[:] return sum[:]
} }
func getMachineID() (string, string, error) {
switch runtime.GOOS {
case "darwin":
return getDarwinMachineID()
case "linux":
return getLinuxMachineID()
case "windows":
return getWindowsMachineID()
default:
return "", "", fmt.Errorf("stable machine id is not implemented for %s", runtime.GOOS)
}
}
func getDarwinMachineID() (string, string, error) {
out, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
if err != nil {
return "", "", fmt.Errorf("run ioreg: %w", err)
}
for _, line := range strings.Split(string(out), "\n") {
if !strings.Contains(line, "IOPlatformUUID") {
continue
}
if id := normalizeMachineID(lastQuotedValue(line)); id != "" {
return "darwin-ioplatformuuid", id, nil
}
}
return "", "", errors.New("IOPlatformUUID not found")
}
func getLinuxMachineID() (string, string, error) {
for _, path := range []string{"/etc/machine-id", "/var/lib/dbus/machine-id"} {
out, err := os.ReadFile(path)
if err != nil {
continue
}
if id := normalizeMachineID(string(out)); id != "" {
return "linux-machine-id", id, nil
}
}
return "", "", errors.New("machine-id not found")
}
func getWindowsMachineID() (string, string, error) {
out, err := exec.Command("reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid").Output()
if err != nil {
return "", "", fmt.Errorf("query MachineGuid: %w", err)
}
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) >= 3 && strings.EqualFold(fields[0], "MachineGuid") {
if id := normalizeMachineID(fields[len(fields)-1]); id != "" {
return "windows-machineguid", id, nil
}
}
}
return "", "", errors.New("MachineGuid not found")
}
func lastQuotedValue(line string) string {
values := strings.Split(line, "\"")
if len(values) < 4 {
return ""
}
return values[len(values)-2]
}
func normalizeMachineID(id string) string {
return strings.ToLower(strings.TrimSpace(id))
}
func Start(dirPath string) *badger.DB { func Start(dirPath string) *badger.DB {
var err error var err error
@@ -65,7 +176,7 @@ func Start(dirPath string) *badger.DB {
} }
rootDir = dirPath rootDir = dirPath
encryptionKey = GetHardwareEntropy() encryptionKey, err = GetHardwareEntropy()
if err != nil { if err != nil {
zap.L().Fatal("get_encryption_key", zap.L().Fatal("get_encryption_key",
zap.String("message", "failed to get encryption key"), zap.String("message", "failed to get encryption key"),
@@ -74,22 +185,134 @@ func Start(dirPath string) *badger.DB {
} }
badgerDB := filepath.Join(rootDir, "badger.db") badgerDB := filepath.Join(rootDir, "badger.db")
opts := badger.DefaultOptions(badgerDB). db, err = openBadger(badgerDB, encryptionKey)
WithEncryptionKey(encryptionKey). if err != nil {
WithIndexCacheSize(10 << 20). // 10MB zap.L().Warn("open_badger_db",
WithLoggingLevel(badger.ERROR) zap.String("message", "failed to open badger database with stable machine key; trying legacy key"),
db, err = badger.Open(opts) zap.Error(err),
)
db, err = openBadgerWithLegacyMigration(badgerDB, encryptionKey)
if err != nil { if err != nil {
zap.L().Fatal("new_badger_db", zap.L().Fatal("new_badger_db",
zap.String("message", "failed to open badger database"), zap.String("message", "failed to open badger database"),
zap.Error(err), zap.Error(err),
) )
} }
}
}) })
return db return db
} }
func openBadger(dbPath string, key []byte) (*badger.DB, error) {
opts := badger.DefaultOptions(dbPath).
WithEncryptionKey(key).
WithIndexCacheSize(10 << 20). // 10MB
WithLoggingLevel(badger.ERROR)
return badger.Open(opts)
}
func openBadgerWithLegacyMigration(dbPath string, stableKey []byte) (*badger.DB, error) {
legacyKey := GetLegacyHardwareEntropy()
legacyDB, err := openBadger(dbPath, legacyKey)
if err != nil {
return nil, fmt.Errorf("stable key failed and legacy key failed: %w", err)
}
migratedDB, err := migrateBadgerEncryption(dbPath, legacyDB, stableKey)
if err != nil {
if closeErr := legacyDB.Close(); closeErr != nil {
zap.L().Error("close_legacy_badger_db", zap.Error(closeErr))
}
return nil, err
}
return migratedDB, nil
}
func migrateBadgerEncryption(dbPath string, legacyDB *badger.DB, stableKey []byte) (*badger.DB, error) {
parentDir := filepath.Dir(dbPath)
timestamp := time.Now().Format("20060102-150405")
migrationPath := filepath.Join(parentDir, fmt.Sprintf(".%s.migrating-%s", filepath.Base(dbPath), timestamp))
backupPath := filepath.Join(parentDir, fmt.Sprintf("%s.legacy-backup-%s", filepath.Base(dbPath), timestamp))
newDB, err := openBadger(migrationPath, stableKey)
if err != nil {
return nil, fmt.Errorf("open migration badger db: %w", err)
}
if err := copyBadgerData(legacyDB, newDB); err != nil {
_ = newDB.Close()
_ = os.RemoveAll(migrationPath)
return nil, fmt.Errorf("copy legacy badger data: %w", err)
}
if err := legacyDB.Close(); err != nil {
_ = newDB.Close()
_ = os.RemoveAll(migrationPath)
return nil, fmt.Errorf("close legacy badger db: %w", err)
}
if err := newDB.Close(); err != nil {
_ = os.RemoveAll(migrationPath)
return nil, fmt.Errorf("close migration badger db: %w", err)
}
if err := os.Rename(dbPath, backupPath); err != nil {
_ = os.RemoveAll(migrationPath)
return nil, fmt.Errorf("backup legacy badger db: %w", err)
}
if err := os.Rename(migrationPath, dbPath); err != nil {
if restoreErr := os.Rename(backupPath, dbPath); restoreErr != nil {
return nil, fmt.Errorf("promote migrated badger db: %w; restore legacy backup: %v", err, restoreErr)
}
return nil, fmt.Errorf("promote migrated badger db: %w", err)
}
db, err := openBadger(dbPath, stableKey)
if err != nil {
return nil, fmt.Errorf("open migrated badger db: %w", err)
}
zap.L().Info("migrated_badger_encryption",
zap.String("backup", backupPath),
zap.String("path", dbPath),
)
return db, nil
}
func copyBadgerData(src *badger.DB, dst *badger.DB) error {
return src.View(func(srcTxn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = true
iter := srcTxn.NewIterator(opts)
defer iter.Close()
return dst.Update(func(dstTxn *badger.Txn) error {
for iter.Rewind(); iter.Valid(); iter.Next() {
item := iter.Item()
if item.IsDeletedOrExpired() {
continue
}
key := item.KeyCopy(nil)
value, err := item.ValueCopy(nil)
if err != nil {
return err
}
entry := badger.NewEntry(key, value).WithMeta(item.UserMeta())
entry.ExpiresAt = item.ExpiresAt()
if err := dstTxn.SetEntry(entry); err != nil {
return err
}
}
return nil
})
})
}
func Close() { func Close() {
err := db.Close() err := db.Close()
if err != nil { if err != nil {
+73
View File
@@ -0,0 +1,73 @@
package badger
import (
"crypto/sha256"
"os"
"path/filepath"
"strings"
"testing"
badgerapi "github.com/dgraph-io/badger/v4"
)
func TestMigrateBadgerEncryptionCopiesDataToStableKey(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "badger.db")
legacyKey := testKey("legacy-key")
stableKey := testKey("stable-key")
legacyDB, err := openBadger(dbPath, legacyKey)
if err != nil {
t.Fatalf("open legacy db: %v", err)
}
if err := legacyDB.Update(func(txn *badgerapi.Txn) error {
return txn.Set([]byte("cfg:api_key"), []byte("secret"))
}); err != nil {
t.Fatalf("seed legacy db: %v", err)
}
migratedDB, err := migrateBadgerEncryption(dbPath, legacyDB, stableKey)
if err != nil {
t.Fatalf("migrate db: %v", err)
}
defer migratedDB.Close()
var got string
if err := migratedDB.View(func(txn *badgerapi.Txn) error {
item, err := txn.Get([]byte("cfg:api_key"))
if err != nil {
return err
}
return item.Value(func(value []byte) error {
got = string(value)
return nil
})
}); err != nil {
t.Fatalf("read migrated db: %v", err)
}
if got != "secret" {
t.Fatalf("migrated value = %q, want %q", got, "secret")
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read temp dir: %v", err)
}
foundBackup := false
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), "badger.db.legacy-backup-") {
foundBackup = true
break
}
}
if !foundBackup {
t.Fatal("legacy backup directory was not created")
}
}
func testKey(value string) []byte {
sum := sha256.Sum256([]byte(value))
return sum[:]
}
+14 -5
View File
@@ -2,17 +2,18 @@ package dehashed
import ( import (
"bytes" "bytes"
"crowsnest/internal/debug"
"crowsnest/internal/sqlite"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"go.uber.org/zap"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
type DehashedParameter string type DehashedParameter string
@@ -198,7 +199,7 @@ func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int,
req.Header.Set("Dehashed-Api-Key", dcv2.apiKey) req.Header.Set("Dehashed-Api-Key", dcv2.apiKey)
if dcv2.debug { if dcv2.debug {
headers := req.Header.Clone() headers := redactedHeaders(req.Header)
h := fmt.Sprintf("Headers: %v\n", headers) h := fmt.Sprintf("Headers: %v\n", headers)
debug.PrintJson(h) debug.PrintJson(h)
zap.L().Info("v2_search_debug", zap.L().Info("v2_search_debug",
@@ -286,7 +287,7 @@ func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int,
} }
dcv2.results = append(dcv2.results, responseResults.Entries...) dcv2.results = append(dcv2.results, responseResults.Entries...)
return responseResults.TotalResults, responseResults.Balance, nil return len(responseResults.Entries), responseResults.Balance, nil
} }
func (dcv2 *DehashedClientV2) GetResults() sqlite.DehashedResults { func (dcv2 *DehashedClientV2) GetResults() sqlite.DehashedResults {
@@ -304,3 +305,11 @@ func enquoteSpaced(s string) string {
} }
return s return s
} }
func redactedHeaders(headers http.Header) http.Header {
redacted := headers.Clone()
if redacted.Get("Dehashed-Api-Key") != "" {
redacted.Set("Dehashed-Api-Key", "[REDACTED]")
}
return redacted
}
+186
View File
@@ -0,0 +1,186 @@
package dehashed
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"gopkg.in/yaml.v3"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
)
const dataWellsEndpoint = "https://api.dehashed.com/data-wells"
type DataWellsRequest struct {
Count int
Page int
Sort string
}
type DataWellsResponse struct {
NextPage bool `json:"next_page" xml:"next_page" yaml:"next_page"`
Total int `json:"total" xml:"total" yaml:"total"`
DataWells []DataWell `json:"data_wells" xml:"data_wells" yaml:"data_wells"`
}
type DataWell struct {
Data string `json:"data" xml:"data" yaml:"data"`
Date string `json:"date" xml:"date" yaml:"date"`
Description string `json:"description" xml:"description" yaml:"description"`
Name string `json:"name" xml:"name" yaml:"name"`
Records int `json:"records" xml:"records" yaml:"records"`
IsSensitive bool `json:"is_sensitive" xml:"is_sensitive" yaml:"is_sensitive"`
}
func (dcv2 *DehashedClientV2) DataWells(request DataWellsRequest) (DataWellsResponse, error) {
var dataWells DataWellsResponse
endpoint, err := dataWellsURL(request)
if err != nil {
return dataWells, err
}
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return dataWells, err
}
req.Header.Set("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return dataWells, err
}
if res == nil {
return dataWells, errors.New("response was nil")
}
body, err := io.ReadAll(res.Body)
if err != nil {
return dataWells, err
}
if res.StatusCode != http.StatusOK {
return dataWells, fmt.Errorf("data wells request failed: status=%d body=%s", res.StatusCode, string(body))
}
if err := json.Unmarshal(body, &dataWells); err != nil {
return dataWells, err
}
return dataWells, nil
}
func dataWellsURL(request DataWellsRequest) (string, error) {
if request.Page <= 0 {
return "", errors.New("page must be 1 or greater")
}
if request.Count != 20 && request.Count != 50 {
return "", errors.New("count must be 20 or 50")
}
if request.Sort != "" && !validDataWellsSort(request.Sort) {
return "", fmt.Errorf("invalid sort %q; use added, name, date, records, optionally suffixed with -ASC or -DESC", request.Sort)
}
values := url.Values{}
values.Set("page", strconv.Itoa(request.Page))
values.Set("count", strconv.Itoa(request.Count))
if request.Sort != "" {
values.Set("sort", request.Sort)
}
return dataWellsEndpoint + "?" + values.Encode(), nil
}
func validDataWellsSort(sortValue string) bool {
sortValue = strings.ToLower(strings.TrimSpace(sortValue))
field := sortValue
if before, _, ok := strings.Cut(sortValue, "-"); ok {
field = before
}
switch field {
case "added", "name", "date", "records":
return strings.HasSuffix(sortValue, "-asc") || strings.HasSuffix(sortValue, "-desc") || !strings.Contains(sortValue, "-")
default:
return false
}
}
func WriteDataWellsToFile(dataWells DataWellsResponse, outputFile string, fileType files.FileType) error {
var data []byte
var err error
switch fileType {
case files.JSON:
data, err = json.MarshalIndent(dataWells, "", " ")
case files.XML:
data, err = xml.MarshalIndent(dataWells, "", " ")
case files.YAML:
data, err = yaml.Marshal(dataWells)
case files.TEXT:
data = []byte(dataWells.String())
case files.GREPPABLE:
var outStrings []string
for _, well := range dataWells.DataWells {
outStrings = append(outStrings, dataWellGreppable(well)+"\n")
}
data = []byte(strings.Join(outStrings, ""))
default:
return errors.New("unsupported file type")
}
if err != nil {
return err
}
return os.WriteFile(outputFile+fileType.Extension(), data, 0644)
}
func (dwr DataWellsResponse) String() string {
var b strings.Builder
fmt.Fprintf(&b, "Total: %d\nNext Page: %t\n\n", dwr.Total, dwr.NextPage)
for _, well := range dwr.DataWells {
fmt.Fprintf(&b, "Name: %s\nDate: %s\nRecords: %d\nSensitive: %t\nData: %s\nDescription: %s\n\n",
well.Name,
well.Date,
well.Records,
well.IsSensitive,
well.Data,
well.Description,
)
}
return b.String()
}
func dataWellGreppable(well DataWell) string {
var fields []string
fields = appendDataWellGreppableField(fields, "name", well.Name)
fields = appendDataWellGreppableField(fields, "date", well.Date)
fields = appendDataWellGreppableField(fields, "records", strconv.Itoa(well.Records))
fields = appendDataWellGreppableField(fields, "is_sensitive", strconv.FormatBool(well.IsSensitive))
fields = appendDataWellGreppableField(fields, "data", well.Data)
fields = appendDataWellGreppableField(fields, "description", well.Description)
return strings.Join(fields, " ")
}
func cleanGreppableValue(value string) string {
return strings.Join(strings.Fields(value), "_")
}
func appendDataWellGreppableField(fields []string, key, value string) []string {
value = cleanGreppableValue(value)
if value == "" {
return fields
}
return append(fields, fmt.Sprintf("%s=%s", key, value))
}
+62 -52
View File
@@ -1,14 +1,20 @@
package dehashed package dehashed
import ( import (
"crowsnest/internal/debug"
"crowsnest/internal/export"
"crowsnest/internal/pretty"
"crowsnest/internal/sqlite"
"fmt" "fmt"
"go.uber.org/zap"
"os" "os"
"strings" "strings"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/export"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
const (
maxSearchResultsPerPage = 10000
maxSearchResultsPerQuery = 50000
) )
// Dehasher is a struct for querying the Dehashed API // Dehasher is a struct for querying the Dehashed API
@@ -17,6 +23,7 @@ type Dehasher struct {
nextPage int nextPage int
debug bool debug bool
balance int balance int
maxResults int
request *DehashedSearchRequest request *DehashedSearchRequest
client *DehashedClientV2 client *DehashedClientV2
} }
@@ -51,55 +58,55 @@ func (dh *Dehasher) getNextPage() int {
// setQueries sets the number of queries to make based on the number of records and requests // setQueries sets the number of queries to make based on the number of records and requests
func (dh *Dehasher) setQueries() { func (dh *Dehasher) setQueries() {
var numQueries int
if dh.debug { if dh.debug {
debug.PrintInfo("setting queries") debug.PrintInfo("setting queries")
} }
switch { if dh.options.MaxRequests == 0 {
case dh.options.MaxRequests == 0:
zap.L().Error("max requests cannot be zero") zap.L().Error("max requests cannot be zero")
fmt.Println("[!] Max Requests cannot be zero") fmt.Println("[!] Max Requests cannot be zero")
os.Exit(1) os.Exit(1)
case dh.options.MaxRecords <= 10000 || dh.options.MaxRequests == 1:
numQueries = 1
if dh.options.MaxRecords > 10000 {
dh.options.MaxRecords = 10000
}
zap.L().Info("max requests set to 1", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests < 0 && dh.options.MaxRecords > 20000:
numQueries = 3
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 3", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests < 0 && dh.options.MaxRecords > 10000:
numQueries = 2
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 2", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRecords < 0 && dh.options.MaxRecords < 10000:
numQueries = 1
zap.L().Info("max requests set to 1", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests == 2 && dh.options.MaxRecords > 20000:
numQueries = 2
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 2", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests == 2 && dh.options.MaxRecords <= 10000:
numQueries = 1
zap.L().Info("max requests set to 1", zap.Int("max_records", dh.options.MaxRecords))
default:
numQueries = 3
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 3", zap.Int("max_records", dh.options.MaxRecords))
} }
requestedMaxResults := dh.options.MaxRecords
if requestedMaxResults <= 0 {
requestedMaxResults = maxSearchResultsPerQuery
}
if requestedMaxResults > maxSearchResultsPerQuery {
requestedMaxResults = maxSearchResultsPerQuery
}
pageSize := requestedMaxResults
if pageSize > maxSearchResultsPerPage {
pageSize = maxSearchResultsPerPage
}
numQueries := (requestedMaxResults + pageSize - 1) / pageSize
if dh.options.MaxRequests > 0 && dh.options.MaxRequests < numQueries {
numQueries = dh.options.MaxRequests
}
dh.maxResults = requestedMaxResults
if requestLimit := numQueries * pageSize; requestLimit < dh.maxResults {
dh.maxResults = requestLimit
}
dh.options.MaxRecords = pageSize
dh.options.MaxRequests = numQueries dh.options.MaxRequests = numQueries
zap.L().Info("dehashed_search_pagination",
zap.Int("max_results", dh.maxResults),
zap.Int("page_size", dh.options.MaxRecords),
zap.Int("max_requests", dh.options.MaxRequests),
)
if dh.debug { if dh.debug {
debug.PrintInfo(fmt.Sprintf("setting max requests: %d", numQueries)) debug.PrintInfo(fmt.Sprintf("setting max requests: %d", numQueries))
debug.PrintInfo(fmt.Sprintf("setting max records: %d", dh.options.MaxRecords)) debug.PrintInfo(fmt.Sprintf("setting page size: %d", dh.options.MaxRecords))
debug.PrintInfo(fmt.Sprintf("setting max results: %d", dh.maxResults))
} }
fmt.Printf("Making %d Requests for %d Records (%d Total)\n", dh.options.MaxRequests, dh.options.MaxRecords, dh.options.MaxRequests*dh.options.MaxRecords) fmt.Printf("Making %d Requests for up to %d Records (%d per request)\n", dh.options.MaxRequests, dh.maxResults, dh.options.MaxRecords)
} }
// Start starts the querying process // Start starts the querying process
@@ -151,7 +158,7 @@ func (dh *Dehasher) Start() {
fmt.Printf(" [-] Not enough entries, ending queries\n") fmt.Printf(" [-] Not enough entries, ending queries\n")
break break
} else { } else {
fmt.Printf(" [+] Retrieved %d records\n", dh.options.MaxRecords) fmt.Printf(" [+] Retrieved %d records\n", count)
} }
if dh.options.PrintBalance { if dh.options.PrintBalance {
@@ -211,9 +218,12 @@ func (dh *Dehasher) buildRequest() {
func (dh *Dehasher) parseResults() { func (dh *Dehasher) parseResults() {
zap.L().Info("extracting_credentials") zap.L().Info("extracting_credentials")
results := dh.client.GetResults() results := dh.client.GetResults()
creds := results.ExtractCredentials() if dh.maxResults > 0 && len(results.Results) > dh.maxResults {
results.Results = results.Results[:dh.maxResults]
}
creds := results.ExtractUsers()
fmt.Printf(" [+] Discovered %d Credentials\n", len(creds)) fmt.Printf(" [+] Discovered %d Credentials\n", len(creds))
err := sqlite.StoreDehashedCreds(creds) err := sqlite.StoreUsers(creds)
if err != nil { if err != nil {
zap.L().Error("store_creds", zap.L().Error("store_creds",
zap.String("message", "failed to store creds"), zap.String("message", "failed to store creds"),
@@ -255,25 +265,25 @@ func (dh *Dehasher) parseResults() {
debug.PrintInfo("printing results table") debug.PrintInfo("printing results table")
} }
headers = []string{"Name", "Email", "Username", "Password", "Address", "Phone", "Social", "Crypto Address", "Company"} headers = []string{"Email", "Username", "Password", "Phone", "Company"}
if len(results.Results) > 50 { if len(results.Results) > 50 {
fmt.Println(" [-] Large number of results recovered, displaying first 50...") fmt.Println(" [-] Large number of results recovered, displaying first 50...")
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
r := results.Results[i] r := results.Results[i]
rows = append(rows, []string{ rows = append(rows, []string{
strings.Join(r.Name, ", "), strings.Join(r.Email, ", "), strings.Join(r.Email, ", "),
strings.Join(r.Username, ", "), strings.Join(r.Password, ", "), strings.Join(r.Username, ", "),
strings.Join(r.Address, ", "), strings.Join(r.Phone, ", "), strings.Join(r.Password, ", "),
strings.Join(r.Social, ", "), strings.Join(r.CryptoCurrencyAddress, ", "), strings.Join(r.Phone, ", "),
strings.Join(r.Company, ", ")}) strings.Join(r.Company, ", ")})
} }
} else { } else {
for _, r := range results.Results { for _, r := range results.Results {
rows = append(rows, []string{ rows = append(rows, []string{
strings.Join(r.Name, ", "), strings.Join(r.Email, ", "), strings.Join(r.Email, ", "),
strings.Join(r.Username, ", "), strings.Join(r.Password, ", "), strings.Join(r.Username, ", "),
strings.Join(r.Address, ", "), strings.Join(r.Phone, ", "), strings.Join(r.Password, ", "),
strings.Join(r.Social, ", "), strings.Join(r.CryptoCurrencyAddress, ", "), strings.Join(r.Phone, ", "),
strings.Join(r.Company, ", ")}) strings.Join(r.Company, ", ")})
} }
} }
@@ -284,7 +294,7 @@ func (dh *Dehasher) parseResults() {
if dh.debug { if dh.debug {
debug.PrintInfo("extracting credentials") debug.PrintInfo("extracting credentials")
} }
creds := results.ExtractCredentials() creds := results.ExtractUsers()
if dh.debug { if dh.debug {
debug.PrintInfo("writing credentials to file") debug.PrintInfo("writing credentials to file")
} }
+94
View File
@@ -0,0 +1,94 @@
package dehashed
import (
"strings"
"testing"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
func TestSetQueriesCapsSearchAtFiftyThousandResults(t *testing.T) {
options := &sqlite.QueryOptions{
MaxRecords: 75000,
MaxRequests: -1,
StartingPage: 1,
}
dehasher := NewDehasher(options)
if dehasher.maxResults != maxSearchResultsPerQuery {
t.Fatalf("maxResults = %d, want %d", dehasher.maxResults, maxSearchResultsPerQuery)
}
if dehasher.options.MaxRecords != maxSearchResultsPerPage {
t.Fatalf("page size = %d, want %d", dehasher.options.MaxRecords, maxSearchResultsPerPage)
}
if dehasher.options.MaxRequests != 5 {
t.Fatalf("max requests = %d, want 5", dehasher.options.MaxRequests)
}
}
func TestSetQueriesHonorsExplicitRequestLimit(t *testing.T) {
options := &sqlite.QueryOptions{
MaxRecords: 50000,
MaxRequests: 1,
StartingPage: 1,
}
dehasher := NewDehasher(options)
if dehasher.maxResults != maxSearchResultsPerPage {
t.Fatalf("maxResults = %d, want %d", dehasher.maxResults, maxSearchResultsPerPage)
}
if dehasher.options.MaxRecords != maxSearchResultsPerPage {
t.Fatalf("page size = %d, want %d", dehasher.options.MaxRecords, maxSearchResultsPerPage)
}
if dehasher.options.MaxRequests != 1 {
t.Fatalf("max requests = %d, want 1", dehasher.options.MaxRequests)
}
}
func TestDataWellsURLDoesNotRequireAPIKey(t *testing.T) {
got, err := dataWellsURL(DataWellsRequest{
Count: 50,
Page: 2,
Sort: "records-DESC",
})
if err != nil {
t.Fatalf("dataWellsURL returned error: %v", err)
}
if !strings.HasPrefix(got, dataWellsEndpoint+"?") {
t.Fatalf("url = %q, want prefix %q", got, dataWellsEndpoint+"?")
}
gotLower := strings.ToLower(got)
if strings.Contains(gotLower, "api_key") || strings.Contains(gotLower, "dehashed-api-key") {
t.Fatalf("url contains API key material: %q", got)
}
for _, want := range []string{"count=50", "page=2", "sort=records-DESC"} {
if !strings.Contains(got, want) {
t.Fatalf("url = %q, want %q", got, want)
}
}
}
func TestDataWellGreppableUsesSpaceSeparatedNonEmptyTokens(t *testing.T) {
got := dataWellGreppable(DataWell{
Name: "Example Breach",
Date: "2025-03-01",
Records: 500000,
IsSensitive: true,
Data: "name,email,address",
})
if strings.Contains(got, "\t") {
t.Fatalf("greppable output contains tab: %q", got)
}
if strings.Contains(got, "description=") {
t.Fatalf("greppable output contains empty field: %q", got)
}
for _, want := range []string{"name=Example_Breach", "date=2025-03-01", "records=500000", "is_sensitive=true", "data=name,email,address"} {
if !strings.Contains(got, want) {
t.Fatalf("greppable output = %q, want token %q", got, want)
}
}
}
+3 -2
View File
@@ -1,13 +1,14 @@
package easyTime package easyTime
import ( import (
"crowsnest/internal/debug"
"fmt" "fmt"
"go.uber.org/zap"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
) )
type TimeChunk struct { type TimeChunk struct {
+93 -7
View File
@@ -1,20 +1,21 @@
package export package export
import ( import (
"crowsnest/internal/files"
"crowsnest/internal/sqlite"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"gopkg.in/yaml.v3"
"io/ioutil"
"os" "os"
"sort"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v3"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
func WriteCredsToFile(creds []sqlite.Creds, outputFile string, fileType files.FileType) error { func WriteCredsToFile(creds []sqlite.User, outputFile string, fileType files.FileType) error {
var data []byte var data []byte
var err error var err error
@@ -31,6 +32,16 @@ func WriteCredsToFile(creds []sqlite.Creds, outputFile string, fileType files.Fi
outStrings = append(outStrings, c.ToString()+"\n") outStrings = append(outStrings, c.ToString()+"\n")
} }
data = []byte(strings.Join(outStrings, "")) data = []byte(strings.Join(outStrings, ""))
case files.GREPPABLE:
var outStrings []string
for _, c := range creds {
var fields []string
fields = appendGreppableField(fields, "email", c.Email)
fields = appendGreppableField(fields, "username", c.Username)
fields = appendGreppableField(fields, "password", c.Password)
outStrings = append(outStrings, strings.Join(fields, " ")+"\n")
}
data = []byte(strings.Join(outStrings, ""))
default: default:
return errors.New("unsupported file type") return errors.New("unsupported file type")
} }
@@ -65,6 +76,12 @@ func WriteToFile(results sqlite.DehashedResults, outputFile string, fileType fil
outStrings = append(outStrings, out) outStrings = append(outStrings, out)
} }
data = []byte(strings.Join(outStrings, "")) data = []byte(strings.Join(outStrings, ""))
case files.GREPPABLE:
var outStrings []string
for _, r := range result {
outStrings = append(outStrings, dehashedResultGreppable(r)+"\n")
}
data = []byte(strings.Join(outStrings, ""))
default: default:
return errors.New("unsupported file type") return errors.New("unsupported file type")
} }
@@ -73,8 +90,8 @@ func WriteToFile(results sqlite.DehashedResults, outputFile string, fileType fil
return err return err
} }
filePath := fmt.Sprintf("%s.%s", outputFile, fileType) filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return ioutil.WriteFile(filePath, data, 0644) return os.WriteFile(filePath, data, 0644)
} }
// WriteQueryResultsToFile writes query results to a file in the specified format // WriteQueryResultsToFile writes query results to a file in the specified format
@@ -121,6 +138,22 @@ func WriteQueryResultsToFile(results []map[string]interface{}, outputFile string
outStrings = append(outStrings, strings.Join(rowStrings, "\n")+"\n\n") outStrings = append(outStrings, strings.Join(rowStrings, "\n")+"\n\n")
} }
data = []byte(strings.Join(outStrings, "")) data = []byte(strings.Join(outStrings, ""))
case files.GREPPABLE:
var outStrings []string
for _, r := range results {
keys := make([]string, 0, len(r))
for k := range r {
keys = append(keys, k)
}
sort.Strings(keys)
rowStrings := make([]string, 0, len(keys))
for _, k := range keys {
rowStrings = appendGreppableField(rowStrings, k, greppableAnyValue(r[k]))
}
outStrings = append(outStrings, strings.Join(rowStrings, " ")+"\n")
}
data = []byte(strings.Join(outStrings, ""))
default: default:
return errors.New("unsupported file type") return errors.New("unsupported file type")
} }
@@ -133,6 +166,59 @@ func WriteQueryResultsToFile(results []map[string]interface{}, outputFile string
return os.WriteFile(filePath, data, 0644) return os.WriteFile(filePath, data, 0644)
} }
func dehashedResultGreppable(r sqlite.Result) string {
var fields []string
fields = appendGreppableField(fields, "id", r.DehashedId)
fields = appendGreppableField(fields, "email", strings.Join(r.Email, ","))
fields = appendGreppableField(fields, "ip_address", strings.Join(r.IpAddress, ","))
fields = appendGreppableField(fields, "username", strings.Join(r.Username, ","))
fields = appendGreppableField(fields, "password", strings.Join(r.Password, ","))
fields = appendGreppableField(fields, "hashed_password", strings.Join(r.HashedPassword, ","))
fields = appendGreppableField(fields, "hash_type", r.HashType)
fields = appendGreppableField(fields, "name", strings.Join(r.Name, ","))
fields = appendGreppableField(fields, "vin", strings.Join(r.Vin, ","))
fields = appendGreppableField(fields, "license_plate", strings.Join(r.LicensePlate, ","))
fields = appendGreppableField(fields, "url", strings.Join(r.Url, ","))
fields = appendGreppableField(fields, "social", strings.Join(r.Social, ","))
fields = appendGreppableField(fields, "cryptocurrency_address", strings.Join(r.CryptoCurrencyAddress, ","))
fields = appendGreppableField(fields, "address", strings.Join(r.Address, ","))
fields = appendGreppableField(fields, "phone", strings.Join(r.Phone, ","))
fields = appendGreppableField(fields, "company", strings.Join(r.Company, ","))
fields = appendGreppableField(fields, "database_name", r.DatabaseName)
return strings.Join(fields, " ")
}
func greppableAnyValue(value interface{}) string {
switch v := value.(type) {
case nil:
return ""
case []string:
return greppableValue(strings.Join(v, ","))
case []interface{}:
values := make([]string, 0, len(v))
for _, item := range v {
values = append(values, fmt.Sprintf("%v", item))
}
return greppableValue(strings.Join(values, ","))
case []byte:
return greppableValue(string(v))
default:
return greppableValue(fmt.Sprintf("%v", v))
}
}
func greppableValue(value string) string {
return strings.Join(strings.Fields(value), "_")
}
func appendGreppableField(fields []string, key, value string) []string {
value = greppableValue(value)
if value == "" {
return fields
}
return append(fields, fmt.Sprintf("%s=%s", key, value))
}
func WriteWhoIsHistoryToFile(results []sqlite.HistoryRecord, outputFile string, fileType files.FileType) error { func WriteWhoIsHistoryToFile(results []sqlite.HistoryRecord, outputFile string, fileType files.FileType) error {
var data []byte var data []byte
var err error var err error
+29
View File
@@ -0,0 +1,29 @@
package export
import (
"strings"
"testing"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
func TestDehashedResultGreppableUsesSpaceSeparatedNonEmptyTokens(t *testing.T) {
got := dehashedResultGreppable(sqlite.Result{
DehashedId: "123",
Name: []string{"Hargrave Mall"},
Address: []string{"irving tx"},
Url: []string{"gdt.com", "GDT.COM"},
})
if strings.Contains(got, "\t") {
t.Fatalf("greppable output contains tab: %q", got)
}
if strings.Contains(got, "vin=") {
t.Fatalf("greppable output contains empty field: %q", got)
}
for _, want := range []string{"id=123", "name=Hargrave_Mall", "address=irving_tx", "url=gdt.com,GDT.COM"} {
if !strings.Contains(got, want) {
t.Fatalf("greppable output = %q, want token %q", got, want)
}
}
}
+4 -3
View File
@@ -1,13 +1,14 @@
package export package export
import ( import (
"crowsnest/internal/files"
"crowsnest/internal/sqlite"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"gopkg.in/yaml.v3"
"os" "os"
"gopkg.in/yaml.v3"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
func WriteIStringToFile(iString sqlite.IString, outputFile string, fileType files.FileType) error { func WriteIStringToFile(iString sqlite.IString, outputFile string, fileType files.FileType) error {
+9 -2
View File
@@ -1,5 +1,7 @@
package files package files
import "strings"
type FileType int32 type FileType int32
const ( const (
@@ -7,19 +9,22 @@ const (
XML XML
YAML YAML
TEXT TEXT
GREPPABLE
UNKNOWN UNKNOWN
) )
func GetFileType(filetype string) FileType { func GetFileType(filetype string) FileType {
switch filetype { switch strings.ToLower(strings.TrimSpace(filetype)) {
case "json": case "json":
return JSON return JSON
case "xml": case "xml":
return XML return XML
case "yaml": case "yaml":
return YAML return YAML
case "txt": case "txt", "text":
return TEXT return TEXT
case "grep", "greppable":
return GREPPABLE
default: default:
return JSON return JSON
} }
@@ -35,6 +40,8 @@ func (ft FileType) String() string {
return "yaml" return "yaml"
case TEXT: case TEXT:
return "txt" return "txt"
case GREPPABLE:
return "grep"
default: default:
return "json" return "json"
} }
+4 -3
View File
@@ -1,14 +1,15 @@
package hunter_io package hunter_io
import ( import (
"crowsnest/internal/debug"
"crowsnest/internal/sqlite"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go.uber.org/zap"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
const ( const (
+2 -1
View File
@@ -1,10 +1,11 @@
package pretty package pretty
import ( import (
"crowsnest/internal/sqlite"
"fmt" "fmt"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree" "github.com/charmbracelet/lipgloss/tree"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
func WhoIsTree(root string, record sqlite.WhoisRecord) { func WhoIsTree(root string, record sqlite.WhoisRecord) {
+5 -5
View File
@@ -51,8 +51,8 @@ func InitDB(dbPath string) (*gorm.DB, error) {
} }
// Auto migrate your models // Auto migrate your models
err = db.AutoMigrate(&Result{}, &Creds{}, &QueryOptions{}, &Creds{}, &WhoisRecord{}, &SubdomainRecord{}, err = db.AutoMigrate(&Result{}, &User{}, &QueryOptions{}, &User{}, &WhoisRecord{}, &HistoryRecord{},
&HistoryRecord{}, &LookupResult{}, &HunterDomainData{}, &HunterEmail{}, &PersonData{}) &LookupResult{}, &HunterDomainData{}, &HunterEmail{}, &PersonData{}, &Subdomain{})
if err != nil { if err != nil {
zap.L().Error("Failed to migrate database", zap.Error(err)) zap.L().Error("Failed to migrate database", zap.Error(err))
return nil, fmt.Errorf("failed to migrate database: %w", err) return nil, fmt.Errorf("failed to migrate database: %w", err)
@@ -90,11 +90,11 @@ const (
func GetTable(userInput string) Table { func GetTable(userInput string) Table {
switch strings.ToLower(userInput) { switch strings.ToLower(userInput) {
case "results": case "dehashed", "results":
return ResultsTable return ResultsTable
case "runs": case "runs":
return RunsTable return RunsTable
case "creds": case "users", "creds":
return CredsTable return CredsTable
case "whois": case "whois":
return WhoIsTable return WhoIsTable
@@ -122,7 +122,7 @@ func (t Table) Object() interface{} {
case RunsTable: case RunsTable:
return QueryOptions{} return QueryOptions{}
case CredsTable: case CredsTable:
return Creds{} return User{}
case WhoIsTable: case WhoIsTable:
return WhoisRecord{} return WhoisRecord{}
case SubdomainsTable: case SubdomainsTable:
+18
View File
@@ -0,0 +1,18 @@
package sqlite
import "testing"
func TestGetTableAcceptsDisplayedTableNames(t *testing.T) {
tests := map[string]Table{
"dehashed": ResultsTable,
"results": ResultsTable,
"users": CredsTable,
"creds": CredsTable,
}
for input, want := range tests {
if got := GetTable(input); got != want {
t.Fatalf("GetTable(%q) = %v, want %v", input, got, want)
}
}
}
+15 -47
View File
@@ -1,11 +1,12 @@
package sqlite package sqlite
import ( import (
"crowsnest/internal/files"
"fmt" "fmt"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
) )
type QueryOptions struct { type QueryOptions struct {
@@ -106,15 +107,15 @@ type Result struct {
} }
func (Result) TableName() string { func (Result) TableName() string {
return "results" return "dehashed"
} }
type DehashedResults struct { type DehashedResults struct {
Results []Result `json:"results"` Results []Result `json:"results"`
} }
func (dr *DehashedResults) ExtractCredentials() []Creds { func (dr *DehashedResults) ExtractUsers() []User {
var creds []Creds var creds []User
results := dr.Results results := dr.Results
@@ -126,16 +127,22 @@ func (dr *DehashedResults) ExtractCredentials() []Creds {
email = r.Email[0] email = r.Email[0]
} }
// Get first username if available
username := ""
if len(r.Username) > 0 {
username = r.Username[0]
}
// Get first password // Get first password
password := r.Password[0] password := r.Password[0]
cred := Creds{Email: email, Password: password} cred := User{Email: email, Password: password, Username: username}
creds = append(creds, cred) creds = append(creds, cred)
} }
} }
go func() { go func() {
err := StoreDehashedCreds(creds) err := StoreUsers(creds)
if err != nil { if err != nil {
zap.L().Error("store_creds", zap.L().Error("store_creds",
zap.String("message", "failed to store creds"), zap.String("message", "failed to store creds"),
@@ -148,18 +155,11 @@ func (dr *DehashedResults) ExtractCredentials() []Creds {
return creds return creds
} }
type Creds struct { func (User) TableName() string {
gorm.Model
Email string `json:"email" yaml:"email" xml:"email" gorm:"uniqueIndex:idx_email_username_password"`
Username string `json:"username" yaml:"username" xml:"username" gorm:"uniqueIndex:idx_email_username_password"`
Password string `json:"password" yaml:"password" xml:"password" gorm:"uniqueIndex:idx_email_username_password"`
}
func (Creds) TableName() string {
return "creds" return "creds"
} }
func (c Creds) ToString() string { func (c User) ToString() string {
return fmt.Sprintf("%s%s%s", c.Username, "%", c.Password) return fmt.Sprintf("%s%s%s", c.Username, "%", c.Password)
} }
@@ -197,38 +197,6 @@ func StoreDehashedResults(results DehashedResults) error {
return lastErr return lastErr
} }
func StoreDehashedCreds(creds []Creds) error {
if len(creds) == 0 {
return nil
}
zap.L().Info("Storing credentials", zap.Int("count", len(creds)))
db := GetDB()
// Use batch insert with conflict handling
// This will insert records in batches and continue even if some fail
const batchSize = 100
var lastErr error
for i := 0; i < len(creds); i += batchSize {
end := i + batchSize
if end > len(creds) {
end = len(creds)
}
batch := creds[i:end]
// Use Clauses with OnConflict DoNothing to skip conflicts
err := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&batch, batchSize).Error
if err != nil {
zap.L().Warn("Error storing some credentials", zap.Error(err))
lastErr = err
// Continue with next batch despite error
}
}
return lastErr
}
func StoreDehashedQueryOptions(queryOptions *QueryOptions) error { func StoreDehashedQueryOptions(queryOptions *QueryOptions) error {
db := GetDB() db := GetDB()
return db.Create(queryOptions).Error return db.Create(queryOptions).Error
+1
View File
@@ -0,0 +1 @@
package sqlite
+45
View File
@@ -0,0 +1,45 @@
package sqlite
import (
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Subdomain struct {
gorm.Model
Domain string `json:"domain" yaml:"domain" xml:"domain"`
Subdomain string `json:"subdomain" yaml:"subdomain" xml:"subdomain" gorm:"uniqueIndex:idx_subdomain"`
}
func StoreSubdomains(subs []Subdomain) error {
if len(subs) == 0 {
return nil
}
zap.L().Info("Storing subdomains", zap.Int("count", len(subs)))
db := GetDB()
// Use batch insert with conflict handling
// This will insert records in batches and continue even if some fail
const batchSize = 100
var lastErr error
for i := 0; i < len(subs); i += batchSize {
end := i + batchSize
if end > len(subs) {
end = len(subs)
}
batch := subs[i:end]
// Use Clauses with OnConflict DoNothing to skip conflicts
err := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&batch, batchSize).Error
if err != nil {
zap.L().Warn("Error storing some credentials", zap.Error(err))
lastErr = err
// Continue with next batch despite error
}
}
return lastErr
}
+58
View File
@@ -0,0 +1,58 @@
package sqlite
import (
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type User struct {
gorm.Model
Company string `json:"company" yaml:"company" xml:"company"`
Position string `json:"position" yaml:"position" xml:"position"`
Department string `json:"department" yaml:"department" xml:"department"`
PhoneNumber string `json:"phone_number" yaml:"phone_number" xml:"phone_number"`
FullName string `json:"full_name" yaml:"full_name" xml:"full_name"`
Phone string `json:"phone" yaml:"phone" xml:"phone"`
Linkedin string `json:"linkedin" yaml:"linkedin" xml:"linkedin"`
Twitter string `json:"twitter" yaml:"twitter" xml:"twitter"`
Facebook string `json:"facebook" yaml:"facebook" xml:"facebook"`
Instagram string `json:"instagram" yaml:"instagram" xml:"instagram"`
Youtube string `json:"youtube" yaml:"youtube" xml:"youtube"`
Gravatar string `json:"gravatar" yaml:"gravatar" xml:"gravatar"`
Email string `json:"email" yaml:"email" xml:"email" gorm:"uniqueIndex:idx_email_username_password"`
Username string `json:"username" yaml:"username" xml:"username" gorm:"uniqueIndex:idx_email_username_password"`
Password string `json:"password" yaml:"password" xml:"password" gorm:"uniqueIndex:idx_email_username_password"`
}
func StoreUsers(users []User) error {
if len(users) == 0 {
return nil
}
zap.L().Info("Storing credentials", zap.Int("count", len(users)))
db := GetDB()
// Use batch insert with conflict handling
// This will insert records in batches and continue even if some fail
const batchSize = 100
var lastErr error
for i := 0; i < len(users); i += batchSize {
end := i + batchSize
if end > len(users) {
end = len(users)
}
batch := users[i:end]
// Use Clauses with OnConflict DoNothing to skip conflicts
err := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&batch, batchSize).Error
if err != nil {
zap.L().Warn("Error storing some credentials", zap.Error(err))
lastErr = err
// Continue with next batch despite error
}
}
return lastErr
}
-31
View File
@@ -536,37 +536,6 @@ func StoreWhoisRecord(whoisRecord WhoisRecord) error {
return nil return nil
} }
func StoreWhoisSubdomainRecords(subdomainRecords []SubdomainRecord) error {
if len(subdomainRecords) == 0 {
return nil
}
zap.L().Info("Storing subdomain records", zap.Int("count", len(subdomainRecords)))
db := GetDB()
// Use batch insert with conflict handling
const batchSize = 100
var lastErr error
for i := 0; i < len(subdomainRecords); i += batchSize {
end := i + batchSize
if end > len(subdomainRecords) {
end = len(subdomainRecords)
}
batch := subdomainRecords[i:end]
// Use Clauses with OnConflict DoNothing to skip conflicts
err := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&batch, batchSize).Error
if err != nil {
zap.L().Warn("Error storing some subdomain records", zap.Error(err))
lastErr = err
// Continue with next batch despite error
}
}
return lastErr
}
func StoreWhoisHistoryRecords(historyRecords []HistoryRecord) error { func StoreWhoisHistoryRecords(historyRecords []HistoryRecord) error {
if len(historyRecords) == 0 { if len(historyRecords) == 0 {
return nil return nil
+5 -4
View File
@@ -2,15 +2,16 @@ package whois
import ( import (
"bytes" "bytes"
"crowsnest/internal/debug"
"crowsnest/internal/dehashed"
"crowsnest/internal/sqlite"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"go.uber.org/zap"
"io" "io"
"net/http" "net/http"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/dehashed"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
) )
type DehashedWHOISSearchRequest struct { type DehashedWHOISSearchRequest struct {