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 }