From 8436dcee81b38ee6e45a6112dedcb9738f31eae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Tue, 9 Jun 2020 16:40:03 -0700 Subject: [PATCH] Add nodes and edges to CredView (#1839) This adds support for retrieving cred-augmented nodes and edges to the CredView class. These methods wrap the underlying Graph methods, and return nodes and edges that also include relevant cred information Test plan: Unit tests included; yarn test passes. --- src/analysis/credView.js | 122 +++++++++++++++++++++++++++++++++- src/analysis/credView.test.js | 99 +++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) diff --git a/src/analysis/credView.js b/src/analysis/credView.js index 1852bee..4d2fd45 100644 --- a/src/analysis/credView.js +++ b/src/analysis/credView.js @@ -6,6 +6,58 @@ import {type TimelineCredParameters} from "./timeline/params"; import {Graph} from "../core/graph"; import {type PluginDeclarations} from "./pluginDeclaration"; +import {get as nullGet} from "../util/null"; +import {type EdgeWeight} from "../core/weights"; +import type {TimestampMs} from "../util/timestamp"; +import { + type NodeWeightEvaluator, + nodeWeightEvaluator, + type EdgeWeightEvaluator, + edgeWeightEvaluator, +} from "../core/algorithm/weightEvaluator"; +import { + type NodeAddressT, + type EdgeAddressT, + type Node as GraphNode, + type Edge as GraphEdge, +} from "../core/graph"; +import type { + NodeCredSummary, + NodeCredOverTime, + EdgeCredSummary, + EdgeCredOverTime, +} from "./credData"; + +export type CredNode = {| + +address: NodeAddressT, + +description: string, + +minted: number, + +timestamp: TimestampMs | null, + +credSummary: NodeCredSummary, + +credOverTime: NodeCredOverTime | null, +|}; + +export type CredEdge = {| + +address: EdgeAddressT, + +src: CredNode, + +dst: CredNode, + +rawWeight: EdgeWeight, + +credSummary: EdgeCredSummary, + +credOverTime: EdgeCredOverTime | null, + +timestamp: TimestampMs, +|}; + +export type EdgesOptions = {| + // An edge address prefix. Only show edges whose addresses match this prefix. + +addressPrefix?: EdgeAddressT, + // A node address prefix. Only show edges whose src matches + // this prefix. + +srcPrefix?: NodeAddressT, + // A node address prefix. Only show edges whose dst matches + // this prefix. + +dstPrefix?: NodeAddressT, +|}; + /** * The CredView is an interface for Graph-aware queries over a CredResult. * @@ -14,10 +66,21 @@ import {type PluginDeclarations} from "./pluginDeclaration"; * such queries convenient. */ export class CredView { - _credResult: CredResult; + +_credResult: CredResult; + +_nodeAddressToIndex: Map; + +_edgeAddressToIndex: Map; + +_nodeEvaluator: NodeWeightEvaluator; + +_edgeEvaluator: EdgeWeightEvaluator; constructor(result: CredResult) { this._credResult = result; + const {weights, graph} = result.weightedGraph; + const nodes = Array.from(graph.nodes()); + const edges = Array.from(graph.edges({showDangling: false})); + this._nodeAddressToIndex = new Map(nodes.map((n, i) => [n.address, i])); + this._edgeAddressToIndex = new Map(edges.map((n, i) => [n.address, i])); + this._nodeEvaluator = nodeWeightEvaluator(weights); + this._edgeEvaluator = edgeWeightEvaluator(weights); } graph(): Graph { @@ -39,4 +102,61 @@ export class CredView { credResult(): CredResult { return this._credResult; } + + _promoteNode(n: GraphNode): CredNode { + const idx = nullGet(this._nodeAddressToIndex.get(n.address)); + const credSummary = this._credResult.credData.nodeSummaries[idx]; + const credOverTime = this._credResult.credData.nodeOverTime[idx]; + const minted = this._nodeEvaluator(n.address); + return { + timestamp: n.timestampMs, + description: n.description, + address: n.address, + credSummary, + credOverTime, + minted, + }; + } + node(a: NodeAddressT): ?CredNode { + const graphNode = this.graph().node(a); + if (graphNode == null) { + return undefined; + } + return this._promoteNode(graphNode); + } + nodes(options?: {|+prefix: NodeAddressT|}): $ReadOnlyArray { + const graphNodes = Array.from(this.graph().nodes(options)); + return graphNodes.map((x) => this._promoteNode(x)); + } + + _promoteEdge(e: GraphEdge): CredEdge { + const idx = nullGet(this._edgeAddressToIndex.get(e.address)); + const srcNode = nullGet(this.graph().node(e.src)); + const dstNode = nullGet(this.graph().node(e.dst)); + const credSummary = this._credResult.credData.edgeSummaries[idx]; + const credOverTime = this._credResult.credData.edgeOverTime[idx]; + const rawWeight = this._edgeEvaluator(e.address); + return { + timestamp: e.timestampMs, + address: e.address, + src: this._promoteNode(srcNode), + dst: this._promoteNode(dstNode), + credSummary, + credOverTime, + rawWeight, + }; + } + edge(a: EdgeAddressT): ?CredEdge { + const graphEdge = this.graph().edge(a); + if (graphEdge == null || this.graph().isDanglingEdge(a)) { + return undefined; + } + return this._promoteEdge(graphEdge); + } + edges(options?: EdgesOptions): $ReadOnlyArray { + const graphEdges = Array.from( + this.graph().edges({...options, showDangling: false}) + ); + return graphEdges.map((x) => this._promoteEdge(x)); + } } diff --git a/src/analysis/credView.test.js b/src/analysis/credView.test.js index f5a7e1f..3dbbdac 100644 --- a/src/analysis/credView.test.js +++ b/src/analysis/credView.test.js @@ -1,6 +1,7 @@ // @flow import {Graph, NodeAddress, EdgeAddress} from "../core/graph"; +import {get as nullGet} from "../util/null"; import type {NodeType, EdgeType} from "./types"; import { type PluginDeclaration, @@ -141,4 +142,102 @@ describe("analysis/credView", () => { expect(credView.plugins()).toEqual([declaration]); }); }); + + describe("nodes", () => { + it("can retrieve a CredNode", async () => { + const {credView, foo1, graph, credResult} = await example(); + const { + address, + credOverTime, + credSummary, + description, + minted, + timestamp, + } = nullGet(credView.node(foo1.address)); + expect({address, description, timestampMs: timestamp}).toEqual(foo1); + expect(minted).toEqual(2); + const nodeOrder = Array.from(graph.nodes()).map((x) => x.address); + const index = nodeOrder.findIndex((x) => x === foo1.address); + expect(credOverTime).toEqual(credResult.credData.nodeOverTime[index]); + expect(credSummary).toEqual(credResult.credData.nodeSummaries[index]); + }); + it("returns undefined for non-existent node", async () => { + const {credView} = await example(); + expect(credView.node(NodeAddress.fromParts(["nope"]))).toBe(undefined); + }); + it("returns array of all nodes when no arguments provided", async () => { + const {credView, foo1, foo2, user} = await example(); + expect(credView.nodes()).toEqual( + [foo1, foo2, user].map((x) => credView.node(x.address)) + ); + }); + it("nodes can filter by prefix", async () => { + const {credView, foo1, foo2, fooType} = await example(); + expect(credView.nodes({prefix: fooType.prefix})).toEqual( + [foo1, foo2].map((x) => credView.node(x.address)) + ); + }); + }); + describe("edges", () => { + it("can retrieve a CredEdge", async () => { + const {credView, flow1, credResult, graph} = await example(); + const { + address, + src, + dst, + credOverTime, + credSummary, + rawWeight, + timestamp, + } = nullGet(credView.edge(flow1.address)); + expect({ + address, + src: src.address, + dst: dst.address, + timestampMs: timestamp, + }).toEqual(flow1); + const edgeOrder = Array.from(graph.edges({showDangling: false})).map( + (x) => x.address + ); + const index = edgeOrder.findIndex((x) => x === flow1.address); + expect(src).toEqual(credView.node(src.address)); + expect(dst).toEqual(credView.node(dst.address)); + expect(rawWeight).toEqual({forwards: 2, backwards: 3}); + expect(credOverTime).toEqual(credResult.credData.edgeOverTime[index]); + expect(credSummary).toEqual(credResult.credData.edgeSummaries[index]); + }); + it("returns undefined for non-existent edge", async () => { + const {credView} = await example(); + expect(credView.edge(EdgeAddress.fromParts(["nope"]))).toBe(undefined); + }); + it("returns undefined for a dangling edge", async () => { + const {credView, dangling} = await example(); + expect(credView.edge(dangling.address)).toBe(undefined); + }); + it("returns array of all non-dangling edges when no arguments provided", async () => { + const {credView, flow1, flow2, stream1} = await example(); + expect(credView.edges()).toEqual( + [flow1, flow2, stream1].map((x) => credView.edge(x.address)) + ); + }); + it("edges can filter by address prefix", async () => { + const {credView, flow1, flow2, flowType} = await example(); + expect(credView.edges({addressPrefix: flowType.prefix})).toEqual( + [flow1, flow2].map((x) => credView.edge(x.address)) + ); + }); + it("edges can filter by src prefix", async () => { + const {credView, stream1, userType} = await example(); + expect(credView.edges({srcPrefix: userType.prefix})).toEqual([ + credView.edge(stream1.address), + ]); + }); + it("edges can filter by dst prefix", async () => { + const {credView, flow1, flow2, userType} = await example(); + expect(credView.edges({dstPrefix: userType.prefix})).toEqual([ + credView.edge(flow1.address), + credView.edge(flow2.address), + ]); + }); + }); });