@@ -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+
338548func 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"
0 commit comments