Fixed issue where query only exported to file

This commit is contained in:
Evan Hosinski
2026-04-07 09:37:41 -04:00
parent 5905b3478d
commit f23bd04114
7 changed files with 133 additions and 56 deletions
+10 -13
View File
@@ -18,15 +18,16 @@ func init() {
rootCmd.AddCommand(queryCmd) rootCmd.AddCommand(queryCmd)
// Add flags specific to whois command // Add flags specific to whois command
queryCmd.Flags().StringVarP(&dbQueryTableName, "table", "t", "", "Table to query (results, creds, whois, subdomains, history, runs)") queryCmd.Flags().StringVarP(&dbQueryTableName, "table", "t", "", "Table to query (dehashed, users, whois, subdomains, lookup, runs)")
queryCmd.Flags().IntVarP(&dbQueryLimitRows, "limit", "l", 100, "Limit number of results") queryCmd.Flags().IntVarP(&dbQueryLimitRows, "limit", "l", 100, "Limit number of results")
queryCmd.Flags().StringVarP(&dbQueryNotNull, "not-null", "n", "", "Filter for non-null values (comma-separated list, e.g., 'password,email')") queryCmd.Flags().StringVarP(&dbQueryNotNull, "not-null", "n", "", "Filter for non-null values (comma-separated list, e.g., 'password,email')")
queryCmd.Flags().StringVarP(&dbQueryColumns, "columns", "c", "", "Columns to display in output (comma-separated list, e.g., 'username,email,password')") queryCmd.Flags().StringVarP(&dbQueryColumns, "columns", "c", "", "Columns to display in output (comma-separated list, e.g., 'username,email,password')")
queryCmd.Flags().StringVarP(&dbQueryUserQuery, "user-query", "q", "", "User query to execute") queryCmd.Flags().StringVarP(&dbQueryUserQuery, "user-query", "q", "", "User query to execute")
queryCmd.Flags().StringVarP(&dbQueryRawQuery, "raw-query", "r", "", "Raw SQL query to execute") queryCmd.Flags().StringVarP(&dbQueryRawQuery, "raw-query", "r", "", "Raw SQL query to execute")
queryCmd.Flags().BoolVarP(&dbQueryListAll, "list-all", "a", false, "List all tables and their columns") queryCmd.Flags().BoolVarP(&dbQueryListAll, "list-all", "a", false, "List all tables and their columns")
queryCmd.Flags().BoolVarP(&dbQueryExport, "export", "x", false, "Export results to file using --file and --format")
queryCmd.Flags().StringVarP(&dbQueryFormat, "format", "f", "json", "Output format (json, yaml, xml, txt, grep)") queryCmd.Flags().StringVarP(&dbQueryFormat, "format", "f", "json", "Output format (json, yaml, xml, txt, grep)")
queryCmd.Flags().StringVarP(&dbQueryFile, "file", "o", "query", "File to output results to") queryCmd.Flags().StringVarP(&dbQueryFile, "file", "o", "query", "File to output results to when --export is set")
// Add mutually exclusive flags to query and raw-query // Add mutually exclusive flags to query and raw-query
// Cannot use query and raw-query at the same time // Cannot use query and raw-query at the same time
@@ -45,6 +46,7 @@ var (
dbQueryUserQuery string dbQueryUserQuery string
dbQueryRawQuery string dbQueryRawQuery string
dbQueryListAll bool dbQueryListAll bool
dbQueryExport bool
dbQueryFormat string dbQueryFormat string
dbQueryFile string dbQueryFile string
@@ -52,7 +54,7 @@ var (
Use: "query", Use: "query",
Short: "Query the database", Short: "Query the database",
Long: `Query the database for various information. Long: `Query the database for various information.
If file is specified, results are written to file and not displayed in the terminal.`, Use --export with --file and --format to write results to a file instead of displaying them.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// If list-all flag is set, list all tables and columns // If list-all flag is set, list all tables and columns
if dbQueryListAll { if dbQueryListAll {
@@ -70,14 +72,14 @@ If file is specified, results are written to file and not displayed in the termi
// Validate table name // Validate table name
if dbQueryTableName == "" { if dbQueryTableName == "" {
fmt.Println("[!] Error: Table name is required. Use -t or --table to specify a table.") fmt.Println("[!] Error: Table name is required. Use -t or --table to specify a table.")
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs") fmt.Println("[*] Available tables: dehashed, users, whois, subdomains, lookup, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.") fmt.Println("[*] Use --list-all to see all tables and their columns.")
return return
} }
if !isValidTable(dbQueryTableName) { if !isValidTable(dbQueryTableName) {
fmt.Printf("[!] Error: Unknown table '%s'.\n", dbQueryTableName) fmt.Printf("[!] Error: Unknown table '%s'.\n", dbQueryTableName)
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs") fmt.Println("[*] Available tables: dehashed, users, whois, subdomains, lookup, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.") fmt.Println("[*] Use --list-all to see all tables and their columns.")
return return
} }
@@ -124,7 +126,7 @@ If file is specified, results are written to file and not displayed in the termi
table := sqlite.GetTable(dbQueryTableName) table := sqlite.GetTable(dbQueryTableName)
if table == sqlite.UnknownTable { if table == sqlite.UnknownTable {
fmt.Printf("[!] Error: Unknown table type '%s'.\n", dbQueryTableName) fmt.Printf("[!] Error: Unknown table type '%s'.\n", dbQueryTableName)
fmt.Println("[*] Available tables: results, creds, whois, subdomains, history, runs") fmt.Println("[*] Available tables: dehashed, users, whois, subdomains, lookup, runs")
fmt.Println("[*] Use --list-all to see all tables and their columns.") fmt.Println("[*] Use --list-all to see all tables and their columns.")
return return
} }
@@ -203,8 +205,7 @@ func tableQuery(table sqlite.Table) {
return return
} }
// Export results if file name is specified if dbQueryExport {
if len(strings.TrimSpace(dbQueryFile)) > 0 {
fmt.Println("[*] Exporting results to file...") fmt.Println("[*] Exporting results to file...")
if debugGlobal { if debugGlobal {
@@ -244,8 +245,6 @@ func tableQuery(table sqlite.Table) {
return return
} }
fmt.Println("[*] Querying Database...")
// Prepare data for pretty.Table // Prepare data for pretty.Table
headers := cols headers := cols
var tableRows [][]string var tableRows [][]string
@@ -336,7 +335,7 @@ func rawDBQuery() {
return return
} }
if len(strings.TrimSpace(dbQueryFile)) > 0 { if dbQueryExport {
fmt.Println("[*] Exporting results to file...") fmt.Println("[*] Exporting results to file...")
if debugGlobal { if debugGlobal {
@@ -377,8 +376,6 @@ func rawDBQuery() {
return return
} }
fmt.Println("[*] Querying Database...")
// Prepare data for pretty.Table // Prepare data for pretty.Table
headers := columns headers := columns
var tableRows [][]string var tableRows [][]string
+17 -13
View File
@@ -163,20 +163,24 @@ func (dwr DataWellsResponse) String() string {
} }
func dataWellGreppable(well DataWell) string { func dataWellGreppable(well DataWell) string {
fields := []string{ var fields []string
"name=" + cleanGreppableValue(well.Name), fields = appendDataWellGreppableField(fields, "name", well.Name)
"date=" + cleanGreppableValue(well.Date), fields = appendDataWellGreppableField(fields, "date", well.Date)
"records=" + strconv.Itoa(well.Records), fields = appendDataWellGreppableField(fields, "records", strconv.Itoa(well.Records))
"is_sensitive=" + strconv.FormatBool(well.IsSensitive), fields = appendDataWellGreppableField(fields, "is_sensitive", strconv.FormatBool(well.IsSensitive))
"data=" + cleanGreppableValue(well.Data), fields = appendDataWellGreppableField(fields, "data", well.Data)
"description=" + cleanGreppableValue(well.Description), fields = appendDataWellGreppableField(fields, "description", well.Description)
} return strings.Join(fields, " ")
return strings.Join(fields, "\t")
} }
func cleanGreppableValue(value string) string { func cleanGreppableValue(value string) string {
value = strings.ReplaceAll(value, "\r", " ") return strings.Join(strings.Fields(value), "_")
value = strings.ReplaceAll(value, "\n", " ") }
value = strings.ReplaceAll(value, "\t", " ")
return strings.TrimSpace(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))
} }
+22
View File
@@ -70,3 +70,25 @@ func TestDataWellsURLDoesNotRequireAPIKey(t *testing.T) {
} }
} }
} }
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)
}
}
}
+35 -28
View File
@@ -34,8 +34,11 @@ func WriteCredsToFile(creds []sqlite.User, outputFile string, fileType files.Fil
case files.GREPPABLE: case files.GREPPABLE:
var outStrings []string var outStrings []string
for _, c := range creds { for _, c := range creds {
outStrings = append(outStrings, fmt.Sprintf("email=%s\tusername=%s\tpassword=%s\n", var fields []string
greppableValue(c.Email), greppableValue(c.Username), greppableValue(c.Password))) 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, "")) data = []byte(strings.Join(outStrings, ""))
default: default:
@@ -145,9 +148,9 @@ func WriteQueryResultsToFile(results []map[string]interface{}, outputFile string
rowStrings := make([]string, 0, len(keys)) rowStrings := make([]string, 0, len(keys))
for _, k := range keys { for _, k := range keys {
rowStrings = append(rowStrings, fmt.Sprintf("%s=%s", k, greppableAnyValue(r[k]))) rowStrings = appendGreppableField(rowStrings, k, greppableAnyValue(r[k]))
} }
outStrings = append(outStrings, strings.Join(rowStrings, "\t")+"\n") outStrings = append(outStrings, strings.Join(rowStrings, " ")+"\n")
} }
data = []byte(strings.Join(outStrings, "")) data = []byte(strings.Join(outStrings, ""))
default: default:
@@ -163,26 +166,25 @@ func WriteQueryResultsToFile(results []map[string]interface{}, outputFile string
} }
func dehashedResultGreppable(r sqlite.Result) string { func dehashedResultGreppable(r sqlite.Result) string {
fields := []string{ var fields []string
"id=" + greppableValue(r.DehashedId), fields = appendGreppableField(fields, "id", r.DehashedId)
"email=" + greppableValue(strings.Join(r.Email, ",")), fields = appendGreppableField(fields, "email", strings.Join(r.Email, ","))
"ip_address=" + greppableValue(strings.Join(r.IpAddress, ",")), fields = appendGreppableField(fields, "ip_address", strings.Join(r.IpAddress, ","))
"username=" + greppableValue(strings.Join(r.Username, ",")), fields = appendGreppableField(fields, "username", strings.Join(r.Username, ","))
"password=" + greppableValue(strings.Join(r.Password, ",")), fields = appendGreppableField(fields, "password", strings.Join(r.Password, ","))
"hashed_password=" + greppableValue(strings.Join(r.HashedPassword, ",")), fields = appendGreppableField(fields, "hashed_password", strings.Join(r.HashedPassword, ","))
"hash_type=" + greppableValue(r.HashType), fields = appendGreppableField(fields, "hash_type", r.HashType)
"name=" + greppableValue(strings.Join(r.Name, ",")), fields = appendGreppableField(fields, "name", strings.Join(r.Name, ","))
"vin=" + greppableValue(strings.Join(r.Vin, ",")), fields = appendGreppableField(fields, "vin", strings.Join(r.Vin, ","))
"license_plate=" + greppableValue(strings.Join(r.LicensePlate, ",")), fields = appendGreppableField(fields, "license_plate", strings.Join(r.LicensePlate, ","))
"url=" + greppableValue(strings.Join(r.Url, ",")), fields = appendGreppableField(fields, "url", strings.Join(r.Url, ","))
"social=" + greppableValue(strings.Join(r.Social, ",")), fields = appendGreppableField(fields, "social", strings.Join(r.Social, ","))
"cryptocurrency_address=" + greppableValue(strings.Join(r.CryptoCurrencyAddress, ",")), fields = appendGreppableField(fields, "cryptocurrency_address", strings.Join(r.CryptoCurrencyAddress, ","))
"address=" + greppableValue(strings.Join(r.Address, ",")), fields = appendGreppableField(fields, "address", strings.Join(r.Address, ","))
"phone=" + greppableValue(strings.Join(r.Phone, ",")), fields = appendGreppableField(fields, "phone", strings.Join(r.Phone, ","))
"company=" + greppableValue(strings.Join(r.Company, ",")), fields = appendGreppableField(fields, "company", strings.Join(r.Company, ","))
"database_name=" + greppableValue(r.DatabaseName), fields = appendGreppableField(fields, "database_name", r.DatabaseName)
} return strings.Join(fields, " ")
return strings.Join(fields, "\t")
} }
func greppableAnyValue(value interface{}) string { func greppableAnyValue(value interface{}) string {
@@ -205,10 +207,15 @@ func greppableAnyValue(value interface{}) string {
} }
func greppableValue(value string) string { func greppableValue(value string) string {
value = strings.ReplaceAll(value, "\r", " ") return strings.Join(strings.Fields(value), "_")
value = strings.ReplaceAll(value, "\n", " ") }
value = strings.ReplaceAll(value, "\t", " ")
return strings.TrimSpace(value) func appendGreppableField(fields []string, key, value string) []string {
value = greppableValue(value)
if value == "" {
return fields
}
return append(fields, fmt.Sprintf("%s=%s", key, value))
} }
func WriteWhoIsHistoryToFile(results []sqlite.HistoryRecord, outputFile string, fileType files.FileType) error { func WriteWhoIsHistoryToFile(results []sqlite.HistoryRecord, outputFile string, fileType files.FileType) error {
+29
View File
@@ -0,0 +1,29 @@
package export
import (
"strings"
"testing"
"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)
}
}
}
+2 -2
View File
@@ -90,11 +90,11 @@ const (
func GetTable(userInput string) Table { func GetTable(userInput string) Table {
switch strings.ToLower(userInput) { switch strings.ToLower(userInput) {
case "results": case "dehashed", "results":
return ResultsTable return ResultsTable
case "runs": case "runs":
return RunsTable return RunsTable
case "creds": case "users", "creds":
return CredsTable return CredsTable
case "whois": case "whois":
return WhoIsTable return WhoIsTable
+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)
}
}
}