Skip to content

Commit

Permalink
⭐️ CSV export (#821)
Browse files Browse the repository at this point in the history
**Problem**

I want to export cnquery data quickly in CSV to see the data in my
favorite spreadsheet app.

**Solution**

```
cnquery scan ssh [email protected] --output csv > report.csv
```

The response data from the query is stored in json.
  • Loading branch information
chris-rock authored Jan 26, 2023
1 parent 139c7e8 commit 3b2d4b2
Show file tree
Hide file tree
Showing 4 changed files with 8,282 additions and 2 deletions.
152 changes: 152 additions & 0 deletions cli/reporter/csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package reporter

import (
"bytes"
"encoding/csv"

"go.mondoo.com/cnquery/explorer"
"go.mondoo.com/cnquery/llx"
"go.mondoo.com/cnquery/mrn"
"go.mondoo.com/cnquery/shared"
)

type csvStruct struct {
AssetMrn string
AssetId string
AssetName string
QueryMrn string
QueryTitle string
MQL string
QueryResult string
}

func (c csvStruct) toSlice() []string {
return []string{c.AssetMrn, c.AssetId, c.AssetName, c.QueryMrn, c.QueryTitle, c.MQL, c.QueryResult}
}

// ReportCollectionToCSV writes the given report collection to the given output directory
func ReportCollectionToCSV(data *explorer.ReportCollection, out shared.OutputHelper) error {
w := csv.NewWriter(out)

// write header
err := w.Write(csvStruct{
"Asset Mrn",
"Asset ID",
"Asset Name",
"Query Mrn",
"Query Title",
"MQL",
"Query Result",
}.toSlice())
if err != nil {
return err
}

queryMrnIdx := map[string]*explorer.Mquery{}
if data.Bundle != nil {
for i := range data.Bundle.Packs {
pack := data.Bundle.Packs[i]
for j := range pack.Queries {
query := pack.Queries[j]
queryMrnIdx[query.CodeId] = query
}
}
}

for i := range data.Assets {
asset := data.Assets[i]
parsedMrn, err := mrn.NewMRN(asset.Mrn)
if err != nil {
return err
}
assetId, err := parsedMrn.ResourceID("assets")
if err != nil {
return err
}

if data.Errors != nil {
errMsg, ok := data.Errors[asset.Mrn]
if ok {
err := w.Write(csvStruct{
AssetMrn: asset.Mrn,
AssetId: assetId,
AssetName: asset.Name,
QueryMrn: "",
QueryTitle: "",
MQL: "",
QueryResult: errMsg,
}.toSlice())
if err != nil {
return err
}
}
}

if data.Reports != nil {
report, ok := data.Reports[asset.Mrn]
if ok {
results := report.RawResults()
resolvedPack := data.Resolved[asset.Mrn]
if resolvedPack != nil && resolvedPack.ExecutionJob != nil {
for qid, query := range resolvedPack.ExecutionJob.Queries {
buf := &bytes.Buffer{}
resultWriter := &shared.IOWriter{Writer: buf}
err := ResultsToCsvEntry(query.Code, results, resultWriter)
if err != nil {
return err
}

var queryMrn string
var queryTitle string
mQuery := queryMrnIdx[qid]
if mQuery != nil {
queryMrn = mQuery.Mrn
queryTitle = mQuery.Title
}

entry := csvStruct{
AssetMrn: asset.Mrn,
AssetId: assetId,
AssetName: asset.Name,
QueryMrn: queryMrn,
QueryTitle: queryTitle,
MQL: query.Query,
QueryResult: string(buf.Bytes()),
}

err = w.Write(entry.toSlice())
if err != nil {
return err
}
}
}
}
}
}

w.Flush()
return w.Error()
}

func ResultsToCsvEntry(code *llx.CodeBundle, results map[string]*llx.RawResult, out shared.OutputHelper) error {
var checksums []string
eps := code.CodeV2.Entrypoints()
checksums = make([]string, len(eps))
for i, ref := range eps {
checksums[i] = code.CodeV2.Checksums[ref]
}

// We try to flatten the information as much as possible. If we have multiple checksums we have no choice but to use
// a full json out, otherwise we can use a simple value without the labels.
if len(checksums) == 1 {
checksum := checksums[0]
result := results[checksum]
if result != nil {
jsonData := result.Data.JSON(checksum, code)
out.Write(jsonData)
}
return nil
}

return BundleResultsToJSON(code, results, out)
}
23 changes: 23 additions & 0 deletions cli/reporter/csv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package reporter

import (
"encoding/json"
"os"
"testing"

"github.com/stretchr/testify/require"
"go.mondoo.com/cnquery/explorer"
"go.mondoo.com/cnquery/shared"
)

func TestCSVExport(t *testing.T) {
data, err := os.ReadFile("testdata/kubernetes_report.json")
require.NoError(t, err)

var report *explorer.ReportCollection
err = json.Unmarshal(data, &report)
require.NoError(t, err)
w := shared.IOWriter{Writer: os.Stdout}
err = ReportCollectionToCSV(report, &w)
require.NoError(t, err)
}
5 changes: 3 additions & 2 deletions cli/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ func (r *Reporter) Print(data *explorer.ReportCollection, out io.Writer) error {
case JSON:
w := shared.IOWriter{Writer: out}
return ReportCollectionToJSON(data, &w)
// case CSV:
// res, err = data.ToCsv()
case CSV:
w := shared.IOWriter{Writer: out}
return ReportCollectionToCSV(data, &w)
default:
return errors.New("unknown reporter type, don't recognize this Format")
}
Expand Down
Loading

0 comments on commit 3b2d4b2

Please sign in to comment.