Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
GraphComponent.vue 26.15 KiB
<template>
  <v-sheet class="pa-4" style="position: relative">
    <div
      class="legend"
      style="
        position: absolute;
        bottom: 10px;
        right: 10px;
        border: 1px solid black;
        padding: 10px;
        font-weight: bold;
      "
    >
      <div v-for="(color, key) in colors">
        <v-icon :color="color" class="mr-2">mdi-circle</v-icon> {{ color_alias[key] }}
      </div>
    </div>
    <v-row no-gutters>
      <v-col cols="12">
        <v-row vertical-align="center">
          <v-col cols="2">
            <v-select
              v-model="architectures.current"
              :items="architectures.items"
              label="Arch. / Target machine"
              clearable
              disabled
            >
            </v-select
          ></v-col>
          <v-col cols="2">
            <v-combobox
              v-if="selections['local_binfile']"
              v-model="selections['local_binfile'].current"
              :items="selections['local_binfile'].items"
              item-value="vertex.path"
              item-text="vertex.path"
              :label="filter_alias[selections['local_binfile'].label]"
              clearable
            >
            </v-combobox>
          </v-col>

          <v-col cols="1" v-if="selections['local_binfile']" justify="bottom" align="bottom">
            <v-btn color="primary" icon x-large @click="changePackageView()"
              ><v-icon>mdi-update</v-icon></v-btn
            >
          </v-col>
          <v-col cols="2" v-if="selectedNode.vertex">
            <v-text-field disabled :value="selectedNode.vertex.path"> </v-text-field>
          </v-col>
          <v-col cols="2">
            <v-checkbox
              v-model="options.drawIncludes"
              v-if="false"
              label="Draw includes"
              @change="resetGraph"
            ></v-checkbox>
          </v-col>
          <v-spacer></v-spacer>
          <v-col cols="1" class="d-flex">
            <v-spacer />
            <v-btn
              fab
              class="mt-2 push-right"
              small
              v-if="false"
              @click="
                overlay = true
                resetGraph()
              "
              ><v-icon>mdi-filter-remove</v-icon></v-btn
            >
            <v-btn fab class="mt-2 push-right ml-2" small @click="resetHoverNode"
              ><v-icon>mdi-eye</v-icon></v-btn
            >
          </v-col>
        </v-row>
      </v-col>
      <v-col cols="12">
        <div id="sigma-container" class="full-height" style="height: 800px">
          <div id="sigma"></div>
          <div id="sigma-detail"></div>
          <v-card
            color="transparent"
            style="position: absolute; top: 80px; left: 20px; z-index: 1000"
            v-if="selectedNode.vertex && false"
          >
            <vue-json-pretty :data="selectedNode" style="width: 500px"></vue-json-pretty>
          </v-card>
        </div>
      </v-col>
      <v-overlay absolute :value="overlay">
        <v-progress-circular indeterminate size="64"></v-progress-circular>
      </v-overlay>
    </v-row>
  </v-sheet>
</template>
  
  <script>
import Sigma from "sigma"
import { aql, Database } from "arangojs"
import Graph from "graphology"
import { circular } from "graphology-layout"
import FA2Layout from "graphology-layout-forceatlas2/worker"
import forceAtlas2 from "graphology-layout-forceatlas2"
import VueJsonPretty from "vue-json-pretty"
import "vue-json-pretty/lib/styles.css"
import query from "../presets/aql/all_entities"

export default {
  name: "Graph",
  components: {
    VueJsonPretty
  },
  data: function() {
    return {
      // TODO: serverside proxy to hide non-test credentials
      arango: {
        url: "https://graph-db.software.bz.it/",
        user: "dashboard",
        pass: "dashboard",
        draggedNode: null,
        isDragging: false,
        database: {
          spdx: "fulldata_test",
          banger: "test-database"
        },
        graph: {
          spdx: "test",
          banger: "banger-view"
        }
      },
      db: {
        a4f: false,
        compliance: false
      },
      filter_alias: {
        local_binfile: "Current package",
        local_srcfile: "Dig deeper into sources..."
      },
      selectedNode: {},
      graphstate: {},
      selections: {},
      filter: null,
      current: null,
      overlay: true,
      currentBinNode: {},
      token: "",
      graph: {
        sigma: false,
        "sigma-detail": false
      },
      renderer: {
        sigma: false,
        "sigma-detail": false
      },
      sigma: false,
      vertexes: {},
      detail_vertexes: {},
      edges: {},
      done: [],
      options: {
        drawImplicitNodes: false,
        drawIncludes: false
      },
      // fake it till you make it
      architectures: {
        items: ["qemu-system-x86_64"],
        current: "qemu-system-x86_64"
      },
      colors: {
        startnode: "#00edfc",
        //binary: "#fcef5e",
        sourcefile: "#6714b0",
        upstrfile: "#fcef5e",
        //patch: "#00FF00",
        //include: "#90ce44",
        elf: "#ff8800",
        //license: "#DDDDDD",
        //package: "#af6e3d",
        implicit: "#EEEEEE"
      },
      color_alias: {
        startnode: "Start vertices (local_binfile)",
        sourcefile: "Local Sourcefiles",
        upstrfile: "Upstream Sources",
        patch: "",
        elf: "Image elf data",
        implicit: "Implicit nodes"
      },
      startVertex: "/usr/lib/libcurl.so.4.7.0"
    }
  },
  created: async function() {
    this.db.a4f = new Database({
      url: this.arango.url,
      databaseName: this.arango.database.spdx,
      auth: { username: this.arango.user, password: this.arango.pass }
    })

    this.db.compliance = new Database({
      url: this.arango.url,
      databaseName: this.arango.database.banger,
      auth: { username: this.arango.user, password: this.arango.pass }
    })

    this.graph.sigma = new Graph()

    // get full bang elf view
    await this.fetchGraph(this.db.compliance, this.arango.graph.banger)

    // get all available local_binfiles
    await this.fetchGraph(this.db.a4f, this.arango.graph.spdx, false, "local_binfile")

    this.selections["local_binfile"].current = this.startVertex

    this.changePackageView()
  },
  methods: {
    resetGraph: function() {
      this.resetHoverNode()
      this.edges["binary_matches"] = []
      this.detail_vertexes = {}
    },
    changePackageView: async function() {
      this.resetHoverNode()

      this.overlay = true
      this.vertexes["local_srcfile"] = []
      this.vertexes["upstr_srcfile"] = []

      this.edges["generated_from"] = []
      this.edges["copy_of"] = []
      this.edges["modified_from"] = []
      this.edges["patch_applied_to"] = []
      this.edges["patch_for"] = []

      if (!this.selections["local_binfile"].current) this.selections["local_binfile"].current = ":"

      let currentPath = this.selections["local_binfile"].current.split(":")
      currentPath = currentPath[currentPath.length - 1]

      this.startVertex = currentPath
      await this.getAlienPackageData(this.db.a4f, currentPath)
      this.draw()
      this.overlay = false
    },
    async fetchGraph(db, name = "test", stream = false, filter = false) {
      const graph = db.graph(name)

      if (stream) {
        // implement collection _cursor / pagination if graph grows
      } else {
        try {
          const graphEdgeCollections = await graph.edgeCollections()
          for (const collection of graphEdgeCollections) {
            if (filter && collection.name != filter) continue
            const coll = await this.fetchCollection(db, collection, true)
          }

          const graphVertexCollections = await graph.vertexCollections()
          for (const collection of graphVertexCollections) {
            if (filter && collection.name != filter) continue
            const coll = await this.fetchCollection(db, collection)
          }
        } catch (e) {
          console.error(e)
        }
      }
    },
    async fetchCollection(db = this.db, collection, edges = false) {
      try {
        const entries = await db.query(aql`
                                FOR e IN ${collection} LIMIT 99999
                                RETURN e 
                                `)

        for await (const entry of entries) {
          if (!edges) {
            if (typeof this.vertexes[collection.name] == "undefined")
              this.vertexes[collection.name] = []
            this.vertexes[collection.name].push(entry)

            if (typeof this.selections[collection.name] == "undefined")
              this.selections[collection.name] = { items: [], current: null }
            this.selections[collection.name].items.push(entry["label"])
            this.selections[collection.name].label = collection.name
          } else {
            if (typeof this.edges[collection.name] == "undefined") this.edges[collection.name] = []
            this.edges[collection.name].push(entry)
          }
        }
      } catch (err) {
        console.error(err.message)
      }
    },
    async getAlienPackageData(db = this.db, path) {
      try {
        const entries = await db.query(aql`
        LET localbin = first(
          FOR v IN local_binfile
            FILTER v.path == ${path}
            RETURN v
          )

          LET path = (
            FOR v, e, p IN 1..3 OUTBOUND localbin copy_of, generated_from, modified_from, patch_for, INBOUND patch_applied_to
              RETURN p
          )

          let vertices = first(return unique(flatten(path[**].vertices)))
          let edges = first(return unique(flatten(path[**].edges)))

          RETURN { "vertices": vertices, "edges": edges }
                                `)
        for await (const entry of entries) {
          for (const vertex of entry.vertices) {
            var type = vertex._id.split("/")[0]
            if (typeof this.vertexes[type] == "undefined") this.vertexes[type] = []
            this.vertexes[type].push(vertex)
          }
          for (const edge of entry.edges) {
            var type = edge._id.split("/")[0]
            if (typeof this.edges[type] == "undefined") this.edges[type] = []
            this.edges[type].push(edge)
          }
        }
      } catch (err) {
        console.error(err.message)
      }
    },
    drawEdges: function(graph, collection, fromtype, totype, deactivate = true, label = "label") {
      this.edges[collection].forEach(line => {
        if (!graph.hasNode(line._from) && this.options.drawImplicitNodes)
          graph.addNode(line._from, {
            nodeType: deactivate ? "implicit" : fromtype,
            label: line[label],
            x: 0,
            y: 0
          })

        if (!graph.hasNode(line._to) && this.options.drawImplicitNodes)
          graph.addNode(line._to, {
            nodeType: deactivate ? "implicit" : totype,
            label: line[label],
            x: 0,
            y: 0
          })

        if (
          !graph.hasEdge(line._from, line._to) &&
          graph.hasNode(line._from) &&
          graph.hasNode(line._to)
        )
          graph.addEdge(line._from, line._to, { weight: 1, label: collection })
      })
    },
    matchBangerBinary: function(needle) {
      if (typeof this.edges["binary_matches"] == "undefined") this.edges["binary_matches"] = []

      var res = this.vertexes["elf"].filter(o => o.sha1.indexOf(needle["sha1"]) != -1)

      if (res.length > 0) {
        this.edges["binary_matches"].push({
          _from: needle["_id"],
          _to: res[0]["_id"]
        })
      }
    },
    upstreamVertexes: function(needle) {
      if (typeof this.edges["binary_matches"] == "undefined") this.edges["binary_matches"] = []

      var res = this.vertexes["elf"].filter(o => o.sha1.indexOf(needle["sha1"]) != -1)

      if (res.length > 0) {
        this.edges["binary_matches"].push({
          _from: needle["_id"],
          _to: res[0]["_id"]
        })
      }
    },
    drawVertices: function(graph, name, type, label) {
      var vertices =
        Object.keys(this.detail_vertexes).length > 0 ? this.detail_vertexes : this.vertexes

      if (vertices[name])
        vertices[name].forEach(vertex => {
          // TBD, just as showcase, matching between bang binary scan and a4f package analysis (?)

          if (name == "local_binfile") {
            this.matchBangerBinary(vertex)
          }

          var mylabel = !vertex[label] ? vertex["label"] : vertex[label]

          if (!graph.hasNode(vertex._id)) {
            graph.addNode(vertex._id, {
              nodeType: type,
              collection: name,
              vertex: vertex,
              // TODO: banger should shorten the paths
              label:
                mylabel.indexOf(
                  "/oniro-image-base-raspberrypi4-64.wic.gz-0x00000000-gzip-1/oniro-image-base-raspberrypi4-64.wic-0x20400000-squashfs-1"
                ) != -1
                  ? mylabel.replace(
                      "/oniro-image-base-raspberrypi4-64.wic.gz-0x00000000-gzip-1/oniro-image-base-raspberrypi4-64.wic-0x20400000-squashfs-1",
                      ""
                    )
                  : mylabel
            })
          }
        })
    },
    draw: async function(containerId = "sigma", styles = "width:100%;height:800px") {
      if (!this.renderer[containerId]) {
        this.refreshGraph(containerId, styles)
      } else {
        this.renderer[containerId].graph.clear()
      }

      this.overlay = true

      this.graph[containerId] = new Graph()

      this.drawVertices(this.graph[containerId], "local_binfile", "startnode", "label")

      // spdx data
      this.drawVertices(this.graph[containerId], "local_srcfile", "sourcefile", "label")
      this.drawVertices(this.graph[containerId], "upstr_srcfile", "upstrfile", "label")
      this.drawEdges(this.graph[containerId], "generated_from", "binary", "sourcefile")
      this.drawEdges(this.graph[containerId], "patch_for", "patch", "sourcefile")
      this.drawEdges(this.graph[containerId], "patch_applied_to", "patch", "sourcefile")
      this.drawEdges(this.graph[containerId], "modified_from", "sourcefile", "upstrfile")
      this.drawEdges(this.graph[containerId], "copy_of", "sourcefile", "upstrfile")
      if (this.options.drawIncludes)
        this.drawEdges(this.graph[containerId], "includes", "sourcefile", "sourcefile")

      // banger data
      this.drawVertices(this.graph[containerId], "elf", "elf", "name")
      this.drawEdges(this.graph[containerId], "linkswith", "elf", "elf")

      // calculated/mapped edges data: can be easily saved to arangodb...
      this.drawEdges(this.graph[containerId], "binary_matches", "elf", "binary")

      this.graph[containerId].forEachNode((node, attributes) =>
        this.graph[containerId].setNodeAttribute(node, "color", this.colors[attributes.nodeType])
      )

      circular.assign(this.graph[containerId], { scale: 100 })

      const settings = forceAtlas2.inferSettings(this.graph[containerId])
      forceAtlas2.assign(this.graph[containerId], { settings, iterations: 150 })

      /*

      const fa2Layout = new FA2Layout(this.graph[containerId], {
        settings: settings
      })

      fa2Layout.start()

      */

      let sourcepackage_clusters = {}
      let sourcepackage_index = 0
      let sourcefile_grid_index = 0

      const selection_atts = this.currentBinNode
      sourcefile_grid_index = selection_atts.x || 0

      this.graph[containerId].forEachNode(node => {
        const atts = this.graph[containerId].getNodeAttributes(node)
        let size = 5

        if (atts["label"] && atts["label"].indexOf(".patch") != -1) {
          this.graph[containerId].setNodeAttribute(node, "patch", true)
        }

        if (atts["collection"] && atts["collection"] == "local_binfile") {
          size = size + 5
        }

        this.graph[containerId].setNodeAttribute(node, "size", size)

        // trivial clustering
        if (containerId == "sigma") {
          // move own upstream sources to the left, and others to the right
          if (
            atts.vertex.upstr_srcpkg &&
            atts.vertex.upstr_srcpkg.indexOf("git://git.openembedded.org/") != -1
          ) {
            this.graph[containerId].setNodeAttribute(node, "y", atts.y - 0)
          }

          // move all upstream sources a level higher
          if (atts.collection == "upstr_srcfile") {
            this.graph[containerId].setNodeAttribute(node, "y", atts.y + 1000)

            if (typeof sourcepackage_clusters[atts.vertex.upstr_srcpkg] == "undefined") {
              sourcepackage_clusters[atts.vertex.upstr_srcpkg] = atts.y + sourcepackage_index * 500
              sourcepackage_index++
            }

            this.graph[containerId].setNodeAttribute(
              node,
              "y",
              sourcepackage_clusters[atts.vertex.upstr_srcpkg]
            )
          }

          if (atts.collection == "elf") {
            this.graph[containerId].setNodeAttribute(node, "y", atts.y - 1000)
          }

          if (atts.collection == "local_srcfile") {
            this.graph[containerId].setNodeAttribute(node, "y", atts.y + 500)
          }
        }
      })

      if (containerId == "sigma") {
        let hindex = 0

        let sourcepackage_clusters = {}

        this.graph[containerId].forEachNode(node => {
          const atts = this.graph[containerId].getNodeAttributes(node)

          if (atts.collection == "upstr_srcfile") {
            if (typeof sourcepackage_clusters[atts.vertex.upstr_srcpkg] == "undefined") {
              sourcepackage_clusters[atts.vertex.upstr_srcpkg] = sourcefile_grid_index
            } else {
              sourcepackage_clusters[atts.vertex.upstr_srcpkg] += 30
            }

            // we assume 1:1
            const neighbors = this.graph[containerId].neighbors(node)
            neighbors.forEach(val => {
              const vatts = this.graph[containerId].getNodeAttributes(val)
              this.graph[containerId].setNodeAttribute(
                val,
                "x",
                sourcepackage_clusters[atts.vertex.upstr_srcpkg]
              )
              this.graph[containerId].setNodeAttribute(val, "y", atts.y - 250)
              // this.graph[containerId].setNodeAttribute(node, "y", atts.y + 500)
              this.graph[containerId].setNodeAttribute(
                node,
                "x",
                sourcepackage_clusters[atts.vertex.upstr_srcpkg]
              )
              // this.graph[containerId].setNodeAttribute(node, "y", vatts.y - hindex)
              hindex += 2
            })
          }
        })
      }

      const container = document.getElementById(containerId)

      // only update graph if renderer exists
      if (this.renderer[containerId]) {
        this.overlay = false
        this.renderer[containerId].graph = this.graph[containerId]
        this.renderer[containerId].refresh()
        return
      }

      // else create renderers
      let renderer = new Sigma(this.graph[containerId], container, {
        labelColor: { color: "#888" },
        labelRenderer: function(context, data, settings) {
          if (!data.label) return
          var size = settings.labelSize,
            font = settings.labelFont,
            weight = settings.labelWeight,
            color = settings.labelColor.attribute
              ? data[settings.labelColor.attribute] || settings.labelColor.color || "#000"
              : settings.labelColor.color
          context.fillStyle = color
          context.font = ""
            .concat(weight, " ")
            .concat(size, "px ")
            .concat(font)

          if (data.vertex && data.vertex.license && data.vertex.license != "") {
            context.fillText(data.vertex.license, data.x + data.size + 3, data.y + size / 3 - 20)
          }

          var splitted = data.label.split(":")

          if (data.vertex && data.vertex.upstr_srcpkg && data.vertex.upstr_srcpkg != "") {
            context.fillText(
              data.vertex.upstr_srcpkg,
              data.x + data.size + 3,
              data.y + size / 3 + 20
            )
          }

          context.fillText(splitted[splitted.length - 1], data.x + data.size + 3, data.y + size / 3)
        },
        renderEdgeLabels: true,
        allowInvalidContainer: true,
        defaultEdgeType: "arrow"
      })

      if (containerId == "sigma") {
        renderer.on("clickNode", ({ node }) => {
          this.setHoveredNode(node)
        })

        renderer.setSetting("nodeReducer", (node, data) => {
          const res = { ...data }

          if (this.graphstate.hoveredNode == node && res.nodeType == "sourcefile") {
            res.color = "#ff0000"
          }
          if (res.nodeType == "startnode" && res.vertex.path != this.startVertex) {
            res.hidden = true
          } else if (res.nodeType == "startnode") {
            this.currentBinNode = JSON.parse(JSON.stringify(res))
          }

          if (
            this.graphstate.hoveredNeighbors &&
            !this.graphstate.hoveredNeighbors.has(node) &&
            this.graphstate.hoveredNode !== node
          ) {
            res.label = ""
            res.color = "#f6f6f6"
          }

          return res
        })

        renderer.setSetting("edgeReducer", (edge, data) => {
          const res = { ...data }
          return res
        })

        renderer.on("downNode", e => {
          this.isDragging = true
          this.draggedNode = e.node
          this.graph[containerId].setNodeAttribute(this.draggedNode, "highlighted", true)
        })

        renderer.getMouseCaptor().on("mousemovebody", e => {
          if (!this.isDragging || !this.draggedNode) return

          const pos = renderer.viewportToGraph(e)

          this.graph[containerId].setNodeAttribute(this.draggedNode, "x", pos.x)
          this.graph[containerId].setNodeAttribute(this.draggedNode, "y", pos.y)

          e.preventSigmaDefault()
          e.original.preventDefault()
          e.original.stopPropagation()
        })

        renderer.getMouseCaptor().on("mouseup", () => {
          if (this.draggedNode) {
            this.graph[containerId].removeNodeAttribute(this.draggedNode, "highlighted")
          }
          this.isDragging = false
          this.draggedNode = null
        })

        renderer.getMouseCaptor().on("mousedown", () => {
          if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox())
        })
      } else if (containerId == "sigma-detail") {
        renderer.setSetting("nodeReducer", (node, data) => {
          const res = { ...data }

          if (this.graphstate.hoveredNode == node && res.nodeType == "sourcefile") {
            res.color = "#ff0000"
          }

          return res
        })
      }

      this.renderer[containerId] = renderer

      this.overlay = false
    },
    setHoveredNode(node, fixed = true) {
      this.overlay = true
      if (node) {
        const atts = this.graph["sigma"].getNodeAttributes(node)
        this.graphstate.hoveredNode = node
        this.done = []
        this.graphstate.hoveredNeighbors = new Set(this.getRelevantNeighbors(node))
        this.selectedNode = atts
        if (atts.collection == "local_srcfile" || atts.collection == "local_binfile") {
          if (typeof this.selections["local_srcfile"] == "undefined")
            this.selections["local_srcfile"] = {}
          this.selections["local_srcfile"].current = [atts.label]
          this.inspectSourcefile()
        }
      } else if (!fixed) {
        this.graphstate.hoveredNode = undefined
        this.graphstate.hoveredNeighbors = undefined
      }

      // Refresh rendering:
      this.renderer["sigma"].refresh()
      this.overlay = false
    },
    resetHoverNode() {
      this.graphstate.hoveredNode = undefined
      this.graphstate.hoveredNeighbors = undefined
      this.graphstate.suggestions = undefined
      if (this.renderer["sigma"]) this.renderer["sigma"].refresh()
    },
    refreshGraph(containerId = "sigma", styles = "height: 800px; width: 100%") {
      var g = document.querySelector("#" + containerId)
      var p = g.parentNode
      p.removeChild(g)
      var c = document.createElement("div")
      c.setAttribute("id", containerId)
      c.setAttribute("style", styles)
      p.appendChild(c)
    },
    getRelevantNeighbors(node, depth = 1) {
      var isPatch = this.graph["sigma"].getNodeAttribute(node, "patch")
      var type = this.graph["sigma"].getNodeAttribute(node, "nodeType")
      var vertex = this.graph["sigma"].getNodeAttribute(node, "vertex")

      if (vertex.path == "/usr/lib/libc.so") {
        return []
      }

      var dep = 3
      if (type == "elf" || type == "startnode") dep = 2
      if (type == "startnode") depth = 1
      if (depth > dep && !isPatch) return []

      this.done.push(node)

      var neighbors = this.graph["sigma"].neighbors(node)
      neighbors.forEach(val => {
        if (this.done.includes(val)) return
        neighbors = [...neighbors, ...new Set(this.getRelevantNeighbors(val, depth + 1))]
      })

      return neighbors
    },
    inspectSourcefile() {
      this.overlay = true
      this.detail_vertexes = {}
      if (this.selections["local_srcfile"].current)
        this.selections["local_srcfile"].current.forEach(obj => {
          var needle = obj

          if (needle.length == 0) return

          var node = this.graph["sigma"]
            .nodes()
            .map(n => ({ id: n, label: this.graph["sigma"].getNodeAttribute(n, "label") }))
            .filter(({ label }) => label.toLowerCase().includes(needle.toLowerCase()))

          if (node.length > 0) {
            this.done = []
            this.graphstate.hoveredNeighbors = new Set(this.getRelevantNeighbors(node[0].id))

            this.graphstate.hoveredNeighbors.forEach(node => {
              var nodeinfo = this.graph["sigma"].getNodeAttribute(node, "collection")
              if (typeof this.detail_vertexes[nodeinfo] == "undefined")
                this.detail_vertexes[nodeinfo] = []
              this.detail_vertexes[nodeinfo].push({
                ...this.graph["sigma"].getNodeAttributes(node),
                _id: node,
                x: 0,
                y: 0
              })
            })

            this.draw(
              "sigma-detail",
              "position:absolute;bottom:10px;left:10px;background:#FFFFFF;width:600px;height:400px;border:1px solid black"
            )

            this.detail_vertexes = {}
          }
        })
      this.overlay = false
    }
  }
}
</script>