Forked from
Eclipse Projects / Eclipse Oniro Compliance Toolchain / toolchain / dashboard
17 commits behind, 7 commits ahead of the upstream repository.
-
Alex Complojer authored
new arango endpoint
Alex Complojer authorednew arango endpoint
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>