Governance, Risk, and Compliance (GRC) - OpenPages

Governance, Risk, and Compliance (GRC) - OpenPages

Intended for IBM OpenPages and IBM FIRST Risk Case Studies customers to interact with their industry peers and communicate with IBM product experts.


#OpenPages-Governance,Risk,andCompliance(GRC)
 View Only

Real-Time Integrations Made Simple: Introducing Webhooks in IBM OpenPages

By Ilamathi Periyasamy posted 8 days ago

  

Authors:

Ilamathi Periyasamy, Software Developer
Ghada Obaid, Senior Software Developer

Real-Time Integrations Made Simple: Introducing Webhooks in IBM OpenPages

In today’s fast-paced digital world, real-time communication between systems isn’t just a nice-to-have—it’s essential. Until now, integrating OpenPages with external applications often meant writing custom logic using triggers, workflows or  scheduled jobs. While powerful, these approaches  are tightly coupled with the OpenPages server technology stack, and require access to the server to make modifications.  This is not possible on native SaaS environments.

To simplify and modernize these integrations, OpenPages now supports webhooks—a lightweight, event-driven way to notify external systems when something important happens.

What Are Webhooks?

Think of webhooks as a “notification system”. Instead of external systems polling OpenPages for updates, OpenPages pushes event notifications to a specified URL the moment something changes—like when a record is created or updated. This makes integrations faster, more efficient, and easier to manage.

Webhook Configuration Overview

Setting up a webhook in OpenPages is simple and flexible. Here's what you need to configure:

Endpoint Setup

Your webhook endpoint must:
- Accept POST requests over HTTPS
- Handle OpenPages event JSON payloads
- Respond to an empty {} payload for connection testing

For SaaS deployments, the endpoint’s domain must be on the approved allowlist.

Authentication

OpenPages supports multiple authentication methods:
- Basic Auth (username/password)
- Bearer Token
- API Key (custom header and token prefix)
- OAuth 2.0 (IBM IAM API keys only)

Event Subscriptions

Choose which object types and events you want to monitor. Supported event types include:

-       create – published whenever an object of a type in the subscription is created

-       update – published whenever an object of a type in the subscription is updated.  This event contains information as to which fields changed in the object.

-       delete – published whenever an object of a type in the subscription is deleted

-   associate – published whenever an object of a type in the subscription is associated with a parent object

-       disassociate – published whenever an object of a type in the subscription is removed from a parent object

Activating and Testing Your Webhook

Webhooks are disabled by default, giving you control over when they go live.

-       SaaS Deployments: Enable the webhook first. After a short wait for the egress connection to open, run the Test Connection.

-       On-Premise Deployments: Test the connection first, then enable the webhook.

Once the webhook is enabled, wait for 5 minutes for the events broker to pick up the configuration.  After that any event relevant for the webhook will get dispatched to the endpoint.

System Constraints

To keep things running smoothly, OpenPages enforces a few limits:

-       Each webhook must use a unique endpoint URL—duplicates aren’t allowed.

-       While you can create multiple configurations, only a limited number can be enabled at once.
This is controlled by the registry setting /Platform/Messaging/Webhooks Limit, which defaults to 50 and can go up to 100.

Automatic Resource Management

OpenPages handles webhook resources automatically:

-       If a webhook is disabled or its endpoint fails, the system removes its message queue consumer. This means that the queue will continue receiving events but these events will not be dispatched to the endpoint unless enabled again.  Please note that events in the queue have a default time to live (TTL) of 1 day, after which they are automatically removed. Should the webhook become active again, any events remaining in the queue will be dispatched to the endpoint.

-       When a webhook is deleted, its message queue is permanently deleted.

A Real-World GRC Scenario: Callback in Action

Let’s walk through a practical example of how webhook-driven automation can enhance Governance, Risk, and Compliance (GRC) workflows.

A diagram of a message

Description automatically generated


Imagine a user updates a risk object in IBM OpenPages, and its risk rating changes—a critical event that could impact compliance, audit readiness, or operational thresholds. With webhook support, OpenPages immediately sends a notification to any external system that has subscribed to this event in its webhook configuration.

This real-time alert allows the external system to respond intelligently: it can fetch full object details via the OpenPages API and take appropriate actions such as reassigning the risk, escalating it to compliance, or triggering automated workflows.

Here’s a sample webhook message for such a risk rating change:

{
  "action":"update",
  "type":"GRC_OBJECT",
  "objectType":"SOXRisk",
  "message":{
    "event":"update",
    "publishedDate":"2025-09-25 02:14:26.797Z",
    "details":{
      "object_id":"9032",
      "object_name":"RB-01-Risk00190",
      "object_path":"/_op_sox/Project/Default/ICDocumentation/Risks/Global Financial Services/North America/Retail Banking/RB-01-Risk00190.txt",
      "updated_fields":[
        "OPSS-Risk-Qual:Inherent Likelihood",
        "OPSS-Risk-Qual:Inherent Risk Rating",
        "OPSS-Risk-Quant:Inherent Frequency",
        "OPSS-Risk-Quant:Inherent Risk Exposure"
      ],
      "last_modification":"2025-08-30 08:58:48.000Z"
    },
    "actor":{
      "iam_id":"user1@ibm.com",
      "user_id":"2087",
      "user_name":"user1"
    }
  },
  "version":"9.1.2"
}

Final Thoughts

Webhooks introduce a streamlined, event-driven way to integrate IBM OpenPages with external systems. They reduce complexity, eliminate polling, and enable real-time responsiveness across your GRC ecosystem. Whether it's triggering alerts, automating compliance actions, or syncing data, webhooks unlock faster, smarter workflows.

Appendix: Sample Webhook Code

The following is a sample webhook that can be deployed to node.js. It demonstrates how a webhook event can be leveraged to call back the OpenPages API, retrieve object details, and trigger downstream actions—such as sending alerts, updating external systems, orchestrating workflows, etc.

const fs = require('fs');
const http = require('http');
const https = require('https');
const express = require('express');
const axios = require('axios');
const app = express();
const PORT_HTTP = 3000;
const PORT_HTTPS = 3443;
const BEARER_TOKEN = 'your-token'; // ideally from env in production
const IBM_CLOUD_API_KEY = 'your-ibm-cloud-api-key'; // Replace with actual API key in production
const OPENPAGES_BASE_URL = 'https://your-openpages-instance.com'; // Replace with actual OpenPages URL
const IS_SAAS = true; // Set to true for SaaS environment, false for on-premise
const OPENPAGES_USERNAME = 'your-op-username'; // Only used for on-premise environments
const OPENPAGES_PASSWORD = 'your-op-password'; // Only used for on-premise environments

// SSL cert
const sslOptions = {
  key: fs.readFileSync('./ssl/key.pem'),
  cert: fs.readFileSync('./ssl/cert.pem'),
};

// Parse JSON bodies
app.use(express.json());

function bearerTokenMiddleware(req, res, next) {
  const authHeader = req.headers['authorization'];

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'Missing or invalid Authorization header' });
  }

  const token = authHeader.split(' ')[1];
  if (token !== BEARER_TOKEN) {
    return res.status(403).json({ message: 'Forbidden: Invalid token' });
  }

  next();
}


// 🔓 Public route (GET only)
app.get('/', (req, res) => {
  res.send('Welcome to the public route 👋');
});

app.route('/bearer-protected')
  .all(bearerTokenMiddleware)
  .get((req, res) => {
    res.send('🔐 GET: Bearer token accepted.');
  })
  .post(async (req, res) => {
    try {
      console.log('Received webhook with Bearer token authentication:', JSON.stringify(req.body));
      
      // Extract object ID from the webhook payload
      const objectId = extractObjectIdFromWebhook(req.body);
      
      // If objectId is null, respond with success but skip OpenPages API call
      if (objectId === null) {
        console.log('No valid object ID found in webhook payload, skipping OpenPages API call');
        return res.status(200).json({
          success: true,
          message: '🔐 POST: Bearer token accepted but no valid object ID found',
          webhookData: req.body
        });
      }
      
      console.log(`Extracted object ID: ${objectId}`);
      
      // Get the object details from OpenPages
      const objectData = await getOpenPagesObject(objectId);
      
      // Process the object data
      console.log('Successfully retrieved object data from OpenPages');
      
      res.status(200).json({
        success: true,
        message: '🔐 POST: Bearer token accepted and processed OpenPages object',
        objectId: objectId,
        webhookData: req.body,
        openPagesData: objectData
      });
    } catch (error) {
      console.error('Error processing webhook with Bearer token:', error.message);
      res.status(500).json({
        success: false,
        message: error.message,
        webhookData: req.body
      });
    }
  });

// Function to get authentication credentials
async function getAuthCredentials() {
  if (IS_SAAS) {
    // SaaS environment: Use IAM token authentication
    try {
      console.log('Using SaaS authentication method (IAM token)');
      const response = await axios({
        method: 'post',
        url: 'https://iam.cloud.ibm.com/identity/token',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        data: new URLSearchParams({
          'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
          'apikey': IBM_CLOUD_API_KEY
        }).toString()
      });
      
      return {
        type: 'bearer',
        token: response.data.access_token
      };
    } catch (error) {
      console.error('Error getting IAM token:', error.message);
      if (error.response) {
        console.error('Response data:', error.response.data);
        console.error('Response status:', error.response.status);
      }
      throw new Error('Failed to retrieve IAM token');
    }
  } else {
    // On-premise environment: Use basic authentication
    console.log('Using on-premise authentication method (Basic Auth)');
    return {
      type: 'basic',
      username: OPENPAGES_USERNAME,
      password: OPENPAGES_PASSWORD
    };
  }
}

// Helper function to extract object ID from webhook request body
// Returns null if the object ID cannot be extracted
function extractObjectIdFromWebhook(body) {
  try {
    // Try the new structure from the example
    // {"events":[{"eventMessage":{"message":{"details":{"object_id":"14502"}}}}]}
    if (body && body.events && body.events.length > 0 &&
        body.events[0].eventMessage &&
        body.events[0].eventMessage.message &&
        body.events[0].eventMessage.message.details &&
        body.events[0].eventMessage.message.details.object_id) {
      const objectId = body.events[0].eventMessage.message.details.object_id;
      console.log(`Found object ID ${objectId} in events array webhook payload structure`);
      return objectId;
    }
    
    // First, try the direct structure from the example
    // {"action":"create","type":"GRC_OBJECT","objectType":"SOXBusEntity","message":{"details":{"object_id":"13002"}}}
    if (body && body.message && body.message.details && body.message.details.object_id) {
      const objectId = body.message.details.object_id;
      console.log(`Found object ID ${objectId} in direct webhook payload structure`);
      return objectId;
    }
    
    // Then try the nested eventObject structure we initially expected
    // {"eventObject":{"message":{"details":{"object_id":"25502"}}}}
    if (body && body.eventObject && body.eventObject.message &&
        body.eventObject.message.details && body.eventObject.message.details.object_id) {
      const objectId = body.eventObject.message.details.object_id;
      console.log(`Found object ID ${objectId} in nested eventObject webhook payload structure`);
      return objectId;
    }
    
    // If we reach here, we couldn't find the object ID in any expected structure
    console.log('Object ID not found in webhook payload, returning null');
    console.log('Payload structure:', JSON.stringify(body, null, 2));
    return null;
  } catch (error) {
    console.error('Error extracting object ID:', error.message);
    return null;
  }
}

// Function to get OpenPages object
async function getOpenPagesObject(objectId) {
  try {
    // Get authentication credentials
    const auth = await getAuthCredentials();
    
    // Create https agent that ignores SSL certificate validation
    const httpsAgent = new https.Agent({
      rejectUnauthorized: false // Ignore SSL certificate validation
    });
    
    // Set up request configuration
    const requestConfig = {
      method: 'get',
      url: `${OPENPAGES_BASE_URL}/opgrc/api/v2/contents/${objectId}`,
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'NodeJS/Axios'
      },
      httpsAgent: httpsAgent, // Add the https agent to ignore SSL certificate validation
      validateStatus: function (status) {
        return status >= 200 && status < 500; // Accept all status codes less than 500
      }
    };
    
    // Apply the appropriate authentication method
    if (auth.type === 'bearer') {
      requestConfig.headers['Authorization'] = `Bearer ${auth.token}`;
    } else if (auth.type === 'basic') {
      // For basic auth with OpenPages
      const base64Auth = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
      requestConfig.headers['Authorization'] = `Basic ${base64Auth}`;
    }
    
    console.log(`Making API call to OpenPages: ${requestConfig.url}`);
    
    // Call OpenPages API with the appropriate authentication
    const response = await axios(requestConfig);
    
    if (response.status >= 400) {
      throw new Error(`OpenPages API returned status code ${response.status}`);
    }
    
    return response.data;
  } catch (error) {
    console.error('Error getting OpenPages object:', error.message);
    if (error.response) {
      console.error('Response data:', error.response.data);
      console.error('Response status:', error.response.status);
    }
    throw new Error(`Failed to retrieve OpenPages object with ID: ${objectId}`);
  }
}


// HTTPS server
https.createServer(sslOptions, app).listen(PORT_HTTPS, () => {
  console.log(`✅ HTTPS server running at https://localhost:${PORT_HTTPS}`);
});

// HTTP → HTTPS redirect
http.createServer((req, res) => {
  const host = req.headers['host'].replace(/:\d+$/, `:${PORT_HTTPS}`);
  res.writeHead(301, { Location: `https://${host}${req.url}` });
  res.end();
}).listen(PORT_HTTP, () => {
  console.log(`ℹ️  HTTP redirect server on http://localhost:${PORT_HTTP}`);
});

0 comments
23 views

Permalink