Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⭐ add structure for query explore #144

Merged
merged 1 commit into from
Oct 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ prep/tools:

# 🌙 MQL/MOTOR #

cnquery/generate: clean/proto motor/generate resources/generate llx/generate lr shared/generate
cnquery/generate: clean/proto motor/generate resources/generate llx/generate lr shared/generate explorer/generate

motor/generate:
go generate .
Expand Down Expand Up @@ -199,6 +199,9 @@ mqlc: | llx mqlc/test
mqlc/test:
go test -timeout 5s $(shell go list ./mqlc/... | grep -v '/vendor/')

explorer/generate:
go generate ./explorer

# 🏗 Binary / Build #

.PHONY: cnquery/install
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ There are so many things cnquery can do! Gather information about your fleet, fi

Explore:
- The Query Hub
- [Policy as Code](https://mondoo.com/docs/tutorials/mondoo/policy-as-code/)
- [MQL introduction](https://mondoohq.github.io/mql-intro/index.html)
- [MQL resource packs](https://mondoo.com/docs/references/mql/)
- [cnspec](https://github.com/mondoohq/cnspec), our open source, cloud-native security scanner
Expand Down
258 changes: 258 additions & 0 deletions explorer/bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package explorer

import (
"context"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"go.mondoo.com/cnquery/checksums"
llx "go.mondoo.com/cnquery/llx"
"sigs.k8s.io/yaml"
)

const (
MRN_RESOURCE_QUERY = "queries"
MRN_RESOURCE_QUERYPACK = "querypack"
MRN_RESOURCE_ASSET = "assets"
)

// BundleMap is a Bundle with easier access to its data
type BundleMap struct {
OwnerMrn string `json:"owner_mrn,omitempty"`
Packs map[string]*QueryPack `json:"packs,omitempty"`
Queries map[string]*Mquery `json:"queries,omitempty"`
Code map[string]*llx.CodeBundle `json:"code,omitempty"`
}

// NewBundleMap creates a new empty initialized map
// dataLake (optional) connects an additional data layer which may provide queries/packs
func NewBundleMap(ownerMrn string) *BundleMap {
return &BundleMap{
OwnerMrn: ownerMrn,
Packs: make(map[string]*QueryPack),
Queries: make(map[string]*Mquery),
Code: make(map[string]*llx.CodeBundle),
}
}

// BundleFromPaths loads a single bundle file or a bundle that
// was split into multiple files into a single Bundle struct
func BundleFromPaths(paths ...string) (*Bundle, error) {
// load all the source files
resolvedFilenames, err := walkBundleFiles(paths)
if err != nil {
log.Error().Err(err).Msg("could not resolve bundle files")
return nil, err
}

// aggregate all files into a single bundle
aggregatedBundle, err := aggregateFilesToBundle(resolvedFilenames)
if err != nil {
log.Error().Err(err).Msg("could merge bundle files")
return nil, err
}
return aggregatedBundle, nil
}

// walkBundleFiles iterates over all provided filenames and
// checks if the name is a file or a directory. If the filename
// is a directory, it walks the directory recursively
func walkBundleFiles(filenames []string) ([]string, error) {
// resolve file names
resolvedFilenames := []string{}
for i := range filenames {
filename := filenames[i]
fi, err := os.Stat(filename)
if err != nil {
return nil, errors.Wrap(err, "could not load bundle file: "+filename)
}

if fi.IsDir() {
filepath.WalkDir(filename, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// we ignore nested directories
if d.IsDir() {
return nil
}

// only consider .yaml|.yml files
if strings.HasSuffix(d.Name(), ".yaml") || strings.HasSuffix(d.Name(), ".yml") {
resolvedFilenames = append(resolvedFilenames, path)
}

return nil
})
} else {
resolvedFilenames = append(resolvedFilenames, filename)
}
}

return resolvedFilenames, nil
}

// aggregateFilesToBundle iterates over all provided files and loads its content.
// It assumes that all provided files are checked upfront and are not a directory
func aggregateFilesToBundle(paths []string) (*Bundle, error) {
// iterate over all files, load them and merge them
mergedBundle := &Bundle{}

for i := range paths {
path := paths[i]
bundle, err := bundleFromSingleFile(path)
if err != nil {
return nil, errors.Wrap(err, "could not load file: "+path)
}

mergedBundle.AddBundle(bundle)
}

return mergedBundle, nil
}

// bundleFromSingleFile loads a bundle from a single file
func bundleFromSingleFile(path string) (*Bundle, error) {
bundleData, err := os.ReadFile(path)
if err != nil {
return nil, err
}

return BundleFromYAML(bundleData)
}

// BundleFromYAML create a bundle from yaml contents
func BundleFromYAML(data []byte) (*Bundle, error) {
var res Bundle
err := yaml.Unmarshal(data, &res)
return &res, err
}

// ToYAML returns the bundle as yaml
func (p *Bundle) ToYAML() ([]byte, error) {
return yaml.Marshal(p)
}

func (p *Bundle) SourceHash() (string, error) {
raw, err := p.ToYAML()
if err != nil {
return "", err
}
c := checksums.New
c = c.Add(string(raw))
return c.String(), nil
}

// ToMap turns the Bundle into a BundleMap
// dataLake (optional) may be used to provide queries/packs not found in the bundle
func (p *Bundle) ToMap() *BundleMap {
res := NewBundleMap(p.OwnerMrn)

for i := range p.Packs {
c := p.Packs[i]
res.Packs[c.Mrn] = c
for j := range c.Queries {
cq := c.Queries[j]
res.Queries[cq.Mrn] = cq
}
}

return res
}

// Add another bundle into this. No duplicate packs, queries, or
// properties are allowed and will lead to an error. Both bundles must have
// MRNs for everything. OwnerMRNs must be identical as well.
func (p *Bundle) AddBundle(other *Bundle) error {
if p.OwnerMrn == "" {
p.OwnerMrn = other.OwnerMrn
} else if p.OwnerMrn != other.OwnerMrn {
return errors.New("when combining bundles the owner MRNs must be identical")
}

for i := range other.Packs {
c := other.Packs[i]
if c.Mrn == "" {
return errors.New("source bundle that is added has missing query pack MRNs")
}

for j := range p.Packs {
if p.Packs[j].Mrn == c.Mrn {
return errors.New("cannot combine query packs, duplicate query packs: " + c.Mrn)
}
}

p.Packs = append(p.Packs, c)
}

return nil
}

// Compile a bundle
// Does 4 things:
// 1. turns it into a map for easier access
// 2. compile all queries. store code in the bundle map
// 3. validation of all contents
// 4. generate MRNs for all packs, queries, and updates referencing local fields
func (p *Bundle) Compile(ctx context.Context) (*BundleMap, error) {
ownerMrn := p.OwnerMrn
if ownerMrn == "" {
return nil, errors.New("failed to compile bundle, the owner MRN is empty")
}

var warnings []error

code := map[string]*llx.CodeBundle{}

// Index packs + update MRNs and checksums, link properties via MRNs
for i := range p.Packs {
querypack := p.Packs[i]

// !this is very important to prevent user overrides! vv
querypack.InvalidateAllChecksums()

err := querypack.RefreshMRN(ownerMrn)
if err != nil {
return nil, errors.New("failed to refresh query pack " + querypack.Mrn + ": " + err.Error())
}

for i := range querypack.Queries {
query := querypack.Queries[i]

// remove leading and trailing whitespace of docs, refs and tags
query.Sanitize()

// ensure the correct mrn is set
if err = query.RefreshMRN(ownerMrn); err != nil {
return nil, err
}

// recalculate the checksums
codeBundle, err := query.RefreshChecksumAndType(nil)
if err != nil {
log.Error().Err(err).Msg("could not compile the query")
warnings = append(warnings, errors.Wrap(err, "failed to validate query '"+query.Mrn+"'"))
}

code[query.Mrn] = codeBundle
}
}

res := p.ToMap()
res.Code = code

if len(warnings) != 0 {
var msg strings.Builder
for i := range warnings {
msg.WriteString(warnings[i].Error())
msg.WriteString("\n")
}
return res, errors.New(msg.String())
}

return res, nil
}
27 changes: 27 additions & 0 deletions explorer/datalake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package explorer

import "context"

// DataLake provides a shared database access layer
type DataLake interface {
// GetQuery retrieves a given query
GetQuery(ctx context.Context, mrn string) (*Mquery, error)
// SetQuery stores a given query
// Note: the query must be defined, it cannot be nil
SetQuery(ctx context.Context, mrn string, query *Mquery) error

// SetQueryPack stores a given pack in the data lake
SetQueryPack(ctx context.Context, querypack *QueryPack, filters []*Mquery) error
// GetQueryPack retrieves and if necessary updates the pack
GetQueryPack(ctx context.Context, mrn string) (*QueryPack, error)
// DeleteQueryPack removes a given pack
// Note: the MRN has to be valid
DeleteQueryPack(ctx context.Context, mrn string) error
// GetBundle retrieves and if necessary updates the bundle
GetBundle(ctx context.Context, mrn string) (*Bundle, error)
// List all packs for a given owner
// Note: Owner MRN is required
ListQueryPacks(ctx context.Context, ownerMrn string, name string) ([]*QueryPack, error)
// GetQueryPackFilters retrieves the list of asset filters for a pack (fast)
GetQueryPackFilters(ctx context.Context, mrn string) ([]*Mquery, error)
}
3 changes: 3 additions & 0 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package explorer

//go:generate protoc --proto_path=../:. --go_out=. --go_opt=paths=source_relative --rangerrpc_out=. explorer.proto
Loading