Skip to content

Commit 64532a2

Browse files
author
jdv
committed
--output-path options indicates taht reports and eventually details should be saved in that path
1 parent 3a72338 commit 64532a2

File tree

7 files changed

+244
-32
lines changed

7 files changed

+244
-32
lines changed

cmd/ipdex/config/global.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package config
22

33
var (
4-
OutputFormat string
5-
ForceRefresh bool
6-
Yes bool
7-
Detailed bool
8-
ReportName string
9-
Batching bool
4+
OutputFormat string
5+
OutputFilePath string
6+
ForceRefresh bool
7+
Yes bool
8+
Detailed bool
9+
ReportName string
10+
Batching bool
1011
)

cmd/ipdex/file/file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func FileCommand(file string, forceRefresh bool, yes bool) {
206206
}
207207
}
208208
stats := reportClient.GetStats(report)
209-
if err := reportClient.Display(report, stats, viper.GetString(config.OutputFormatOption), config.Detailed); err != nil {
209+
if err := reportClient.Display(report, stats, viper.GetString(config.OutputFormatOption), config.Detailed, config.OutputFilePath); err != nil {
210210
style.Fatal(err.Error())
211211
}
212212
if !reportExist && outputFormat == display.HumanFormat {

cmd/ipdex/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func init() {
7676
rootCmd.Flags().BoolVarP(&config.Yes, "yes", "y", false, "Say automatically yes to the warning about the number of IPs to scan")
7777
rootCmd.PersistentFlags().BoolVarP(&config.Detailed, "detailed", "d", false, "Show all informations about an IP or a report")
7878
rootCmd.PersistentFlags().StringVarP(&config.OutputFormat, "output", "o", "", "Output format: human or json")
79+
rootCmd.PersistentFlags().StringVar(&config.OutputFilePath, "output-path", "", "Output file path for saving CSV reports (saves report and details files separately)")
7980
rootCmd.Flags().StringVarP(&config.ReportName, "name", "n", "", "Report name when scanning a file or making a search query")
8081
rootCmd.Flags().BoolVarP(&config.Batching, "batch", "b", false, "Use batching to request the CrowdSec API. Make sure you have a premium API key to use this feature.")
8182
}

cmd/ipdex/report/show.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func NewShowCommand() *cobra.Command {
5454
} else {
5555
style.Fatal("Please provide a report ID or file used in the report you want to show with `ipdex report show 1`")
5656
}
57-
if err := reportClient.Display(report, report.Stats, viper.GetString(config.OutputFormatOption), config.Detailed); err != nil {
57+
if err := reportClient.Display(report, report.Stats, viper.GetString(config.OutputFormatOption), config.Detailed, config.OutputFilePath); err != nil {
5858
style.Fatal(err.Error())
5959
}
6060
fmt.Println()

cmd/ipdex/search/search.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func SearchCommand(query string, since string, maxResult int) {
118118
style.Fatalf("unable to create report: %s", err)
119119
}
120120
stats := reportClient.GetStats(report)
121-
if err := reportClient.Display(report, stats, viper.GetString(config.OutputFormatOption), config.Detailed); err != nil {
121+
if err := reportClient.Display(report, stats, viper.GetString(config.OutputFormatOption), config.Detailed, config.OutputFilePath); err != nil {
122122
style.Fatal(err.Error())
123123
}
124124
if outputFormat == display.HumanFormat {

pkg/display/display.go

Lines changed: 231 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func displayIP(item *cticlient.SmokeItem, ipLastRefresh time.Time, detailed bool
295295
return nil
296296
}
297297

298-
func (d *Display) DisplayReport(item *models.Report, stats *models.ReportStats, format string, withIPs bool) error {
298+
func (d *Display) DisplayReport(item *models.Report, stats *models.ReportStats, format string, withIPs bool, outputFilePath string) error {
299299
switch format {
300300
case HumanFormat:
301301
if err := displayReport(item, stats, withIPs); err != nil {
@@ -306,16 +306,21 @@ func (d *Display) DisplayReport(item *models.Report, stats *models.ReportStats,
306306
return err
307307
}
308308
case CSVFormat:
309-
// For CSV format, display in human format on screen AND save CSV files
310-
if err := displayReport(item, stats, withIPs); err != nil {
311-
return err
312-
}
313-
if err := saveReportCSV(item, stats, withIPs); err != nil {
309+
// For CSV format, display in CSV format on screen
310+
if err := displayReportCSV(item, stats, withIPs); err != nil {
314311
return err
315312
}
316313
default:
317314
return fmt.Errorf("format '%s' not supported", format)
318315
}
316+
317+
// If output file path is provided, save CSV files
318+
if outputFilePath != "" {
319+
if err := saveReportCSV(item, stats, withIPs, outputFilePath); err != nil {
320+
return err
321+
}
322+
}
323+
319324
return nil
320325
}
321326

@@ -335,6 +340,211 @@ func displayReportJSON(item *models.Report, stats *models.ReportStats) error {
335340
return nil
336341
}
337342

343+
func displayReportCSV(item *models.Report, stats *models.ReportStats, withIPs bool) error {
344+
writer := csv.NewWriter(os.Stdout)
345+
defer writer.Flush()
346+
347+
// Write general section
348+
writer.Write([]string{"General", "", ""})
349+
writer.Write([]string{"", "", ""})
350+
writer.Write([]string{"Report ID", strconv.Itoa(int(item.ID)), ""})
351+
writer.Write([]string{"Report Name", item.Name, ""})
352+
writer.Write([]string{"Creation Date", item.CreatedAt.Format("2006-01-02 15:04:05"), ""})
353+
354+
if item.IsFile {
355+
writer.Write([]string{"File path", item.FilePath, ""})
356+
writer.Write([]string{"SHA256", item.FileHash, ""})
357+
}
358+
359+
if item.IsQuery {
360+
writer.Write([]string{"Query", item.Query, ""})
361+
writer.Write([]string{"Since Duration", item.Since, ""})
362+
writer.Write([]string{"Since Time", item.SinceTime.Format("2006-01-02 15:04:05"), ""})
363+
}
364+
365+
writer.Write([]string{"Number of IPs", strconv.Itoa(len(item.IPs)), ""})
366+
367+
knownIPPercent := float64(stats.NbIPs-stats.NbUnknownIPs) / float64(stats.NbIPs) * 100
368+
ipsInBlocklistPercent := float64(stats.IPsBlockedByBlocklist) / float64(stats.NbIPs) * 100
369+
370+
writer.Write([]string{"Number of known IPs", fmt.Sprintf("%d", stats.NbIPs-stats.NbUnknownIPs), fmt.Sprintf("%.0f%%", knownIPPercent)})
371+
writer.Write([]string{"Number of IPs in Blocklist", fmt.Sprintf("%d", stats.IPsBlockedByBlocklist), fmt.Sprintf("%.0f%%", ipsInBlocklistPercent)})
372+
373+
// Empty line before Stats section
374+
writer.Write([]string{"", "", ""})
375+
376+
// Stats section
377+
writer.Write([]string{"Stats", "", ""})
378+
writer.Write([]string{"", "", ""})
379+
380+
// Top Reputation
381+
TopReputation := getTopN(stats.TopReputation, maxTopDisplayReport)
382+
if len(TopReputation) > 0 {
383+
writer.Write([]string{"Top Reputation", "", ""})
384+
for _, stat := range TopReputation {
385+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
386+
writer.Write([]string{cases.Title(language.Und).String(stat.Key), fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
387+
}
388+
writer.Write([]string{"", "", ""})
389+
}
390+
391+
// Top Classifications
392+
topClassification := getTopN(stats.TopClassifications, maxTopDisplayReport)
393+
if len(topClassification) > 0 {
394+
writer.Write([]string{"Top Classifications", "", ""})
395+
for _, stat := range topClassification {
396+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
397+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
398+
}
399+
writer.Write([]string{"", "", ""})
400+
}
401+
402+
// Top Behaviors
403+
topBehaviors := getTopN(stats.TopBehaviors, maxTopDisplayReport)
404+
if len(topBehaviors) > 0 {
405+
writer.Write([]string{"Top Behaviors", "", ""})
406+
for _, stat := range topBehaviors {
407+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
408+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
409+
}
410+
writer.Write([]string{"", "", ""})
411+
}
412+
413+
// Top Blocklists
414+
topBlocklists := getTopN(stats.TopBlocklists, maxTopDisplayReport)
415+
if len(topBlocklists) > 0 {
416+
writer.Write([]string{"Top Blocklists", "", ""})
417+
for _, stat := range topBlocklists {
418+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
419+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
420+
}
421+
writer.Write([]string{"", "", ""})
422+
}
423+
424+
// Top CVEs
425+
topCVEs := getTopN(stats.TopCVEs, maxTopDisplayReport)
426+
if len(topCVEs) > 0 {
427+
writer.Write([]string{"Top CVEs", "", ""})
428+
for _, stat := range topCVEs {
429+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
430+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
431+
}
432+
writer.Write([]string{"", "", ""})
433+
}
434+
435+
// Top IP Ranges
436+
TopIPRange := getTopN(stats.TopIPRange, maxTopDisplayReport)
437+
if len(TopIPRange) > 0 {
438+
writer.Write([]string{"Top IP Ranges", "", ""})
439+
for _, stat := range TopIPRange {
440+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
441+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
442+
}
443+
writer.Write([]string{"", "", ""})
444+
}
445+
446+
// Top Autonomous Systems
447+
topAS := getTopN(stats.TopAS, maxTopDisplayReport)
448+
if len(topAS) > 0 {
449+
writer.Write([]string{"Top Autonomous Systems", "", ""})
450+
for _, stat := range topAS {
451+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
452+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
453+
}
454+
writer.Write([]string{"", "", ""})
455+
}
456+
457+
// Top Countries
458+
topCountry := getTopN(stats.TopCountries, maxTopDisplayReport)
459+
if len(topCountry) > 0 {
460+
writer.Write([]string{"Top Countries", "", ""})
461+
for _, stat := range topCountry {
462+
percent := float64(stat.Value) / float64(stats.NbIPs) * 100
463+
writer.Write([]string{stat.Key, fmt.Sprintf("%d", stat.Value), fmt.Sprintf("%.0f%%", percent)})
464+
}
465+
writer.Write([]string{"", "", ""})
466+
}
467+
468+
// If detailed IP information is requested, show it
469+
if withIPs {
470+
writer.Write([]string{"", "", ""})
471+
writer.Write([]string{"IP Details", "", ""})
472+
writer.Write([]string{
473+
"IP", "Country", "AS Name", "Reputation", "Confidence",
474+
"Reverse DNS", "Profile", "Behaviors", "Range", "First Seen", "Last Seen",
475+
})
476+
477+
for _, ipItem := range item.IPs {
478+
country := "N/A"
479+
ipRange := "N/A"
480+
asName := "N/A"
481+
reverseDNS := "N/A"
482+
483+
if ipItem.ReverseDNS != nil && *ipItem.ReverseDNS != "" {
484+
reverseDNS = *ipItem.ReverseDNS
485+
}
486+
if ipItem.Location.Country != nil && *ipItem.Location.Country != "" {
487+
country = *ipItem.Location.Country
488+
}
489+
if ipItem.IpRange != nil && *ipItem.IpRange != "" {
490+
ipRange = *ipItem.IpRange
491+
}
492+
if ipItem.AsName != nil && *ipItem.AsName != "" {
493+
asName = *ipItem.AsName
494+
}
495+
496+
behaviors := ""
497+
for i, behavior := range ipItem.Behaviors {
498+
if i > 0 {
499+
behaviors += ", "
500+
}
501+
behaviors += behavior.Label
502+
}
503+
if behaviors == "" {
504+
behaviors = "N/A"
505+
}
506+
507+
classif := "N/A"
508+
if len(ipItem.Classifications.Classifications) > 0 {
509+
for _, classification := range ipItem.Classifications.Classifications {
510+
if len(ipItem.Classifications.Classifications) > 1 && strings.ToLower(classification.Label) == "crowdsec community blocklist" {
511+
continue
512+
}
513+
classif = classification.Label
514+
}
515+
}
516+
if len(ipItem.Classifications.FalsePositives) > 0 {
517+
for _, classification := range ipItem.Classifications.FalsePositives {
518+
classif = classification.Label
519+
}
520+
}
521+
522+
firstSeen := "N/A"
523+
lastSeen := "N/A"
524+
if ipItem.History.FirstSeen != nil && *ipItem.History.FirstSeen != "" {
525+
firstSeen = strings.Split(*ipItem.History.FirstSeen, "+")[0]
526+
}
527+
if ipItem.History.LastSeen != nil && *ipItem.History.LastSeen != "" {
528+
lastSeen = strings.Split(*ipItem.History.LastSeen, "+")[0]
529+
}
530+
531+
reputation := ipItem.Reputation
532+
confidence := ipItem.Confidence
533+
if reputation == "" {
534+
reputation = "N/A"
535+
confidence = "N/A"
536+
}
537+
538+
writer.Write([]string{
539+
ipItem.Ip, country, asName, reputation, confidence,
540+
reverseDNS, classif, behaviors, ipRange, firstSeen, lastSeen,
541+
})
542+
}
543+
}
544+
545+
return nil
546+
}
547+
338548
func TruncateWithEllipsis(s string, max int) string {
339549
if len(s) <= max {
340550
return s
@@ -570,9 +780,9 @@ func displayReport(report *models.Report, stats *models.ReportStats, withIPs boo
570780
return nil
571781
}
572782

573-
func saveReportCSV(item *models.Report, stats *models.ReportStats, withIPs bool) error {
783+
func saveReportCSV(report *models.Report, stats *models.ReportStats, withIPs bool, outputFilePath string) error {
574784
// Always save the report summary
575-
reportFilename := fmt.Sprintf("report.%d.csv", item.ID)
785+
reportFilename := fmt.Sprintf("%s/report-%d.csv", outputFilePath, report.ID)
576786
reportFile, err := os.Create(reportFilename)
577787
if err != nil {
578788
return fmt.Errorf("failed to create report CSV file %s: %v", reportFilename, err)
@@ -588,22 +798,22 @@ func saveReportCSV(item *models.Report, stats *models.ReportStats, withIPs bool)
588798
// General section
589799
csvRows = append(csvRows, []string{"General", "", ""})
590800
csvRows = append(csvRows, []string{"", "", ""})
591-
csvRows = append(csvRows, []string{"Report ID", strconv.Itoa(int(item.ID)), ""})
592-
csvRows = append(csvRows, []string{"Report Name", item.Name, ""})
593-
csvRows = append(csvRows, []string{"Creation Date", item.CreatedAt.Format("2006-01-02 15:04:05"), ""})
801+
csvRows = append(csvRows, []string{"Report ID", strconv.Itoa(int(report.ID)), ""})
802+
csvRows = append(csvRows, []string{"Report Name", report.Name, ""})
803+
csvRows = append(csvRows, []string{"Creation Date", report.CreatedAt.Format("2006-01-02 15:04:05"), ""})
594804

595-
if item.IsFile {
596-
csvRows = append(csvRows, []string{"File path", item.FilePath, ""})
597-
csvRows = append(csvRows, []string{"SHA256", item.FileHash, ""})
805+
if report.IsFile {
806+
csvRows = append(csvRows, []string{"File path", report.FilePath, ""})
807+
csvRows = append(csvRows, []string{"SHA256", report.FileHash, ""})
598808
}
599809

600-
if item.IsQuery {
601-
csvRows = append(csvRows, []string{"Query", item.Query, ""})
602-
csvRows = append(csvRows, []string{"Since Duration", item.Since, ""})
603-
csvRows = append(csvRows, []string{"Since Time", item.SinceTime.Format("2006-01-02 15:04:05"), ""})
810+
if report.IsQuery {
811+
csvRows = append(csvRows, []string{"Query", report.Query, ""})
812+
csvRows = append(csvRows, []string{"Since Duration", report.Since, ""})
813+
csvRows = append(csvRows, []string{"Since Time", report.SinceTime.Format("2006-01-02 15:04:05"), ""})
604814
}
605815

606-
csvRows = append(csvRows, []string{"Number of IPs", strconv.Itoa(len(item.IPs)), ""})
816+
csvRows = append(csvRows, []string{"Number of IPs", strconv.Itoa(len(report.IPs)), ""})
607817

608818
knownIPPercent := float64(stats.NbIPs-stats.NbUnknownIPs) / float64(stats.NbIPs) * 100
609819
ipsInBlocklistPercent := float64(stats.IPsBlockedByBlocklist) / float64(stats.NbIPs) * 100
@@ -717,7 +927,7 @@ func saveReportCSV(item *models.Report, stats *models.ReportStats, withIPs bool)
717927

718928
// If detailed IP information is requested, save to a separate file
719929
if withIPs {
720-
detailsFilename := fmt.Sprintf("details.%d.csv", item.ID)
930+
detailsFilename := fmt.Sprintf("%s/details-%d.csv", outputFilePath, report.ID)
721931
detailsFile, err := os.Create(detailsFilename)
722932
if err != nil {
723933
return fmt.Errorf("failed to create details CSV file %s: %v", detailsFilename, err)
@@ -737,7 +947,7 @@ func saveReportCSV(item *models.Report, stats *models.ReportStats, withIPs bool)
737947
})
738948

739949
// IP data
740-
for _, ipItem := range item.IPs {
950+
for _, ipItem := range report.IPs {
741951
country := "N/A"
742952
ipRange := "N/A"
743953
asName := "N/A"

pkg/report/report_client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ func (r *ReportClient) GetExpiredIPFromReport(reportID uint) ([]string, error) {
197197
return ret, nil
198198
}
199199

200-
func (r *ReportClient) Display(report *models.Report, stats *models.ReportStats, outputFormat string, withIPs bool) error {
200+
func (r *ReportClient) Display(report *models.Report, stats *models.ReportStats, outputFormat string, withIPs bool, outputFilePath string) error {
201201
displayer := display.NewDisplay()
202-
return displayer.DisplayReport(report, stats, outputFormat, withIPs)
202+
return displayer.DisplayReport(report, stats, outputFormat, withIPs, outputFilePath)
203203
}
204204

205205
func (r *ReportClient) DeleteExpiredReports(expiration string) error {

0 commit comments

Comments
 (0)