go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/cli/reporter/cli.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package reporter
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"sort"
    10  	"strconv"
    11  
    12  	"github.com/muesli/termenv"
    13  	"go.mondoo.com/cnquery/explorer"
    14  	"go.mondoo.com/cnquery/llx"
    15  	"go.mondoo.com/cnquery/mrn"
    16  	"go.mondoo.com/cnquery/utils/stringx"
    17  )
    18  
    19  type cliReporter struct {
    20  	*Reporter
    21  	isCompact bool
    22  	isSummary bool
    23  	out       io.Writer
    24  	data      *explorer.ReportCollection
    25  
    26  	// vv the items below will be automatically filled
    27  	bundle *explorer.BundleMap
    28  }
    29  
    30  func (r *cliReporter) print() error {
    31  	// catch case where the scan was not successful and no bundle was fetched from server
    32  	if r.data == nil {
    33  		return nil
    34  	}
    35  
    36  	if r.data.Bundle != nil {
    37  		r.bundle = r.data.Bundle.ToMap()
    38  	} else {
    39  		r.bundle = nil
    40  	}
    41  
    42  	if !r.isSummary && r.bundle != nil {
    43  		r.printQueryData()
    44  	}
    45  
    46  	r.printSummary()
    47  	return nil
    48  }
    49  
    50  func (r *cliReporter) printSummary() {
    51  	r.out.Write([]byte(r.Printer.H1("Summary (" + strconv.Itoa(len(r.data.Assets)) + " assets)")))
    52  
    53  	for mrn, asset := range r.data.Assets {
    54  		r.printAssetSummary(mrn, asset)
    55  	}
    56  }
    57  
    58  func (r *cliReporter) printAssetSummary(assetMrn string, asset *explorer.Asset) {
    59  	target := asset.Name
    60  	if target == "" {
    61  		target = assetMrn
    62  	}
    63  
    64  	r.out.Write([]byte(termenv.String(fmt.Sprintf("Target:     %s\n", target)).Foreground(r.Colors.Primary).String()))
    65  	report, ok := r.data.Reports[assetMrn]
    66  	if !ok {
    67  		// If scanning the asset has failed, there will be no report, we should first look if there's an error for that target.
    68  		if errStatus, ok := r.data.Errors[assetMrn]; ok {
    69  			switch errStatus.ErrorCode().Category() {
    70  			case explorer.ErrorCategoryInformational:
    71  				r.out.Write([]byte(errStatus.Message + "\n\n"))
    72  			case explorer.ErrorCategoryWarning:
    73  				r.out.Write([]byte(r.Printer.Warn(errStatus.Message) + "\n\n"))
    74  			case explorer.ErrorCategoryError:
    75  				r.out.Write([]byte(r.Printer.Error(errStatus.Message) + "\n\n"))
    76  			}
    77  		} else {
    78  			r.out.Write([]byte(fmt.Sprintf(
    79  				`✕ Could not find asset %s`,
    80  				target,
    81  			)))
    82  			r.out.Write([]byte("\n\n"))
    83  		}
    84  		return
    85  	}
    86  
    87  	r.out.Write([]byte(fmt.Sprintf("Datapoints: %d\n", len(report.Data))))
    88  	r.out.Write([]byte("\n"))
    89  }
    90  
    91  func (r *cliReporter) printQueryData() {
    92  	r.out.Write([]byte(r.Printer.H1("Data (" + strconv.Itoa(len(r.data.Assets)) + " assets)")))
    93  
    94  	if len(r.data.Assets) == 0 {
    95  		r.out.Write([]byte("No assets to report on."))
    96  		return
    97  	}
    98  
    99  	queriesMap := map[string]*explorer.Mquery{}
   100  	for mrn, q := range r.bundle.Queries {
   101  		queriesMap[mrn] = q
   102  	}
   103  	for _, p := range r.bundle.Packs {
   104  		for i := range p.Queries {
   105  			query := p.Queries[i]
   106  			queriesMap[query.Mrn] = query
   107  		}
   108  
   109  		for i := range p.Groups {
   110  			group := p.Groups[i]
   111  			for j := range group.Queries {
   112  				query := group.Queries[j]
   113  				queriesMap[query.Mrn] = query
   114  			}
   115  		}
   116  	}
   117  
   118  	queries := make([]*explorer.Mquery, len(queriesMap))
   119  	i := 0
   120  	for _, v := range queriesMap {
   121  		queries[i] = v
   122  		i++
   123  	}
   124  	sort.Slice(queries, func(i, j int) bool {
   125  		a := queries[i].Title
   126  		b := queries[j].Title
   127  		if a == "" {
   128  			a = queries[i].Query
   129  		}
   130  		if b == "" {
   131  			b = queries[j].Query
   132  		}
   133  		return a < b
   134  	})
   135  
   136  	for k := range r.data.Assets {
   137  		cur := r.data.Assets[k]
   138  
   139  		r.out.Write([]byte(r.Printer.H2("Asset: " + cur.Name)))
   140  
   141  		// check if the asset has an error
   142  		errStatus, ok := r.data.Errors[k]
   143  		if ok {
   144  			switch errStatus.ErrorCode().Category() {
   145  			case explorer.ErrorCategoryInformational:
   146  				r.out.Write([]byte(errStatus.Message + "\n\n"))
   147  			case explorer.ErrorCategoryWarning:
   148  				r.out.Write([]byte(r.Printer.Warn(errStatus.Message) + "\n\n"))
   149  			case explorer.ErrorCategoryError:
   150  				r.out.Write([]byte(r.Printer.Error(errStatus.Message) + "\n\n"))
   151  			}
   152  			continue
   153  		}
   154  
   155  		// print the query data
   156  		r.printAssetQueries(k, queries)
   157  	}
   158  }
   159  
   160  func (r *cliReporter) printAssetQueries(assetMrn string, queries []*explorer.Mquery) {
   161  	report, ok := r.data.Reports[assetMrn]
   162  	if !ok {
   163  		// nothing to do, we get an error message in the summary code
   164  		return
   165  	}
   166  
   167  	resolved, ok := r.data.Resolved[assetMrn]
   168  	if !ok {
   169  		// TODO: we can compute these on the fly too
   170  		return
   171  	}
   172  
   173  	results := report.RawResults()
   174  
   175  	for i := range queries {
   176  		query := queries[i]
   177  		equery, ok := resolved.ExecutionJob.Queries[query.CodeId]
   178  		if !ok {
   179  			continue
   180  		}
   181  
   182  		subRes := map[string]*llx.RawResult{}
   183  		sums := equery.Code.EntrypointChecksums()
   184  		for j := range sums {
   185  			sum := sums[j]
   186  			subRes[sum] = results[sum]
   187  		}
   188  		sums = equery.Code.DatapointChecksums()
   189  		for j := range sums {
   190  			sum := sums[j]
   191  			subRes[sum] = results[sum]
   192  		}
   193  
   194  		result := r.Reporter.Printer.Results(equery.Code, subRes)
   195  		if result == "" {
   196  			return
   197  		}
   198  		if r.isCompact {
   199  			result = stringx.MaxLines(10, result)
   200  		}
   201  
   202  		title := query.Title
   203  		if title == "" {
   204  			title, _ = mrn.GetResource(query.Mrn, "queries")
   205  		}
   206  		if title != "" {
   207  			title += ":\n"
   208  		}
   209  
   210  		r.out.Write([]byte(title))
   211  		r.out.Write([]byte(result))
   212  		r.out.Write([]byte("\n\n"))
   213  	}
   214  }