Google Cloud Spanner and ReGraph: how to build a cybersecurity app

In this tutorial, I’ll show you how to build a cloud security application that integrates Google Cloud’s Spanner Graph with ReGraph, our graph visualization SDK. By leveraging these tools, I’ll showcase how you can achieve complete cloud asset visibility, get insight into security posture, and identify attack paths – critical components of any cybersecurity strategy.

To follow the demo, you need access to ReGraph. Sign up for a free trial if you’re not already using it.

A real-time cloud security dashboard built with ReGraph and Google Cloud’s Spanner Graph

Why cloud visibility matters

To build a solid cloud security strategy, you need a clear understanding of your cloud assets. You need to know where they are, how they’re connected, and what risks they face. Graphs are the best way to map out this information, because they expose threats, security posture, alerts, and attack paths in a way that’s easy to understand and act upon.

But simply visualizing the data can lead to high levels of visual complexity, which gets in the way of insight. ReGraph’s advanced capabilities – like combos, sequential layouts, and styling options – let you represent complex cloud architecture in a way that’s not only clear but meaningful and familiar to end users. This makes it easier to manage large-scale cloud infrastructure while maintaining a strong security posture.

This combination of Google Spanner Graph, ReGraph, and React is a powerful stack for building scalable, high-performance cloud security applications. And this tutorial demonstrates just how easily this stack can help to achieve insightful cloud asset visibility and robust security monitoring.

About Google Spanner Graph

Google Spanner Graph is a unified database solution that integrates graph, relational, search, and AI capabilities within Cloud Spanner, enabling enterprises to analyze complex, interconnected data at scale. By supporting the ISO Graph Query Language (GQL), it allows seamless pattern matching and relationship traversal, facilitating applications such as fraud detection, recommendation engines, and network security. This integration eliminates the need for separate graph databases, reducing data fragmentation and operational overhead, while leveraging Spanner’s scalability and consistency to provide real-time insights into connected data.

Getting started with Google Spanner

1. Initialize Google Spanner

To work with Spanner, you need to enable the API and create an instance. See Google Spanner documentation. If you’ve already set up Spanner, you can skip this part and move directly to creating the database.

2. Create the database and schema

    Using Spanner Studio:
  • Create a new PostgreSQL database.
  • Define a schema by creating a Graph table with three columns:
CREATE TABLE Graph (
    ID VARCHAR PRIMARY KEY,
    TYPE VARCHAR,
    ID1 VARCHAR,
    ID2 VARCHAR,
    SERVER VARCHAR,
    VPC VARCHAR,
    REGION VARCHAR,
    CLOUD VARCHAR
);

This table stores nodes and links representing your cloud architecture.

3. Insert data

Populate the table with cloud infrastructure nodes:

INSERT INTO
Graph (id, type, id1, id2, server, vpc, region, cloud)
VALUES
('internet', 'internet', '', '', '', '', '', ''),
('gcs-bucket-1a', 'gcs', '', '', 'demo-private-us-west-1a', 'demo-vpc', 'us-west', 'google-cloud'),
('gcs-bucket-2a', 'gcs', '', '', 'demo-private-us-west-1a', 'demo-vpc', 'us-west', 'google-cloud'),
('gcp-2b154e', 'gcp', '', '', 'demo-private-us-west-1a', 'demo-vpc', 'us-west', 'google-cloud'),
('gce-13cc45d', 'gce', '', '', 'demo-private-us-west-1a', 'demo-vpc', 'us-west', 'google-cloud'),
('customer-data-copy-1a', 'db', '', '', 'demo-private-us-west-1a', 'demo-vpc', 'us-west', 'google-cloud'),
('gcs-bucket-1f', 'gcs', '', '', 'demo-private-us-west-1f', 'demo-vpc', 'us-west', 'google-cloud'),
('gcs-bucket-2f', 'gcs', '', '', 'demo-private-us-west-1f', 'demo-vpc', 'us-west', 'google-cloud'),
('gcp-2b354e', 'gcp', '', '', 'demo-private-us-west-1f', 'demo-vpc', 'us-west', 'google-cloud'),
('gce-13cd45d', 'gce', '', '', 'demo-private-us-west-1f', 'demo-vpc', 'us-west', 'google-cloud'),
('customer-data-copy-1f', 'db', '', '', 'demo-private-us-west-1f', 'demo-vpc', 'us-west', 'google-cloud'),
('gateway-elb-1', 'gateway', '', '', '', 'demo-vpc', 'us-west', 'google-cloud'),
('gateway-elb-2', 'gateway', '', '', '', 'demo-vpc', 'us-west', 'google-cloud'),
('desktop-app-alb-1', 'gateway', '', '', '', 'demo-vpc', 'us-west', 'google-cloud')
RETURNING
id,
type,
id1,
id2,
server,
vpc,
region,
cloud;

Add links to connect the nodes:

INSERT INTO
Graph (id, type, id1, id2, server, vpc, region, cloud)
VALUES
('l1', '', 'gce-13cc45d', 'gcs-bucket-1a', '', '', '', ''),
('l2', '', 'gce-13cc45d', 'gcs-bucket-2a', '', '', '', ''),
('l3', '', 'gce-13cc45d', 'customer-data-copy-1a', '', '', '', ''),
('l4', '', 'gcp-2b154e', 'gce-13cc45d', '', '', '', ''),
('l5', '', 'gce-13cd45d', 'gcs-bucket-1f', '', '', '', ''),
('l6', '', 'gce-13cd45d', 'gcs-bucket-2f', '', '', '', ''),
('l7', '', 'gce-13cd45d', 'customer-data-copy-1f', '', '', '', ''),
('l8', '', 'gcp-2b354e', 'gce-13cd45d', '', '', '', ''),
('l9', '', 'internet', 'gateway-elb-1', '', '', '', ''),
('l10', '', 'internet', 'gateway-elb-2', '', '', '', ''),
('l11', '', 'gateway-elb-1', 'gcp-2b154e', '', '', '', ''),
('l12', '', 'gateway-elb-1', 'gcp-2b354e', '', '', '', ''),
('l13', '', 'gateway-elb-2', 'gcp-2b154e', '', '', '', ''),
('l14', '', 'gateway-elb-2', 'gcp-2b354e', '', '', '', '')
RETURNING
id,
type,
id1,
id2,
server,
vpc,
region,
cloud;

These links represent the data flow between cloud assets, such as GCE instances, Cloud functions, and Cloud storage.

Building a server with Node.js and Express

1. Set up a Node.js environment

  • Install the Google Cloud CLI (Instructions) and initialize authentication:
./google-cloud-sdk/install.sh
./google-cloud-sdk/bin/gcloud init
gcloud auth application-default login

Install the Cloud Spanner Client Library for Node.js: npm install –save @google-cloud/spanner

2. Build an Express.js server

Create a simple Express.js application to query Spanner:

Initialize a new repository and set up an Express server.

Authenticate using: gcloud auth application-default login

Use the following Node.js code to connect to Spanner, query the graph table, and return the results:

const express = require("express");
const { Spanner } = require("@google-cloud/spanner");
const cors = require("cors");
const app = express();


const port = 3000;
const projectId = "your-project-id";
const instanceId = "your-instance-id";
const databaseId = "your-database-id";


app.use(
   cors({
       origin: "http://localhost:5173",
       methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
       allowedHeaders: ["Content-Type", "Authorization"],
   })
);


// Creates a client
const spanner = new Spanner({ projectId });
// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);
// The query to execute
const query = {
   sql: 'SELECT id, type, id1, id2, server, vpc, region, cloud FROM "graph" LIMIT 50',
};
async function runSQL() {
   const [rows] = await database.run(query);
   return rows;
}


app.get("/", async (req, res) => {
   const data = await runSQL();
   res.send(data);
});


app.listen(port, () => {
   console.log(`Example app listening on port ${port}`);
});

    This script:
  • connects to the Spanner database.
  • executes a SQL query to fetch graph data.
  • sets up a basic API endpoint with Express.

Now run the Express server. It retrieves and displays data from Spanner, and validates that everything is set up correctly.

Integrating ReGraph for cloud asset visualization

Now that our server can retrieve data from Spanner, let’s integrate it into a ReGraph-powered front end for visualization.

1. Set up a ReGraph application

Follow the ReGraph documentation (you’ll need to request a trial to access this) to create a new React app that will serve as the front end for our cloud security visualization.

2. Fetch data from the express server

Create a services/api.js file to request data from the Express server:

const API_BASE_URL = "http://localhost:3000/";


export const fetchData = async () => {
   try {
       console.log("Starting fetch...");
       const response = await fetch(API_BASE_URL);
       console.log("Response status:", response.status);
       if (!response.ok) {
           console.log("HTTP error:", response.status);
           throw new Error("Network response was not ok");
       }
       return await response.json();
   } catch (error) {
       console.error("Error fetching data:", error);
       throw error;
   }
};

This function makes an API request, logs the response, and returns the retrieved data.

3. Handle the data

We need to parse the data we get from Spanner so that ReGraph can understand it and render it correctly. Create a data.js file and add this:

import { fetchData } from "./services/api.js";


export const COLORS = {
   ORANGE: "rgb(255,153,0)",
   BLUE: "rgb(20,110,180)",
   WHITE: "rgb(255,255,255)",
   BLACK: "rgb(0,0,0)",
   TRANSPARENT: "rgba(0,0,0,0)",
   DARK_GRAY: "rgb(35,47,62)",
   LIGHT_GRAY: "rgb(242,242,242)",
   COMBO: "rgba(242,242,242,0.1)",
   COMBO_LABEL: "rgba(242,242,242,0.1)",
   ALERT: "rgb(253,69,75)",
   NON_ALERT: "rgb(34, 68, 102)",
   INTERNET: "rgb(116,116,116)",
   GATEWAY: "rgb(140, 79, 255)",
   DB: "rgb(201, 37, 209)",
   GCE: "rgb(237, 113, 0)",
   GCP: "rgb(237,113,0)",
   LB: "rgb(140, 79, 255)",
   GCS: "rgb(34, 136, 85)",
};


const icons = {
   GATEWAY: "\u{f3ed}",
   INTERNET: "\u{f0ac}",
   GCE: "\u{f2db}",
   GCP: "\u{f085}",
   GCS: "\u{f0c7}",
   DB: "\u{f007}",
};


export const CHART_OPTIONS = {
   hoverDelay: 0,
   navigation: false,
   overview: false,
   selection: {},
   backgroundGradient: [
       { color: "#246", stop: 0 },
       { color: "#123", stop: 1 },
   ],
   iconFontFamily: "Font Awesome 6 Free",
   imageAlignment: {
       "fa-cloud": { size: 0.88 },
   },
};


export const LAYOUT_OPTIONS = {
   SEQUENTIAL_COMBO: {
       name: "sequential",
       orientation: "right",
       linkShape: "curved",
       tightness: 6,
   },
};


// STYLING FUNCTIONS


const closedComboBorderWidth = 1.5;
const selectionBorderWidth = 2.5;


export function createNode(label, imageName, combo) {
   const nodeLabel = {
       text: label,
       padding: { left: 6, right: 6, bottom: 2, top: 4 },
       margin: { top: 2 },
       position: "s",
       border: {
           radius: 4,
           width: selectionBorderWidth,
           color: COLORS.TRANSPARENT,
       },
       backgroundColor: COLORS.DARK_GRAY,
       color: COLORS.WHITE,
       fontFamily: "verdana",
       fontSize: 16,
   };


   const data = {
       imageName,
       ...(combo !== undefined ? { ...combo } : {}),
   };


   return {
       shape: {
           width: 72,
           height: 72,
       },
       color: COLORS[imageName.toUpperCase()],
       label: nodeLabel,
       fontIcon: {
           text: icons[imageName.toUpperCase()],
           color: "white",
       },
       border: {
           radius: 4,
           width: selectionBorderWidth,
           color: COLORS.TRANSPARENT,
       },
       data,
   };
}


export function createLink(id1, id2, options) {
   return {
       id1,
       id2,
       end2: { arrow: true },
       color: COLORS.WHITE,
       ...options,
   };
}


export function closedComboStyle() {
   return {
       border: {
           color: COLORS.LIGHT_GRAY,
           radius: 4,
           width: closedComboBorderWidth,
       },
       color: COLORS.DARK_GRAY,
       shape: {
           width: 84,
           height: 84,
       },
       fontIcon: { text: "\u{f0c2}", color: COLORS.ORANGE },
   };
}


export function closedComboLabel(name, count) {
   return [
       {
           backgroundColor: COLORS.DARK_GRAY,
           border: {
               color: COLORS.LIGHT_GRAY,
               radius: 4,
               width: closedComboBorderWidth,
           },
           margin: { top: 3 },
           position: "s",
           textAlignment: { horizontal: "center" },
           padding: "6 8 6 8",
           text: name,
           color: COLORS.WHITE,
           minHeight: 20,
           fontSize: 18,
       },
       {
           text: `${count}`,
           position: { horizontal: 70, vertical: -14 },
           backgroundColor: COLORS.DARK_GRAY,
           border: {
               color: COLORS.LIGHT_GRAY,
               width: closedComboBorderWidth,
               radius: 4,
           },
           color: COLORS.WHITE,
           fontSize: 18,
           padding: "6 4 4 4",
           textAlignment: { vertical: "middle", horizontal: "center" },
           margin: { top: 2 },
           minWidth: 18,
           minHeight: 18,
       },
   ];
}


export function openComboStyle() {
   return {
       border: { width: 0, radius: 4 },
       color: COLORS.COMBO,
       arrange: {
           name: "sequential",
           orientation: "right",
           stacking: { arrange: "grid" },
           linkShape: "curved",
       },
   };
}


export function openComboLabel(name) {
   const comboBannerBackground = {
       border: {
           width: 0,
           radius: "4 4 0 0",
       },
       backgroundColor: COLORS.DARK_GRAY,
       minWidth: "stretch",
       position: { vertical: "top" },
       minHeight: 40,
   };


   const comboIcon = {
       fontIcon: { text: "\u{f0c2}", color: COLORS.ORANGE },
       position: { horizontal: "left", vertical: "top" },
       backgroundColor: COLORS.TRANSPARENT,
       minHeight: 42,
       fontSize: 30,
       padding: { left: 10, right: 8 },
   };
   const comboBanner = {
       backgroundColor: COLORS.TRANSPARENT,
       fontFamily: "verdana",
       position: { vertical: "inherit" },
       color: COLORS.WHITE,
       fontSize: 20,
       minHeight: 42,
       padding: { right: 120 / name.length },
   };


   return [comboBannerBackground, comboIcon, { ...comboBanner, text: name }];
}


const response = await fetchData();
export const data = {};


response.forEach((r) => {
   if (r.id1 == "") {
       // Node
       if (r.cloud) {
           // Combo
           data[r.id] = createNode(r.id, r.type, {
               server: r.server ? r.server : undefined,
               vpc: r.vpc ? r.vpc : undefined,
               region: r.region ? r.region : undefined,
               cloud: r.cloud ? r.cloud : undefined,
           });
       } else {
           // Not combo
           data[r.id] = createNode(r.id, r.type);
       }
   } else {
       // Link
       data[`${r.id1}-${r.id2}`] = createLink(r.id1, r.id2);
   }
});

Here we’ve transformed the data we received from Spanner into an object that contains nodes and links. We also set styles for colors, combos, and icons.

4. Render the data in ReGraph

The next step is to bring in ReGraph’s Chart component and hook up all of our transformed data from data.js to it. We’ll set up the interaction event handlers as well.

import "./App.css";
import { useState, useRef } from 'react';
import { Chart, FontLoader } from 'regraph';
import "@fortawesome/fontawesome-free/css/fontawesome.css";
import "@fortawesome/fontawesome-free/css/solid.css";
import {
   data,
   LAYOUT_OPTIONS,
   CHART_OPTIONS,
   closedComboStyle,
   closedComboLabel,
   openComboStyle,
   openComboLabel,
} from './data.js';


function App() {
   const [state, setState] = useState({
       items: data,
       animateOptions: {
         animate: false,
         time: 500,
       },
       closedCombos: {},
       combine: {
         properties: ['server', 'vpc', 'region', 'cloud'],
         level: 4,
         shape: 'rectangle',
       },
       layout: LAYOUT_OPTIONS.SEQUENTIAL_COMBO,
     });
     const comboIds = useRef({});


     function onCombineNodesHandler(comboDefinition) {
       const combo = comboDefinition.combo;
       const comboName =
         combo.server || combo.vpc || combo.region || combo.cloud || '';
  
       if (!comboIds.current[comboDefinition.id]) {
         comboIds.current[comboDefinition.id] = comboName;
       }
  
       comboDefinition.setStyle({
         open: !state.closedCombos[comboDefinition.id],
  
         label: openComboLabel(comboName),
         ...openComboStyle(),
  
         closedStyle: {
           label: closedComboLabel(
             comboName,
             Object.keys(comboDefinition.nodes).length
           ),
           ...closedComboStyle(),
         },
       });
     }


     function onCombineLinksHandler(summaryLink) {
       summaryLink.setStyle({ contents: true, summary: false });
     }
  
     function doubleClickHandler({ id }) {
       if (!id) return;
  
       if (comboIds.current[id]) {
         setState((current) => {
           return {
             ...current,
             animateOptions: {
               animate: true,
               time: 500,
             },
             combine: { ...current.combine },
             closedCombos: {
               ...current.closedCombos,
               [id]: !current.closedCombos[id],
             },
           };
         });
       }
     }
    
   return (
       
); } export default App
    Here, we:
  • initialize a ReGraph chart
  • import the settings and items from our data file
  • set up handlers for combos and user interactions

5. Run the application

Start both the Express server and ReGraph app:

npm run start # Start the Express server

npm run dev # Start the ReGraph app

The server runs on localhost:3000 and the front end on localhost:5173. If everything is set up correctly, you should see your graph render.

This visual representation gives analysts the situational awareness and actionable insights they need to keep their cloud network safe. Grouping related assets into “combos” simplifies complex infrastructure, allowing analysts to easily navigate and focus on specific areas. Starting from a collapsed view, users can drill down by double-clicking on each combo node to reveal more detailed information, without being overwhelmed by the full dataset.

Infrastructure as a Service: Visualizing a cloud network asset inventory with ReGraph and Google Spanner Graph

The interactive graph highlights relationships between cloud infrastructure assets. This helps analysts identify vulnerabilities, monitor data flows, and respond to security alerts quickly. By visualizing network topology in real-time, analysts can make informed decisions, detect patterns and collaborate more effectively to maintain a secure cloud environment.

Ready to upgrade your cybersecurity defense?

By combining Google Spanner Graph with ReGraph, this proof-of-concept demonstrates how you can unlock cloud asset visibility and gain valuable insights into your cloud infrastructure’s security posture. The ability to visualize complex interconnections between cloud services, identify potential attack paths, and understand data flows is key to modern cybersecurity defense.

The next steps could include real-time data updates, interactive filtering, or machine learning integrations for predictive security analytics – the possibilities are endless. Let us know how you use this setup!

FREE: Start your ReGraph trial today

Visualize your data! Request full access to our ReGraph SDK, demos and live-coding playground.

TRY REGRAPH

How can we help you?

Request trial

Ready to start?

Request a free trial

Learn more

Want to learn more?

Explore our resource hub

“case

Looking for success stories?

Browse our case studies

Registered in England and Wales with Company Number 07625370 | VAT Number 113 1740 61
6-8 Hills Road, Cambridge, CB2 1JP. All material © Cambridge Intelligence .
Read our Privacy Policy.