Graphs

Understand comes with many built-in graphs such as call trees and control flow graphs. Graphs can also be defined through plugins, like the butterfly graph. Users can export graphs from the Python API.

Discovery

Available graphs depend on the target: a Db, an Ent, or an Arch. For example, the “Control Flow” graph is typically available for function entities whereas a “UML Class Diagram” is available for class entities. The “Architecture Graph” is only available for architectures.

Use Graph.list to discover available graphs. The Graph objects returned can be used to query information about the graph such as the name and variant. Note that both the name and variant are necessary to uniquely identify a graph.

Graphs support options. Use Graph.options followed by Options.list to discover available options.

List catalog graphs for the open project (pass a Db so availability matches the database):

import understand

db = understand.open("/path/to/myproject.und")
for g in understand.Graph.list(db):
    print(g.name(), "-", g.variant())

Generation

Call db.draw(), ent.draw(), or arch.draw() to generate graphs for projects, entities, and architectures respectively.

The graph and variant parameters are used to identify the graph to generate. If graph is an Graph object, both values are determined from the Graph object. If graph is a string with multiple variants and no variant is specified, a default variant is chosen.

The draw command can save the graph to disk, return the graph as bytes or return a read-only GraphContext object. Use filename to save to disk (returning None) or format to return the graph. The filename and format parameters are mutually exclusive. When using filename, the format is determined by the extension. Supported extensions and format arguments are jpg, png, svg, vsdx, vdx, and dot. Additionally, format can be "raw" to return a GraphContext instead of bytes. Note that while all graphs technically have dot output, not all dot files are meant to be used with Graphviz. Graphs using custom Understand layout algorithms will have custom attributes in the dot files to support them.

Graphs can have options. See the understand.Options class for how to format the options argument. For Relationship graphs use secondent=EntityUniqueName to indicate the second entity.

If an error occurs, an UnderstandError will be raised. Some possible errors are:

  • Draw Too Large - failed to allocate memory for the image

  • Unknown Graph - the graph name did not match a known graph

  • Unsupported File Type - unsupported file format

Save a graph to disk by passing a filename (extension selects the format):

import understand

db = understand.open("/path/to/myproject.und")
fn = next(db.ents("function ~unresolved ~unknown"))
fn.draw("Called By", f"callby_{fn.name()}.png")

Return a graph in memory with format (mutually exclusive with filename). Use format="raw" for a read-only GraphContext. Pass a Graph from Graph.list so the name and variant match the catalog:

import understand

db = understand.open("/path/to/myproject.und")
g = understand.Graph.list(db)[0]
png = db.draw(g, format="png")
ctx = db.draw(g, format="raw")

Plugin writing

A graph plugin must have a name function and a draw function. If the name is used by other graphs, the style function should also be defined to provide the variant name. The draw function creates the graph. At least one test_ function should be defined to indicate when the graph is available.

The parameter graph passed to init and draw is an GraphContext object. It represents the root of the graph. Use the cluster, node, and edge methods to add clusters, nodes, and edges to the graph. Each of those objects has a set method to change the Graphviz attributes. Default attributes can be set on the root graph context with the default method. Nodes and edges can also be synced to entities, references or file locations.

Use options to access an Options object. The options object can be used to define and retrieve custom options.

You can also provide a legend for your graph using legend to obtain a Legend object. Use define to create entries. Use set to update existing entries.

The following sample script is a call tree. It can be run as a project level graph which graphs all functions in a database, or as an entity level graph showing the call tree from the initial entity.

import understand

def name():
  """
  Required, the name of the graph.
  """
  return "Calls"

def style():
  """
  Optional, the name as it appears in the graph variants drop down.

  This defaults to "Custom"
  """
  return "Python Template"

def test_global(db):
  """
  Optional, return True if this graph is a project level graph

  If True, this graph will appear in the top level Graph Menu.
  """
  return True

def test_entity(ent):
  """
  Optional, return True if the graph exists for the given entity

  If True, this graph will appear in the Graphical Views menu for the
  entity.
  """
  return ent.kind().check("function ~unknown ~unresolved");

def test_architecture(arch):
  """
  Optional, return True if the graph exists for the given architecture

  If True, this graph will appear in the Graphical Views menu for the
  architecture.
  """
  return True;

def init(graph, target):
  """
  Initialize the graph

  This function is called once on graph creation. Use it to define the
  available graph options and/or a legend.
  """
  # Define options through the options object
  graph.options().define("Fill", ["On","Off"], "Off");

  # Use isinstance on the target to see if it's an entity, architecture, or
  # project level graph (if your plugin supports multiple). You can use the
  # target to customize available options.
  if isinstance(target, understand.Ent):
    graph.options().define("Depth", ["1", "2", "3"], "3")

  # Defining a legend is optional. You can add multiple entries to the legend.
  graph.legend().define("func", "roundedrect", "Function", "blue", "#FFFFFF")

def grabNode(graph, nodes, ent):
  """
  This is a custom function for this script to get a graphviz node
  """
  if ent in nodes:
    node = nodes[ent]
  else:
    # passing an ent to the node object will automatically sync the entity.
    node = graph.node(ent.name(),ent)

    if ent.kind().check("unresolved"):
      # Set graph, node, and edge attributes with the set function.
      # See Graphviz documentation for available attributes.
      node.set("shape","octagon")
      node.set("color","gray")
      node.set("fillcolor","white")
    nodes[ent] = node

  return node


def draw(graph, target):
  """
  Draw the graph

  The second argument can be a database, architecture, or an entity depending
  which test functions return True.
  """

  fore = "blue"
  back = "#FFFFFF"

  # Use options to lookup the current values
  if graph.options().lookup("Fill") == "On":
    fore = "#000000"
    back = "blue"

  # Use set to update the legend outside of init
  graph.legend().set("func","fore",fore)
  graph.legend().set("func","back",back)

  # Use set to change graph, node, and edge attributes
  graph.set("rankdir", "LR")

  # Use default to set the default attributes for graphs, nodes, and edges
  graph.default("color", fore, "node")
  graph.default("fillcolor", back, "node")
  graph.default("style","filled,rounded","node")
  graph.default("shape","box","node")

  # store the ent->graphviz node so that each entity node appears only
  # once no matter how many calls to it there are.
  nodes = dict()

  # avoid visiting any node more than once
  visited = set()

  # If the graph can be more than one type, use isinstance to determine the type
  depth = 1
  curLevel = []
  if isinstance(target, understand.Db):
    # This sample graphs all functions in the database for a project level
    # graph
    curLevel = target.ents("function ~unknown ~unresolved")
  elif isinstance(target, understand.Arch):
    # Graph all functions in the architecture
    for ent in target.ents(False):
      if ent.kind().check("function ~unknown ~unresolved"):
        curLevel.append(ent)
      elif ent.kind().check("file"):
        for ref in ent.refs("define", "function ~unknown ~unresolved", True):
          curLevel.append(ref.ent())
    # Create clusters with cluster
    cluster = graph.cluster(target.name(), target)
    # add nodes to the cluster
    for ent in curLevel:
      grabNode(cluster, nodes, ent)
  else:
    # For an entity level graph, generate a calls tree
    curLevel.append(target)
    depth = int(graph.options().lookup("Depth"))

  # Loop over the levels of the tree
  while depth > 0:
    depth -= 1
    nextLevel = []
    for ent in curLevel:

      # avoid visiting nodes multiple times
      if ent in visited:
        continue
      visited.add(ent)

      # Get a graphviz node for the entity
      tail = grabNode(graph,nodes,ent)

      # Add edges for each call
      for ref in ent.refs("call",unique=True):
        headEnt = ref.ent()
        nextLevel.append(headEnt)
        head = grabNode(graph,nodes,headEnt)

        # create an edge
        edge = graph.edge(tail,head)
        # Use sync so that clicking on the edge will visit the reference
        edge.sync(ref)
    curLevel = nextLevel