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
# 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
@@ -30,14 +30,14 @@ clean:
# Build for current platform
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-all: clean
@for platform in $(PLATFORMS); do \
for arch in $(ARCHS); do \
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 \
mv $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch $(BUILD_DIR)/$(BINARY_NAME)-$$platform-$$arch.exe; \
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:
![Alt text](.img/set-dehashed.png "Set Dehashed Key")
```bash
ar1ste1a@kali:~$ crowsnest set-dehashed <redacted>
ar1ste1a@kali:~$ crowsnest set dehashed <redacted>
```
### 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.
The default output format is JSON.
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
# Return matches for usernames exactly matching "admin" and write to text file 'admins_file.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
CrowsNest supports Hunter.io lookups.
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")
```bash
# Set the Hunter.io API key
crowsnest set-hunter <redacted>
crowsnest set hunter <redacted>
```
### Domain Search
+70 -9
View File
@@ -1,31 +1,35 @@
package cmd
import (
"crowsnest/internal/badger"
"crowsnest/internal/debug"
"crowsnest/internal/dehashed"
"crowsnest/internal/sqlite"
"fmt"
"github.com/spf13/cobra"
"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() {
// Add api command to root command
rootCmd.AddCommand(dehashedCmd)
dehashedCmd.AddCommand(dehashedDataWellsCmd)
// 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(&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(&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(&credsOnly, "creds-only", "C", false, "Return credentials only")
dehashedCmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json, yaml, xml, txt)")
dehashedCmd.Flags().StringVarP(&outputFile, "output", "o", "query", "File to output results to including extension")
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 without extension")
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(&domainQuery, "domain", "D", "", "Domain 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
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 (
@@ -66,6 +76,11 @@ var (
phoneQuery string
socialQuery string
cryptoCurrencyAddressQuery string
dataWellsCount int
dataWellsPage int
dataWellsSort string
dataWellsOutputFormat string
dataWellsOutputFile string
// Query command
dehashedCmd = &cobra.Command{
@@ -77,7 +92,7 @@ var (
// Validate credentials
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
}
@@ -118,6 +133,7 @@ var (
dehasher.Start()
fmt.Println("\n[*] Completing Process")
// Store query options
err := sqlite.StoreDehashedQueryOptions(queryOptions)
if err != nil {
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
func getDehashedApiKey() string {
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
import (
"crowsnest/internal/badger"
"crowsnest/internal/debug"
"crowsnest/internal/export"
"crowsnest/internal/files"
hunter "crowsnest/internal/hunter.io"
"crowsnest/internal/pretty"
"fmt"
"time"
"github.com/spf13/cobra"
"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() {
@@ -111,6 +113,24 @@ var (
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
fmt.Printf("[*] Writing Hunter.io Domain Search Result to file: %s%s\n", hunterOutputFile, fType.Extension())
err = export.WriteIStringToFile(result, hunterOutputFile, fType)
@@ -220,24 +240,17 @@ var (
// Pretty Print Hunter.io Email Verification Result
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 = append(rows, []string{
result.Email,
result.Status,
result.Result,
fmt.Sprintf("%d", result.Score),
fmt.Sprintf("%t", result.Regexp),
fmt.Sprintf("%t", result.Gibberish),
fmt.Sprintf("%t", result.Disposable),
fmt.Sprintf("%t", result.Webmail),
fmt.Sprintf("%t", result.MXRecords),
fmt.Sprintf("%t", result.SMTPServer),
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:")
+4 -3
View File
@@ -1,15 +1,16 @@
package cmd
import (
"crowsnest/internal/easyTime"
"crowsnest/internal/pretty"
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"hub.krkn.tech/KrakenTech/crowsnest/internal/easyTime"
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
)
func init() {
+119 -134
View File
@@ -1,153 +1,34 @@
package cmd
import (
"crowsnest/internal/pretty"
"crowsnest/internal/sqlite"
"encoding/json"
"fmt"
"strings"
"github.com/spf13/cobra"
"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() {
// Add whois command to root command
rootCmd.AddCommand(queryCmd)
// 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().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(&dbQueryUserQuery, "user-query", "q", "", "User 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(&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
// Cannot use query and raw-query at the same time
@@ -166,11 +47,15 @@ var (
dbQueryUserQuery string
dbQueryRawQuery string
dbQueryListAll bool
dbQueryExport bool
dbQueryFormat string
dbQueryFile string
queryCmd = &cobra.Command{
Use: "query",
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) {
// If list-all flag is set, list all tables and columns
if dbQueryListAll {
@@ -188,14 +73,14 @@ var (
// Validate table name
if dbQueryTableName == "" {
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.")
return
}
if !isValidTable(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.")
return
}
@@ -242,7 +127,7 @@ var (
table := sqlite.GetTable(dbQueryTableName)
if table == sqlite.UnknownTable {
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.")
return
}
@@ -321,6 +206,46 @@ func tableQuery(table sqlite.Table) {
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
headers := cols
var tableRows [][]string
@@ -411,6 +336,47 @@ func rawDBQuery() {
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
headers := columns
var tableRows [][]string
@@ -477,3 +443,22 @@ func rawDBQuery() {
// Display the table
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
import (
"crowsnest/internal/badger"
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"go.uber.org/zap"
"os"
"strings"
"hub.krkn.tech/KrakenTech/crowsnest/internal/badger"
)
var (
@@ -16,8 +17,8 @@ var (
// rootCmd is the base command for the CLI.
rootCmd = &cobra.Command{
Use: "dehasher",
Short: `Dehasher is a cli tool for querying the dehashed api.`,
Use: "crowsnest",
Short: `CrowsNest is a cli tool for querying the common OSINT api's.`,
Long: fmt.Sprintf(
"%s\n",
`
@@ -53,15 +54,23 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&debugGlobal, "debug", false, "Show debug information")
// Add subcommands
rootCmd.AddCommand(setDehashedKeyCmd)
rootCmd.AddCommand(setHunterKeyCmd)
rootCmd.AddCommand(setCmd)
rootCmd.AddCommand(setLocalDb)
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
var setDehashedKeyCmd = &cobra.Command{
Use: "set-dehashed [key]",
Use: "dehashed [key]",
Short: "Set and store Dehashed.com API key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
@@ -77,7 +86,7 @@ var setDehashedKeyCmd = &cobra.Command{
}
var setHunterKeyCmd = &cobra.Command{
Use: "set-hunter [key]",
Use: "hunter [key]",
Short: "Set and store Hunter.io API key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
@@ -94,7 +103,7 @@ var setHunterKeyCmd = &cobra.Command{
var setLocalDb = &cobra.Command{
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),
Run: func(cmd *cobra.Command, args []string) {
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
import (
"crowsnest/internal/debug"
"crowsnest/internal/export"
"crowsnest/internal/files"
"crowsnest/internal/pretty"
"crowsnest/internal/sqlite"
"crowsnest/internal/whois"
"fmt"
"github.com/spf13/cobra"
"go.uber.org/zap"
"os"
"strings"
"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() {
@@ -59,7 +60,7 @@ var (
// Validate credentials
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
}
@@ -195,8 +196,8 @@ var (
// Write history records to file if any
if len(historyRecords) > 0 {
fmt.Println("[*] Records Found: %d\n", len(historyRecords))
fmt.Println("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension())
fmt.Printf("[*] Records Found: %d\n", len(historyRecords))
fmt.Printf("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension())
writeErr := export.WriteWhoIsHistoryToFile(historyRecords, filename, fType)
if writeErr != nil {
if debugGlobal {
@@ -240,6 +241,7 @@ var (
fmt.Println("[*] Performing WHOIS subdomain scan...")
subdomains, err := w.WhoisSubdomainScan(whoisDomain)
// Get credits
if whoisShowCredits {
checkBalance(w)
}
@@ -255,7 +257,13 @@ var (
)
fmt.Printf("Error performing subdomain scan: %v\n", err)
} 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 debugGlobal {
debug.PrintInfo("failed to store subdomain record")
@@ -265,7 +273,7 @@ var (
zap.String("message", "failed to store subdomain record"),
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
+7 -6
View File
@@ -1,16 +1,17 @@
package main
import (
"crowsnest/cmd"
"crowsnest/internal/badger"
"crowsnest/internal/sqlite"
"fmt"
"os"
"path/filepath"
"github.com/winking324/rzap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"path/filepath"
"hub.krkn.tech/KrakenTech/crowsnest/cmd"
"hub.krkn.tech/KrakenTech/crowsnest/internal/badger"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
var (
@@ -82,7 +83,7 @@ func main() {
useLocalDB := badger.GetUseLocalDB()
if useLocalDB {
// Use local database in current directory
dbPath = "./crowsnest.sqlite"
dbPath = "./"
zap.L().Info("Using local database", zap.String("path", dbPath))
} else {
// Use default database path
+1 -3
View File
@@ -1,4 +1,4 @@
module crowsnest
module hub.krkn.tech/KrakenTech/crowsnest
go 1.23.0
@@ -14,7 +14,6 @@ require (
go.uber.org/zap v1.20.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.26.1
)
@@ -41,7 +40,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
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/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
+235 -12
View File
@@ -3,15 +3,19 @@ package badger
import (
"crypto/sha256"
"errors"
"github.com/dgraph-io/badger/v4"
"go.uber.org/zap"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/dgraph-io/badger/v4"
"go.uber.org/zap"
)
var (
@@ -21,13 +25,35 @@ var (
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
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown-host"
log.Printf("Error getting hostname: %v", err)
}
if legacyHostname := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_HOSTNAME")); legacyHostname != "" {
hostname = legacyHostname
}
// Get username
currentUser, err := user.Current()
@@ -35,9 +61,15 @@ func GetHardwareEntropy() []byte {
if err == nil && currentUser != nil {
username = currentUser.Username
}
if legacyUsername := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_USERNAME")); legacyUsername != "" {
username = legacyUsername
}
// Get OS and architecture info
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
fingerprint := strings.Join([]string{
@@ -45,14 +77,93 @@ func GetHardwareEntropy() []byte {
username,
osInfo,
// You could add a static salt here for additional security
"CrowsNest-static-salt-value",
fingerprintSalt,
}, ":")
// Hash the fingerprint to get a 32-byte key
return hashFingerprint(fingerprint)
}
func hashFingerprint(fingerprint string) []byte {
sum := sha256.Sum256([]byte(fingerprint))
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 {
var err error
@@ -65,7 +176,7 @@ func Start(dirPath string) *badger.DB {
}
rootDir = dirPath
encryptionKey = GetHardwareEntropy()
encryptionKey, err = GetHardwareEntropy()
if err != nil {
zap.L().Fatal("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")
opts := badger.DefaultOptions(badgerDB).
WithEncryptionKey(encryptionKey).
WithIndexCacheSize(10 << 20). // 10MB
WithLoggingLevel(badger.ERROR)
db, err = badger.Open(opts)
db, err = openBadger(badgerDB, encryptionKey)
if err != nil {
zap.L().Fatal("new_badger_db",
zap.String("message", "failed to open badger database"),
zap.L().Warn("open_badger_db",
zap.String("message", "failed to open badger database with stable machine key; trying legacy key"),
zap.Error(err),
)
db, err = openBadgerWithLegacyMigration(badgerDB, encryptionKey)
if err != nil {
zap.L().Fatal("new_badger_db",
zap.String("message", "failed to open badger database"),
zap.Error(err),
)
}
}
})
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() {
err := db.Close()
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 (
"bytes"
"crowsnest/internal/debug"
"crowsnest/internal/sqlite"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"io"
"net/http"
"strings"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
type DehashedParameter string
@@ -198,7 +199,7 @@ func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int,
req.Header.Set("Dehashed-Api-Key", dcv2.apiKey)
if dcv2.debug {
headers := req.Header.Clone()
headers := redactedHeaders(req.Header)
h := fmt.Sprintf("Headers: %v\n", headers)
debug.PrintJson(h)
zap.L().Info("v2_search_debug",
@@ -286,7 +287,7 @@ func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int,
}
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 {
@@ -304,3 +305,11 @@ func enquoteSpaced(s string) string {
}
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))
}
+68 -58
View File
@@ -1,24 +1,31 @@
package dehashed
import (
"crowsnest/internal/debug"
"crowsnest/internal/export"
"crowsnest/internal/pretty"
"crowsnest/internal/sqlite"
"fmt"
"go.uber.org/zap"
"os"
"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
type Dehasher struct {
options sqlite.QueryOptions
nextPage int
debug bool
balance int
request *DehashedSearchRequest
client *DehashedClientV2
options sqlite.QueryOptions
nextPage int
debug bool
balance int
maxResults int
request *DehashedSearchRequest
client *DehashedClientV2
}
// NewDehasher creates a new Dehasher
@@ -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
func (dh *Dehasher) setQueries() {
var numQueries int
if dh.debug {
debug.PrintInfo("setting queries")
}
switch {
case dh.options.MaxRequests == 0:
if dh.options.MaxRequests == 0 {
zap.L().Error("max requests cannot be zero")
fmt.Println("[!] Max Requests cannot be zero")
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
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 {
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
@@ -151,7 +158,7 @@ func (dh *Dehasher) Start() {
fmt.Printf(" [-] Not enough entries, ending queries\n")
break
} else {
fmt.Printf(" [+] Retrieved %d records\n", dh.options.MaxRecords)
fmt.Printf(" [+] Retrieved %d records\n", count)
}
if dh.options.PrintBalance {
@@ -211,9 +218,12 @@ func (dh *Dehasher) buildRequest() {
func (dh *Dehasher) parseResults() {
zap.L().Info("extracting_credentials")
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))
err := sqlite.StoreDehashedCreds(creds)
err := sqlite.StoreUsers(creds)
if err != nil {
zap.L().Error("store_creds",
zap.String("message", "failed to store creds"),
@@ -255,25 +265,25 @@ func (dh *Dehasher) parseResults() {
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 {
fmt.Println(" [-] Large number of results recovered, displaying first 50...")
for i := 0; i < 50; i++ {
r := results.Results[i]
rows = append(rows, []string{
strings.Join(r.Name, ", "), strings.Join(r.Email, ", "),
strings.Join(r.Username, ", "), strings.Join(r.Password, ", "),
strings.Join(r.Address, ", "), strings.Join(r.Phone, ", "),
strings.Join(r.Social, ", "), strings.Join(r.CryptoCurrencyAddress, ", "),
strings.Join(r.Email, ", "),
strings.Join(r.Username, ", "),
strings.Join(r.Password, ", "),
strings.Join(r.Phone, ", "),
strings.Join(r.Company, ", ")})
}
} else {
for _, r := range results.Results {
rows = append(rows, []string{
strings.Join(r.Name, ", "), strings.Join(r.Email, ", "),
strings.Join(r.Username, ", "), strings.Join(r.Password, ", "),
strings.Join(r.Address, ", "), strings.Join(r.Phone, ", "),
strings.Join(r.Social, ", "), strings.Join(r.CryptoCurrencyAddress, ", "),
strings.Join(r.Email, ", "),
strings.Join(r.Username, ", "),
strings.Join(r.Password, ", "),
strings.Join(r.Phone, ", "),
strings.Join(r.Company, ", ")})
}
}
@@ -284,7 +294,7 @@ func (dh *Dehasher) parseResults() {
if dh.debug {
debug.PrintInfo("extracting credentials")
}
creds := results.ExtractCredentials()
creds := results.ExtractUsers()
if dh.debug {
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
import (
"crowsnest/internal/debug"
"fmt"
"go.uber.org/zap"
"os"
"strconv"
"strings"
"time"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
)
type TimeChunk struct {
+93 -7
View File
@@ -1,20 +1,21 @@
package export
import (
"crowsnest/internal/files"
"crowsnest/internal/sqlite"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"gopkg.in/yaml.v3"
"io/ioutil"
"os"
"sort"
"strings"
"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 err error
@@ -31,6 +32,16 @@ func WriteCredsToFile(creds []sqlite.Creds, outputFile string, fileType files.Fi
outStrings = append(outStrings, c.ToString()+"\n")
}
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:
return errors.New("unsupported file type")
}
@@ -65,6 +76,12 @@ func WriteToFile(results sqlite.DehashedResults, outputFile string, fileType fil
outStrings = append(outStrings, out)
}
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:
return errors.New("unsupported file type")
}
@@ -73,8 +90,8 @@ func WriteToFile(results sqlite.DehashedResults, outputFile string, fileType fil
return err
}
filePath := fmt.Sprintf("%s.%s", outputFile, fileType)
return ioutil.WriteFile(filePath, data, 0644)
filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return os.WriteFile(filePath, data, 0644)
}
// 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")
}
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:
return errors.New("unsupported file type")
}
@@ -133,6 +166,59 @@ func WriteQueryResultsToFile(results []map[string]interface{}, outputFile string
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 {
var data []byte
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
import (
"crowsnest/internal/files"
"crowsnest/internal/sqlite"
"encoding/json"
"encoding/xml"
"fmt"
"gopkg.in/yaml.v3"
"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 {
+9 -2
View File
@@ -1,5 +1,7 @@
package files
import "strings"
type FileType int32
const (
@@ -7,19 +9,22 @@ const (
XML
YAML
TEXT
GREPPABLE
UNKNOWN
)
func GetFileType(filetype string) FileType {
switch filetype {
switch strings.ToLower(strings.TrimSpace(filetype)) {
case "json":
return JSON
case "xml":
return XML
case "yaml":
return YAML
case "txt":
case "txt", "text":
return TEXT
case "grep", "greppable":
return GREPPABLE
default:
return JSON
}
@@ -35,6 +40,8 @@ func (ft FileType) String() string {
return "yaml"
case TEXT:
return "txt"
case GREPPABLE:
return "grep"
default:
return "json"
}
+4 -3
View File
@@ -1,14 +1,15 @@
package hunter_io
import (
"crowsnest/internal/debug"
"crowsnest/internal/sqlite"
"encoding/json"
"fmt"
"go.uber.org/zap"
"io"
"net/http"
"strings"
"go.uber.org/zap"
"hub.krkn.tech/KrakenTech/crowsnest/internal/debug"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
const (
+2 -1
View File
@@ -1,10 +1,11 @@
package pretty
import (
"crowsnest/internal/sqlite"
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
"hub.krkn.tech/KrakenTech/crowsnest/internal/sqlite"
)
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
err = db.AutoMigrate(&Result{}, &Creds{}, &QueryOptions{}, &Creds{}, &WhoisRecord{}, &SubdomainRecord{},
&HistoryRecord{}, &LookupResult{}, &HunterDomainData{}, &HunterEmail{}, &PersonData{})
err = db.AutoMigrate(&Result{}, &User{}, &QueryOptions{}, &User{}, &WhoisRecord{}, &HistoryRecord{},
&LookupResult{}, &HunterDomainData{}, &HunterEmail{}, &PersonData{}, &Subdomain{})
if err != nil {
zap.L().Error("Failed to migrate database", zap.Error(err))
return nil, fmt.Errorf("failed to migrate database: %w", err)
@@ -90,11 +90,11 @@ const (
func GetTable(userInput string) Table {
switch strings.ToLower(userInput) {
case "results":
case "dehashed", "results":
return ResultsTable
case "runs":
return RunsTable
case "creds":
case "users", "creds":
return CredsTable
case "whois":
return WhoIsTable
@@ -122,7 +122,7 @@ func (t Table) Object() interface{} {
case RunsTable:
return QueryOptions{}
case CredsTable:
return Creds{}
return User{}
case WhoIsTable:
return WhoisRecord{}
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
import (
"crowsnest/internal/files"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hub.krkn.tech/KrakenTech/crowsnest/internal/files"
)
type QueryOptions struct {
@@ -106,15 +107,15 @@ type Result struct {
}
func (Result) TableName() string {
return "results"
return "dehashed"
}
type DehashedResults struct {
Results []Result `json:"results"`
}
func (dr *DehashedResults) ExtractCredentials() []Creds {
var creds []Creds
func (dr *DehashedResults) ExtractUsers() []User {
var creds []User
results := dr.Results
@@ -126,16 +127,22 @@ func (dr *DehashedResults) ExtractCredentials() []Creds {
email = r.Email[0]
}
// Get first username if available
username := ""
if len(r.Username) > 0 {
username = r.Username[0]
}
// Get first password
password := r.Password[0]
cred := Creds{Email: email, Password: password}
cred := User{Email: email, Password: password, Username: username}
creds = append(creds, cred)
}
}
go func() {
err := StoreDehashedCreds(creds)
err := StoreUsers(creds)
if err != nil {
zap.L().Error("store_creds",
zap.String("message", "failed to store creds"),
@@ -148,18 +155,11 @@ func (dr *DehashedResults) ExtractCredentials() []Creds {
return creds
}
type Creds struct {
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 {
func (User) TableName() string {
return "creds"
}
func (c Creds) ToString() string {
func (c User) ToString() string {
return fmt.Sprintf("%s%s%s", c.Username, "%", c.Password)
}
@@ -197,38 +197,6 @@ func StoreDehashedResults(results DehashedResults) error {
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 {
db := GetDB()
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
}
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 {
if len(historyRecords) == 0 {
return nil
+5 -4
View File
@@ -2,15 +2,16 @@ package whois
import (
"bytes"
"crowsnest/internal/debug"
"crowsnest/internal/dehashed"
"crowsnest/internal/sqlite"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"io"
"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 {