tools/gcpviz/main.go (1,298 lines of code) (raw):

/* # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # */ package gcpviz import ( "bufio" "bytes" "context" "encoding/binary" "encoding/gob" "encoding/json" "fmt" "io" "io/ioutil" "log" "net" "os" "reflect" "strconv" "strings" "sync" "time" "github.com/cayleygraph/cayley" "github.com/cayleygraph/cayley/graph" "github.com/cayleygraph/cayley/query" "github.com/cayleygraph/cayley/query/gizmo" cquad "github.com/cayleygraph/quad" "github.com/gogo/protobuf/proto" "github.com/pkg/errors" "github.com/forseti-security/config-validator/pkg/api/validator" cvasset "github.com/forseti-security/config-validator/pkg/asset" "github.com/golang/protobuf/jsonpb" "github.com/yalp/jsonpath" "gopkg.in/yaml.v3" "text/template" "github.com/boltdb/bolt" _ "github.com/cayleygraph/cayley/graph/kv/bolt" "github.com/mitchellh/go-wordwrap" "github.com/tidwall/sjson" "github.com/willf/bloom" ) type GcpViz struct { QS graph.QuadStore QW graph.QuadWriter Relations ResourceRelations AssetDatabase *bolt.DB Assets *bolt.Bucket Aliases *bolt.Bucket Graph *bolt.Bucket transaction *bolt.Tx bfilter *bloom.BloomFilter OrgRoots []string TotalVertexes int64 TotalEdges int64 TotalAliases int64 TotalIps int64 } type TemplateResourceResource struct { Data interface{} `json:"data"` DiscoveryDocumentUri string `json:"discovery_document_uri"` DiscoveryName string `json:"discovery:name"` Version string `json:"version"` Parent string `json:"parent"` } type TemplateResource struct { Name string `json:"name"` AssetType string `json:"asset_type"` Resource TemplateResourceResource `json:"resource"` Ancestors []string `json:"ancestors"` } type ResourceRelations struct { AssetTypes map[string][]jsonpath.FilterFunc Aliases map[string][]jsonpath.FilterFunc Enrich map[string]map[string]map[string]jsonpath.FilterFunc IpAddresses map[string][]jsonpath.FilterFunc } type RawResourceRelations struct { AssetTypes map[string][]string `yaml:"asset_types"` Aliases map[string][]string `yaml:"aliases"` Enrich map[string]map[string]map[string]string `yaml:"enrich"` IpAddresses map[string][]string `yaml:"ip_addresses"` } type GraphStyle struct { Global map[string]string `yaml:"global" json:"global"` Options map[string]string `yaml:"options" json:"options"` Edges map[string]map[string]string `yaml:"edges" json:"edges"` Nodes map[string]string `yaml:"nodes" json:"nodes"` } type NodeStyle struct { Label string `json:"label"` HeadLabel string `json:"headLabel"` TailLabel string `json:"tailLabel"` Link string `json:"link"` Resource *TemplateResourceResource } type IpAddressLink struct { Ip *net.IPNet Resource string AssetType string } var Labels map[string]*template.Template var HeadLabels map[string]*template.Template var TailLabels map[string]*template.Template var Links map[string]*template.Template var Style GraphStyle var Nodes map[string]*template.Template var Edges map[string]map[string]*template.Template var templateFuncMap = template.FuncMap{ // The name "title" is what the function will be called in the template text. "GetLastPart": GetLastPart, "GetPartFromEnd": GetPartFromEnd, "GetRegion": GetRegion, "Join": Join, "JoinNicely": JoinNicely, "ToLower": ToLower, "DaysLeft": DaysLeft, "NotLast": NotLast, "Replace": Replace, } func NotLast(x int, a interface{}) bool { return x != reflect.ValueOf(a).Len()-1 } func DaysLeft(s string) string { t, err := time.Parse(time.RFC3339, s) if err != nil { return "[error parsing time]" } duration := t.Sub(time.Now()) if duration.Hours()/24 < 0.0 { return "0" } return fmt.Sprintf("%.0f", duration.Hours()/24) } func ToLower(s string) string { return strings.ToLower(s) } func Join(s []interface{}) string { strs := make([]string, len(s)) for i, v := range s { strs[i] = fmt.Sprint(v) } return strings.Join(strs, ", ") } func JoinNicely(s []interface{}) string { strs := make([]string, len(s)) for i, v := range s { strs[i] = fmt.Sprint(v) } return wordwrap.WrapString(strings.Join(strs, ", "), 80) } func GetLastPart(s string) string { sp := strings.Split(s, "/") return sp[len(sp)-1] } func GetPartFromEnd(s string, idx int) string { sp := strings.Split(s, "/") return sp[len(sp)-idx] } func GetRegion(s string) string { sp := strings.Split(s, "/") for i := 0; i < len(sp); i++ { if sp[i] == "regions" { return sp[i+1] } } return "" } func Replace(from string, to string, input string) string { return strings.Replace(input, from, to, -1) } func NewGcpViz(relationsFile string, labelsFile string, styleFile string, override map[string]string) (*GcpViz, error) { qs, _ := graph.NewQuadStore("memstore", "", nil) qw, _ := graph.NewQuadWriter("single", qs, nil) gcpViz := GcpViz{QS: qs, QW: qw} err := gcpViz.loadRelationsMap(relationsFile) if err != nil { return nil, fmt.Errorf("error loading relations map: %v", err) } err = gcpViz.loadLabelsMap(labelsFile) if err != nil { return nil, fmt.Errorf("error loading labels map: %v", err) } err = gcpViz.loadStyleMap(styleFile, override) if err != nil { return nil, fmt.Errorf("error loading styles map: %v", err) } return &gcpViz, nil } func (v *GcpViz) Create(dbFile string) error { db, err := bolt.Open(dbFile, 0600, nil) if err != nil { return err } v.AssetDatabase = db err = v.initializeBolt() if err != nil { return err } return nil } func (v *GcpViz) Load(dbFile string) error { if _, err := os.Stat(dbFile); os.IsNotExist(err) { return err } db, err := bolt.Open(dbFile, 0600, &bolt.Options{ReadOnly: true}) if err != nil { return err } v.AssetDatabase = db tx, err := v.AssetDatabase.Begin(false) if err != nil { return err } defer tx.Rollback() v.Assets = tx.Bucket([]byte("Assets")) if v.Assets == nil { return errors.New("could not find bucket Assets in database") } v.Graph = tx.Bucket([]byte("Graph")) if v.Graph == nil { return errors.New("could not find bucket Graph in database") } v.Aliases = tx.Bucket([]byte("Aliases")) if v.Aliases == nil { return errors.New("could not find bucket Aliases in database") } rootBucket := tx.Bucket([]byte("Organizations")) if rootBucket == nil { return errors.New("could not find bucket OrgRoots in database") } orgRoots := rootBucket.Get([]byte("Roots")) buffer := bytes.NewReader(orgRoots) dec := gob.NewDecoder(buffer) for { err := dec.Decode(&v.OrgRoots) if err != nil { break } } var cstr cquad.String = "quad" gob.Register(cstr) graph := v.Graph.Get([]byte("Graph")) buffer = bytes.NewReader(graph) dec = gob.NewDecoder(buffer) for { var q cquad.Quad err := dec.Decode(&q) if err != nil { break } err = v.QW.AddQuad(q) if err != nil { break } } if err != io.EOF { return err } if err := tx.Rollback(); err != nil { return err } return nil } func (v *GcpViz) Save() error { // Insert resource to BoltDB tx, err := v.AssetDatabase.Begin(true) if err != nil { return err } defer tx.Rollback() ctx := context.Background() var orgBuffer bytes.Buffer enc := gob.NewEncoder(&orgBuffer) err = enc.Encode(v.OrgRoots) if err != nil { return err } err = tx.Bucket([]byte("Organizations")).Put([]byte("Roots"), []byte(orgBuffer.Bytes())) if err = tx.Commit(); err != nil { return err } var buffer bytes.Buffer enc = gob.NewEncoder(&buffer) var cstr cquad.String = "quad" gob.Register(cstr) it := v.QS.QuadsAllIterator() for it.Next(ctx) { ref := it.Result() q := v.QS.Quad(ref) err := enc.Encode(q) if err != nil { return err } } tx, err = v.AssetDatabase.Begin(true) if err != nil { return err } defer tx.Rollback() err = tx.Bucket([]byte("Graph")).Put([]byte("Graph"), []byte(buffer.Bytes())) if err = tx.Commit(); err != nil { return err } if err = v.AssetDatabase.Close(); err != nil { return err } return nil } func (v *GcpViz) EscapeLabel(label string) string { if strings.HasPrefix(strings.Trim(label, " \n"), "<") && strings.HasSuffix(strings.Trim(label, " \n"), ">") { return fmt.Sprintf("%s", strings.ReplaceAll(label, "\n", "<br/>")) } return fmt.Sprintf("%s", strconv.Quote(label)) } func (v *GcpViz) getAsset(node string) (*TemplateResource, error) { if strings.HasPrefix(node, "https://www.googleapis.com/compute/v1/") { node = strings.Replace(node, "https://www.googleapis.com/compute/v1/", "//compute.googleapis.com/", 1) } tx, err := v.AssetDatabase.Begin(false) if err != nil { return nil, err } defer tx.Rollback() v.Assets = tx.Bucket([]byte("Assets")) if v.Assets == nil { return nil, errors.New("could not find bucket Assets in database") } resource := v.Assets.Get([]byte(node)) if resource == nil { return nil, fmt.Errorf("resource %s not found in database", node) } var templateResource TemplateResource err = json.Unmarshal(resource, &templateResource) if err != nil { return nil, errors.Wrap(err, "marshaling resource to template resource") } return &templateResource, nil } func (v *GcpViz) renderNode(out io.Writer, node string, id int64) (bool, error) { templateResource, err := v.getAsset(node) if err != nil { return false, errors.Wrapf(err, fmt.Sprintf("resource %s not found", node)) } if _, found := Labels[templateResource.AssetType]; !found { return false, fmt.Errorf("label template not found for resource type %s", templateResource.AssetType) } if Labels[templateResource.AssetType] != nil { var label bytes.Buffer Labels[templateResource.AssetType].Execute(&label, templateResource) var link string = "" if _, found := Links[templateResource.AssetType]; found { var linkBuf bytes.Buffer Links[templateResource.AssetType].Execute(&linkBuf, templateResource) link = linkBuf.String() } if _, found := Nodes[templateResource.AssetType]; !found { return false, fmt.Errorf("node style template not found for resource type %s", templateResource.AssetType) } var nodeOut bytes.Buffer nodeStyle := NodeStyle{Resource: &templateResource.Resource, Label: v.EscapeLabel(strings.Trim(label.String(), "\n")), Link: v.EscapeLabel(strings.Trim(link, "\n"))} err = Nodes[templateResource.AssetType].Execute(&nodeOut, nodeStyle) if err != nil { return false, errors.Wrapf(err, fmt.Sprintf("error rending resource %s node", node)) } if strings.TrimSpace(nodeOut.String()) != "" { fmt.Fprintf(out, " N_%d [%s];\n", id, strings.Trim(nodeOut.String(), "\n")) return true, nil } } return false, nil } func (v *GcpViz) renderBothNodes(parent string, node string, parentId int64, id int64, out io.Writer) error { if parentId != -1 { pRaw := make([]byte, 8) binary.BigEndian.PutUint64(pRaw, uint64(parentId)) if !v.bfilter.Test(pRaw) { rendered, err := v.renderNode(out, parent, parentId) if err == nil { if rendered { v.bfilter.Add(pRaw) } } else { fmt.Fprintf(os.Stderr, "Failed to render parent node %s: %v\n", parent, err) } } } nRaw := make([]byte, 8) binary.BigEndian.PutUint64(nRaw, uint64(id)) if !v.bfilter.Test(nRaw) { rendered, err := v.renderNode(out, node, id) if err == nil { if rendered { v.bfilter.Add(nRaw) } } else { fmt.Fprintf(os.Stderr, "Failed to render node %s: %v\n", node, err) } } return nil } func (v *GcpViz) renderEdge(parent string, node string, parentId int64, id int64, out io.Writer) error { templateResourceParent, err := v.getAsset(parent) if err != nil { return errors.Wrapf(err, fmt.Sprintf("parent resource %s not found", parent)) } templateResourceTarget, err := v.getAsset(node) if err != nil { return errors.Wrapf(err, fmt.Sprintf("target resource %s not found", node)) } var edgeStyle *template.Template = nil if parentStyle, found := Edges[templateResourceParent.AssetType]; found { if targetStyle, found := parentStyle[templateResourceTarget.AssetType]; found { edgeStyle = targetStyle } else { fmt.Fprintf(os.Stderr, "Missing style %s -> %s (from %s TO %s)\n", templateResourceParent.AssetType, templateResourceTarget.AssetType, parent, node) } } else { fmt.Fprintf(os.Stderr, "Missing style %s -> %s (FROM %s to %s)\n", templateResourceParent.AssetType, templateResourceTarget.AssetType, parent, node) } if edgeStyle != nil { var headLabel bytes.Buffer if _, found := HeadLabels[templateResourceParent.AssetType]; found { HeadLabels[templateResourceParent.AssetType].Execute(&headLabel, templateResourceParent) } var tailLabel bytes.Buffer if _, found := TailLabels[templateResourceParent.AssetType]; found { TailLabels[templateResourceParent.AssetType].Execute(&tailLabel, templateResourceParent) } var edgeOut bytes.Buffer nodeStyle := NodeStyle{TailLabel: v.EscapeLabel(strings.Trim(tailLabel.String(), "\n")), HeadLabel: v.EscapeLabel(strings.Trim(headLabel.String(), "\n"))} edgeStyle.Execute(&edgeOut, nodeStyle) edge := strings.Trim(edgeOut.String(), "\n") if edge != "" { fmt.Fprintf(out, " N_%d -> N_%d [%s];\n", parentId, id, edge) } else { fmt.Fprintf(out, " N_%d -> N_%d;\n", parentId, id) } } return nil } func (v *GcpViz) GenerateNodes(wg *sync.WaitGroup, ctx context.Context, gizmoQuery string, parameters map[string]interface{}, out io.Writer) error { defer wg.Done() stats, err := v.QS.Stats(ctx, true) if err != nil { return err } v.bfilter = bloom.New(uint(20*stats.Quads.Size), 5) fmt.Fprintf(out, "digraph GCP {\n") for k, v := range Style.Global { var style bytes.Buffer styleTemplate, err := template.New("style").Parse(v) if err != nil { return fmt.Errorf("error parsing style template: %v", err) } styleTemplate.Execute(&style, parameters) fmt.Fprintf(out, " %s [%s];\n", k, style.String()) } for k, v := range Style.Options { fmt.Fprintf(out, " %s=%s;\n", k, v) } queryTemplate, err := template.New("query").Parse(gizmoQuery) if err != nil { return fmt.Errorf("error parsing query template: %v", err) } var gizmoQ bytes.Buffer parameters["Organizations"] = v.OrgRoots queryTemplate.Execute(&gizmoQ, parameters) session := gizmo.NewSession(v.QS) it, err := session.Execute(ctx, gizmoQ.String(), query.Options{ Collation: query.Raw, Limit: -1, }) if err != nil { return err } defer it.Close() for it.Next(ctx) { result := it.Result() switch result.(type) { case (*gizmo.Result): case string: log.Fatalf("Invalid result from query, expected nodes, got string: %v", result.(string)) default: log.Fatalf("Invalid result from query, expected nodes, got: %v", result) } data := result.(*gizmo.Result) var ( node string id int64 val map[string]interface{} ) if _, found := data.Tags[gizmo.TopResultTag]; !found { val = data.Val.(map[string]interface{}) if _, found = val[gizmo.TopResultTag]; !found { continue } else { node = val[gizmo.TopResultTag].(string) qval, err := cquad.AsValue(node) if !err { continue } id = reflect.ValueOf(v.QS.ValueOf(qval).Key()).Int() } } else { node = v.QS.NameOf(data.Tags[gizmo.TopResultTag]).Native().(string) id = reflect.ValueOf(data.Tags[gizmo.TopResultTag]).Int() } var parent string = "" var parentId int64 = -1 if _, found := data.Tags["parent"]; !found { if _, found := val["parent"]; found { parent = val["parent"].(string) qval, err := cquad.AsValue(parent) if !err { continue } parentId = reflect.ValueOf(v.QS.ValueOf(qval).Key()).Int() } else { if !strings.HasPrefix(node, "//cloudresourcemanager.googleapis.com/") { keys := make([]string, 0, len(data.Tags)) for key := range data.Tags { keys = append(keys, key) } return errors.New(fmt.Sprintf("Node %s missing parent tag (tags: %s)", node, strings.Join(keys, ","))) } } } else { parent = v.QS.NameOf(data.Tags["parent"]).Native().(string) parentId = reflect.ValueOf(data.Tags["parent"]).Int() } err := v.renderBothNodes(parent, node, parentId, id, out) if err != nil { fmt.Fprintf(os.Stderr, "Error rendering node (%s -> %s): %v\n", parent, node, err) } } if err := it.Err(); err != nil { return err } // Second pass, render edges session = gizmo.NewSession(v.QS) it, err = session.Execute(ctx, gizmoQ.String(), query.Options{ Collation: query.Raw, Limit: -1, }) if err != nil { return err } defer it.Close() for it.Next(ctx) { data := it.Result().(*gizmo.Result) var ( node string id int64 val map[string]interface{} ) if _, found := data.Tags[gizmo.TopResultTag]; !found { val = data.Val.(map[string]interface{}) if _, found = val[gizmo.TopResultTag]; !found { continue } else { node = val[gizmo.TopResultTag].(string) qval, err := cquad.AsValue(node) if !err { continue } id = reflect.ValueOf(v.QS.ValueOf(qval).Key()).Int() } } else { node = v.QS.NameOf(data.Tags[gizmo.TopResultTag]).Native().(string) id = reflect.ValueOf(data.Tags[gizmo.TopResultTag]).Int() } qval, err := cquad.AsValue(node) if !err { continue } p := cayley.StartPath(v.QS, qval).In("uses") var hadEdge bool = false p.Iterate(nil).EachValue(v.QS, func(val cquad.Value) { targetId := reflect.ValueOf(v.QS.ValueOf(val).Key()).Int() tRaw := make([]byte, 8) binary.BigEndian.PutUint64(tRaw, uint64(targetId)) sRaw := make([]byte, 8) binary.BigEndian.PutUint64(sRaw, uint64(id)) if v.bfilter.Test(sRaw) && v.bfilter.Test(tRaw) { target := cquad.NativeOf(val).(string) err := v.renderEdge(target, node, targetId, id, out) // Ignore errors, because some resources might be missing if err == nil { hadEdge = true } } }) if !hadEdge { // Disconnected item // We may want to do something here in the future, likely forcible connect to parent to avoid floating // resources. } } if err := it.Err(); err != nil { return err } fmt.Fprintf(out, "}\n") return nil } func (v *GcpViz) ExportNodes(wg *sync.WaitGroup, ctx context.Context, out io.Writer) error { defer wg.Done() tx, err := v.AssetDatabase.Begin(false) if err != nil { return err } defer tx.Rollback() // Iterate assets in BoltDB b := tx.Bucket([]byte("Assets")) c := b.Cursor() fmt.Fprintf(os.Stderr, "Reading assets...") var assets map[string]interface{} assets = make(map[string]interface{}, 0) for k, v := c.First(); k != nil; k, v = c.Next() { var asset map[string]interface{} err = json.Unmarshal(v, &asset) if err != nil { return err } assetMap := make(map[string]interface{}, 0) assetMap["id"] = string(k) assetMap["asset"] = asset assets[string(k)] = assetMap } fmt.Fprintf(os.Stderr, "done.\n") fmt.Fprintf(os.Stderr, "Reading edges...") // Iterate graph in BoltDB gb := tx.Bucket([]byte("Graph")) gc := gb.Cursor() for k, v := gc.First(); k != nil; k, v = gc.Next() { var q cquad.Quad dec := gob.NewDecoder(bytes.NewReader(v)) for true { err = dec.Decode(&q) if err != nil { break } subject := q.Subject.String() if len(subject) > 2 { subject = subject[1 : len(subject)-1] if _, ok := assets[subject]; ok { var asset map[string]interface{} asset = assets[subject].(map[string]interface{}) if _, ok := asset["edges"]; !ok { asset["edges"] = make(map[string]interface{}, 0) } var edgeMap map[string]interface{} = asset["edges"].(map[string]interface{}) predicate := q.Predicate.String() predicate = predicate[1 : len(predicate)-1] if predicate != "data" { if _, ok := edgeMap[predicate]; !ok { edgeMap[predicate] = make([]string, 0) } var predList []string predList = edgeMap[predicate].([]string) object := q.Object.String() object = object[1 : len(object)-1] predList = append(predList, object) edgeMap[predicate] = predList } } } } } fmt.Fprintf(os.Stderr, " done.\n") fmt.Fprintf(os.Stderr, "Exporting...") for _, v := range assets { assetBlob, err := json.Marshal(v) if err != nil { return err } out.Write(assetBlob) out.Write([]byte("\n")) } fmt.Fprintf(os.Stderr, " done.\n") return nil } // Private methods func (v *GcpViz) loadRelationsMap(fileName string) error { yamlFile, err := ioutil.ReadFile(fileName) if err != nil { return err } var relations RawResourceRelations err = yaml.Unmarshal(yamlFile, &relations) if err != nil { return err } v.Relations.AssetTypes = make(map[string][]jsonpath.FilterFunc, len(relations.AssetTypes)) for assetType, paths := range relations.AssetTypes { v.Relations.AssetTypes[assetType] = make([]jsonpath.FilterFunc, len(paths)) for idx, path := range paths { preparedPath, err := jsonpath.Prepare(path) if err != nil { return err } v.Relations.AssetTypes[assetType][idx] = preparedPath } } v.Relations.Aliases = make(map[string][]jsonpath.FilterFunc, len(relations.Aliases)) for assetType, paths := range relations.Aliases { v.Relations.Aliases[assetType] = make([]jsonpath.FilterFunc, len(paths)) for idx, path := range paths { preparedPath, err := jsonpath.Prepare(path) if err != nil { return err } v.Relations.Aliases[assetType][idx] = preparedPath } } v.Relations.IpAddresses = make(map[string][]jsonpath.FilterFunc, len(relations.IpAddresses)) for assetType, paths := range relations.IpAddresses { v.Relations.IpAddresses[assetType] = make([]jsonpath.FilterFunc, len(paths)) for idx, path := range paths { preparedPath, err := jsonpath.Prepare(path) if err != nil { return err } v.Relations.IpAddresses[assetType][idx] = preparedPath } } v.Relations.Enrich = make(map[string]map[string]map[string]jsonpath.FilterFunc, len(relations.Enrich)) for assetType, fields := range relations.Enrich { v.Relations.Enrich[assetType] = make(map[string]map[string]jsonpath.FilterFunc, len(fields)) for fieldName, subAsset := range fields { v.Relations.Enrich[assetType][fieldName] = make(map[string]jsonpath.FilterFunc, len(subAsset)) for subAssetType, path := range subAsset { preparedPath, err := jsonpath.Prepare(path) if err != nil { return err } v.Relations.Enrich[assetType][fieldName][subAssetType] = preparedPath } } } return nil } func (v *GcpViz) loadLabelsMap(fileName string) error { yamlFile, err := ioutil.ReadFile(fileName) if err != nil { return err } var tempLabels map[string]map[string]string err = yaml.Unmarshal(yamlFile, &tempLabels) if err != nil { return err } Labels = make(map[string]*template.Template, len(tempLabels)) HeadLabels = make(map[string]*template.Template, len(tempLabels)) TailLabels = make(map[string]*template.Template, len(tempLabels)) Links = make(map[string]*template.Template, len(tempLabels)) for k, v := range tempLabels { if labelTemplate, ok := v["label"]; ok { if labelTemplate != "" { s, err := template.New(k).Funcs(templateFuncMap).Parse(labelTemplate) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parsing label template for resource type %s", k)) } Labels[k] = s } else { Labels[k] = nil } } if headLabelTemplate, ok := v["headLabel"]; ok { s, err := template.New(k).Funcs(templateFuncMap).Parse(headLabelTemplate) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parsing head label template for resource type %s", k)) } HeadLabels[k] = s } if tailLabelTemplate, ok := v["tailLabel"]; ok { s, err := template.New(k).Funcs(templateFuncMap).Parse(tailLabelTemplate) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parsing head label template for resource type %s", k)) } TailLabels[k] = s } if linkTemplate, ok := v["link"]; ok { s, err := template.New(k).Funcs(templateFuncMap).Parse(linkTemplate) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parsing link template for resource type %s", k)) } Links[k] = s } } return nil } func (v *GcpViz) loadStyleMap(fileName string, override map[string]string) error { yamlFile, err := ioutil.ReadFile(fileName) if err != nil { return err } err = yaml.Unmarshal(yamlFile, &Style) if err != nil { return err } if override != nil && len(override) > 0 { jsn, err := json.Marshal(Style) if err != nil { return errors.Wrap(err, "marshaling to json") } var jsnStr = string(jsn) for path, newValue := range override { jsnStr, err = sjson.Set(jsnStr, path, newValue) if err != nil { return err } } var temp map[string]interface{} err = json.Unmarshal([]byte(jsnStr), &temp) if err != nil { return errors.Wrap(err, "marshaling to interface") } updatedYaml, err := yaml.Marshal(temp) if err != nil { return err } err = yaml.Unmarshal(updatedYaml, &Style) if err != nil { return err } } Nodes = make(map[string]*template.Template, len(Style.Nodes)) for k, v := range Style.Nodes { s, err := template.New(k).Funcs(templateFuncMap).Parse(v) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parsing node style for resource type %s", k)) } Nodes[k] = s } Edges = make(map[string]map[string]*template.Template, len(Style.Edges)) for k, v := range Style.Edges { Edges[k] = make(map[string]*template.Template, len(v)) for kk, vv := range v { s, err := template.New(k).Funcs(templateFuncMap).Parse(vv) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parsing edge style for resource type %s", k)) } Edges[k][kk] = s } } return nil } func (v *GcpViz) initializeBolt() error { tx, err := v.AssetDatabase.Begin(true) if err != nil { return err } defer tx.Rollback() _, err = tx.CreateBucket([]byte("Assets")) if err != nil { return err } _, err = tx.CreateBucket([]byte("Graph")) if err != nil { return err } _, err = tx.CreateBucket([]byte("Aliases")) if err != nil { return err } _, err = tx.CreateBucket([]byte("Organizations")) if err != nil { return err } if err := tx.Commit(); err != nil { return err } return nil } func (v *GcpViz) jsonPathResultsToString(results interface{}) ([]string, error) { switch results.(type) { case string: return []string{results.(string)}, nil case []interface{}: var temp []string for _, val := range results.([]interface{}) { ret, err := v.jsonPathResultsToString(val) if err != nil { return nil, err } for _, s := range ret { temp = append(temp, s) } } return temp, nil } return nil, errors.New("unable to process JSON path results") } /* Programmatic cross asset resolver: * Some references cannot be handled with simple references since their targets do not match the * name attribute of a resource. This method walks through the builds translations tables. * * Example: firewall rule target service accounts are in email format and iam.googleapis.com/ServiceAccount * IDs are in in projects/proj-id/serviceAccounts/service-account-id format. */ func (v *GcpViz) EnrichAssets() error { // Iterate BoltDB tx, err := v.AssetDatabase.Begin(false) if err != nil { return err } defer tx.Rollback() /* Step 1: alias generation, configured via relations file */ aliasAssetTypes := make([]string, 0, len(v.Relations.Aliases)) for ak, _ := range v.Relations.Aliases { aliasAssetTypes = append(aliasAssetTypes, ak) } fmt.Fprintf(os.Stderr, "\nCreating reference aliases for assets...\n") wrtx, err := v.AssetDatabase.Begin(true) if err != nil { return err } err = tx.Bucket([]byte("Assets")).ForEach(func(bk, bv []byte) error { isAliasAsset := false for _, aliasAssetType := range aliasAssetTypes { // Simple optimization to avoid unmarshaling tons of JSON if strings.Contains(string(bv), aliasAssetType) { isAliasAsset = true break } } if !isAliasAsset { return nil } asset, resource, err := v.parseJsonAsset(bv) if err != nil { return err } name := asset.GetName() assetType := asset.GetAssetType() _resource := resource.(map[string]interface{}) if aliases, ok := v.Relations.Aliases[assetType]; ok { for _, jsonPath := range aliases { if res, ok := _resource["resource"]; ok { _data := res.(map[string]interface{}) if _, ok := _data["data"]; ok { targets, err := jsonPath(_data) if err != nil { continue } _targets, err := v.jsonPathResultsToString(targets) for _, vertex := range _targets { err = wrtx.Bucket([]byte("Aliases")).Put([]byte(vertex), []byte(name)) v.TotalAliases++ } } } } } return nil }) if err != nil { wrtx.Rollback() return err } else { if err = wrtx.Commit(); err != nil { return err } } fmt.Fprintf(os.Stderr, "Creating references between assets...\n") tx, err = v.AssetDatabase.Begin(false) if err != nil { return err } defer tx.Rollback() relationsAssetTypes := make([]string, 0, len(v.Relations.AssetTypes)) for rk, _ := range v.Relations.AssetTypes { relationsAssetTypes = append(relationsAssetTypes, rk) } ipAssetTypes := make([]string, 0, len(v.Relations.IpAddresses)) for ik, _ := range v.Relations.IpAddresses { ipAssetTypes = append(ipAssetTypes, ik) } ipAddresses := make([]IpAddressLink, 0) /* Step 2: standard reference generation, configured via relations file */ err = tx.Bucket([]byte("Assets")).ForEach(func(bk, bv []byte) error { isRelationsAsset := false isIpAsset := false for _, relationsAssetType := range relationsAssetTypes { // Simple optimization to avoid unmarshaling tons of JSON if strings.Contains(string(bv), relationsAssetType) { isRelationsAsset = true break } } for _, ipAssetType := range ipAssetTypes { // Simple optimization to avoid unmarshaling tons of JSON if strings.Contains(string(bv), ipAssetType) { isIpAsset = true break } } if !isRelationsAsset && !isIpAsset { return nil } asset, resource, err := v.parseJsonAsset(bv) if err != nil { return err } assetType := asset.GetAssetType() name := asset.GetName() if isIpAsset { _resource := resource.(map[string]interface{}) if relations, ok := v.Relations.IpAddresses[assetType]; ok { for _, jsonPath := range relations { if res, ok := _resource["resource"]; ok { _data := res.(map[string]interface{}) if _, ok := _data["data"]; ok { targets, err := jsonPath(_data) if err != nil { continue } _targets, err := v.jsonPathResultsToString(targets) for _, ipRange := range _targets { if ipRange == "0.0.0.0/0" { continue } if !strings.Contains(ipRange, "/") { ipRange = ipRange + "/32" } _, parsedIp, err := net.ParseCIDR(ipRange) if err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to parse IP %v for resource %v.\n", ipRange, name) } ip := IpAddressLink{Ip: parsedIp, Resource: name, AssetType: assetType} ipAddresses = append(ipAddresses, ip) v.TotalIps++ } } } } } } if isRelationsAsset { _resource := resource.(map[string]interface{}) if relations, ok := v.Relations.AssetTypes[assetType]; ok { for _, jsonPath := range relations { if res, ok := _resource["resource"]; ok { _data := res.(map[string]interface{}) if _, ok := _data["data"]; ok { targets, err := jsonPath(_data) if err != nil { continue } _targets, err := v.jsonPathResultsToString(targets) for _, vertex := range _targets { if strings.HasPrefix(vertex, "https://www.googleapis.com/compute/v1/") { vertex = strings.Replace(vertex, "https://www.googleapis.com/compute/v1/", "//compute.googleapis.com/", 1) } if !strings.HasPrefix(vertex, "//") && strings.Contains(vertex, ".googleapis.com/") { vertex = fmt.Sprintf("//%s", vertex) } rotx, err := v.AssetDatabase.Begin(false) if err != nil { return err } defer rotx.Rollback() target := rotx.Bucket([]byte("Assets")).Get([]byte(vertex)) if target == nil { alias := rotx.Bucket([]byte("Aliases")).Get([]byte(vertex)) if alias != nil { vertex = string(alias) } else { if !strings.HasPrefix(vertex, "//") && !strings.Contains(vertex, ".googleapis.com/") { p := strings.SplitN(assetType, "/", 2) vertex = fmt.Sprintf("//%s/%s", p[0], vertex) } } } v.QW.AddQuad(cayley.Quad(name, "uses", vertex, assetType)) v.TotalEdges++ } } } } } } return nil }) /* Step 3: Link via IP addresses */ fmt.Fprintf(os.Stderr, "Processing resource IP addresses...\n") for ik, ip := range ipAddresses { for sik, sip := range ipAddresses { if ik != sik { if ip.AssetType != sip.AssetType && sip.Ip.Contains(ip.Ip.IP) { v.QW.AddQuad(cayley.Quad(ip.Resource, "uses", sip.Resource, ip.AssetType)) v.TotalEdges++ } } } } /* Step 4: Enrich existing asset types by incorporating linked assets into new fields */ enrichAssetTypes := make([]string, 0, len(v.Relations.Enrich)) for ek, _ := range v.Relations.Enrich { enrichAssetTypes = append(enrichAssetTypes, ek) } enrichedAssets := make(map[string]map[string][]interface{}, 0) fmt.Fprintf(os.Stderr, "Integrating subassets as part of main assets...\n") err = tx.Bucket([]byte("Assets")).ForEach(func(bk, bv []byte) error { isEnrichAsset := false for _, enrichAssetType := range enrichAssetTypes { // Simple optimization to avoid unmarshaling tons of JSON if strings.Contains(string(bv), enrichAssetType) { isEnrichAsset = true break } } if !isEnrichAsset { return nil } asset, _, err := v.parseJsonAsset(bv) if err != nil { return err } assetType := asset.GetAssetType() name := asset.GetName() qval, qerr := cquad.AsValue(name) if !qerr { return err } var subAssets []interface{} subAssets = append(subAssets, nil) for _, subAssetTypes := range v.Relations.Enrich[assetType] { for subAssetType, _ := range subAssetTypes { subAssets = append(subAssets, subAssetType) } } newFields := make(map[string][]interface{}, 0) p := cayley.StartPath(v.QS, qval).LabelContext(subAssets...).In("uses") p.Iterate(nil).EachValue(v.QS, func(val cquad.Value) { target := cquad.NativeOf(val).(string) targetAsset, err := v.getAsset(target) if err != nil { fmt.Fprintf(os.Stderr, "Warning: could not fetch sub-asset %v\n", target) } else { _data := targetAsset.Resource.Data.(map[string]interface{}) for field, subAssetTypes := range v.Relations.Enrich[assetType] { for subAssetType, jsonPath := range subAssetTypes { if subAssetType == targetAsset.AssetType { targets, err := jsonPath(_data) if err != nil { fmt.Fprintf(os.Stderr, "Warning: JSON-Path error in enrichment: %v\n", err) continue } if _, ok := newFields[field]; !ok { newFields[field] = make([]interface{}, 0) } newFields[field] = append(newFields[field], targets) } } } enrichedAssets[name] = newFields } }) return nil }) tx, err = v.AssetDatabase.Begin(true) if err != nil { return err } defer tx.Rollback() for name, newFields := range enrichedAssets { asset, err := v.getAsset(name) if err == nil { for k, v := range newFields { asset.Resource.Data.(map[string]interface{})[k] = v } err = v.UpdateAsset(tx, name, asset) if err != nil { return err } } else { return err } } if err = tx.Commit(); err != nil { return err } fmt.Fprintf(os.Stderr, "\nTotal vertexes: %d, total edges: %d, total aliases: %d, total IPs: %d\n", v.TotalVertexes, v.TotalEdges, v.TotalAliases, v.TotalIps) return nil } func (v *GcpViz) AddAsset(tx *bolt.Tx, asset validator.Asset, resource interface{}, addResourceData bool) error { name := asset.GetName() parent := asset.GetResource().GetParent() assetType := asset.GetAssetType() jsn, err := json.Marshal(resource) if err != nil { return errors.Wrap(err, "marshaling to json") } err = tx.Bucket([]byte("Assets")).Put([]byte(name), []byte(jsn)) if err != nil { return err } if assetType == "cloudresourcemanager.googleapis.com/Organization" { v.OrgRoots = append(v.OrgRoots, name) } v.QW.AddQuad(cayley.Quad(parent, "child", name, assetType)) if addResourceData { data := resource.(map[string]interface{}) if resourceValue, ok := data["resource"]; ok { resourceValueMap := resourceValue.(map[string]interface{}) if _, ok := resourceValueMap["data"]; ok { v.QW.AddQuad(cayley.Quad(name, "data", string(jsn), assetType)) } } } v.TotalVertexes++ v.TotalEdges++ return nil } func (v *GcpViz) UpdateAsset(tx *bolt.Tx, name string, resource interface{}) error { jsn, err := json.Marshal(resource) if err != nil { return errors.Wrap(err, "marshaling to json") } err = tx.Bucket([]byte("Assets")).Put([]byte(name), []byte(jsn)) if err != nil { return err } return nil } func (v *GcpViz) ReadAssetsFromFile(input string, addResourceData bool) error { file, err := os.Open(input) if err != nil { return err } finfo, err := file.Stat() if err != nil { return nil } fileSize := finfo.Size() var reader = bufio.NewReader(file) const bufferSize = 5 * 1024 * 1024 // Length of one line is maximum 5 MB scanner := bufio.NewScanner(reader) buf := make([]byte, bufferSize) scanner.Buffer(buf, bufferSize) var ( lastPos int64 = 0 printThreshold int64 = fileSize / 20 writes int64 = 0 ) // Insert resource to BoltDB tx, err := v.AssetDatabase.Begin(true) if err != nil { return err } defer tx.Rollback() for scanner.Scan() { if writes > 1000 { // Commit, start new transaction if err = tx.Commit(); err != nil { return err } tx, err = v.AssetDatabase.Begin(true) if err != nil { return err } defer tx.Rollback() writes = 0 } currentPos, err := file.Seek(0, io.SeekCurrent) if err != nil { return err } if (currentPos - lastPos) >= printThreshold { fmt.Fprintf(os.Stderr, "Reading assets: %d/%d bytes processed.\r", currentPos, fileSize) lastPos = currentPos } pbAsset, resource, err := v.parseJsonAsset(scanner.Bytes()) if err != nil { return err } err = v.AddAsset(tx, *pbAsset, resource, addResourceData) if err != nil { return err } writes++ } if err = tx.Commit(); err != nil { return err } return nil } // Code graciously borrowed from CFT Scorecard func (v *GcpViz) unmarshallAsset(from []byte, to proto.Message) (interface{}, error) { var temp map[string]interface{} err := json.Unmarshal(from, &temp) if err != nil { return nil, errors.Wrap(err, "marshaling to interface") } if val, ok := temp["org_policy"]; ok { for _, op := range val.([]interface{}) { orgPolicy := op.(map[string]interface{}) delete(orgPolicy, "update_time") } } err = v.protoViaJSON(temp, to) if err == nil { return temp, nil } return nil, err } // protoViaJSON uses JSON as an intermediary serialization to convert a value into // a protobuf message. func (v *GcpViz) protoViaJSON(from interface{}, to proto.Message) error { jsn, err := json.Marshal(from) if err != nil { return errors.Wrap(err, "marshaling to json") } umar := &jsonpb.Unmarshaler{AllowUnknownFields: true} if err := umar.Unmarshal(strings.NewReader(string(jsn)), to); err != nil { return errors.Wrap(err, "unmarshaling to proto") } return nil } // Code graciously borrowed from CFT Scorecard func (v *GcpViz) parseJsonAsset(input []byte) (*validator.Asset, interface{}, error) { pbAsset := &validator.Asset{} umj, err := v.unmarshallAsset(input, pbAsset) if err != nil { return nil, nil, errors.Wrapf(err, "converting asset %s to proto", string(input)) } err = cvasset.SanitizeAncestryPath(pbAsset) if err != nil { return nil, nil, errors.Wrapf(err, "fetching ancestry path for %s", pbAsset) } return pbAsset, umj, nil }