cont3xt/integrations/README.md
This guide walks you through creating a new integration for Cont3xt. We'll use real examples from the codebase to demonstrate best practices.
A Cont3xt integration connects to external data sources (APIs, databases, services) to enrich indicators like domains, IPs, emails, hashes, etc. Each integration:
See descriptions.txt for detailed reference on card fields, tidbits, and post-processors.
Create a new directory under cont3xt/integrations/ with the following structure:
cont3xt/integrations/yourservice/
├── index.js # Main integration code
└── icon.png # 200x200 icon (optional but recommended)
Every integration extends the Integration base class and follows this pattern:
/******************************************************************************/
/* Copyright Yahoo Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
const Integration = require('../../integration.js');
const axios = require('axios');
class YourServiceIntegration extends Integration {
name = 'Your Service Name'; // Display name
icon = 'integrations/yourservice/icon.png'; // Path to icon
order = 100; // Display order (lower = earlier)
itypes = { // Indicator types this integration handles
domain: 'fetchDomain', // Map itype to fetch method (only define this for the iTypes applicable to this service)
url: 'fetchUrl',
ip: 'fetchIp',
hash: 'fetchHash',
text: 'fetchText',
phone: 'fetchPhone',
email: 'fetchEmail'
};
homePage = 'https://yourservice.com/'; // Service homepage (optional)
settings = {
username: {
help: 'Your Service Username',
required: true
}
// Add more settings as needed (API keys, etc.)
};
card = {
// Card configuration (see Card Configuration section)
};
tidbits = {
// Tidbit configuration (see Tidbit Configuration section)
};
cacheTimeout = 24 * 60 * 60 * 1000; // Cache results (milliseconds)
constructor () {
super();
Integration.register(this);
}
async fetchDomain (user, domain) {
// Fetch and return data (see Data Fetching section)
}
async fetchIp (user, ip) {
// Fetch and return data (see Data Fetching section)
}
// other fetch functions for every applicable iType
}
new YourServiceIntegration();
name: Display name shown in the UIitypes: Object mapping indicator types to fetch methods
domain, ip, email, phone, hash, url, text'fetchDomain')icon: Path to icon file (makes integration visible as a button)order: Number controlling display order (lower numbers appear first)homePage: URL to the service's homepagesettings: User configuration options (see Authentication section)card: How to display detailed results (see Card Configuration)tidbits: Small bits of info shown in the indicator result treecacheTimeout: How long to cache results in millisecondsconfigName: Use settings from another integration (for shared credentials)// Standard: 24 hours (most services)
cacheTimeout = 24 * 60 * 60 * 1000;
// Extended: 1 week (for flaky, slow, expensive, or rate-limited services)
cacheTimeout = 7 * 24 * 60 * 60 * 1000;
// Short: 1 hour (for rapidly changing data)
cacheTimeout = 60 * 60 * 1000;
Cards display detailed integration results. See descriptions.txt for complete field type reference.
card = {
title: 'Service Name for %{query}', // %{query} replaced with indicator
searchUrls: [{
url: 'https://service.com/search/%{query}',
itypes: ['domain', 'ip'],
name: 'Search Service for %{query}'
}],
fields: [
'simpleField', // String field (label = field name)
{
label: 'Custom Label', // Display name
field: 'nested.path.to.data' // Dot notation for nested data
},
{
label: 'Date Field',
field: 'created_at',
type: 'date' // Format as date
},
{
label: 'Array Field',
field: 'tags',
type: 'array',
join: ', ' // Join array with comma
},
{
label: 'Table Field',
field: 'items', // Path to array
type: 'table',
fields: [ // Columns in table
{
label: 'Name',
field: 'name'
},
{
label: 'Value',
field: 'value'
}
]
}
]
};
string: Default, plain textdate: Formats date/timestamparray: Displays array items (use join to show on one line)table: Displays array of objects as a tableurl: Makes the value clickablejson: Shows raw JSON with formattingms: Millisecond timestampseconds: Second timestamp{
label: 'Complex Field',
field: 'path.to.data',
type: 'string',
defang: true, // Change http->hXXp, .->[-]
pivot: true, // Add pivot dropdown menu
defaultSortField: 'name', // For tables, default sort column
defaultSortDirection: 'asc', // Sort direction
fieldRoot: 'nested.path', // For tables, extract nested objects
filterEmpty: true // Remove empty rows (default: true)
}
Tidbits are small pieces of information shown in the Indicator Result Tree (IRT). See descriptions.txt for complete reference.
tidbits = {
order: 100, // Default order for all fields
fields: [
{
field: 'count',
tooltip: 'total results' // Hover text
},
{
field: 'registration.created',
postProcess: 'removeTime', // Strip time from date
tooltipTemplate: '<value>', // Template for tooltip
purpose: 'registered', // Tidbit purpose
precedence: 2, // Higher = preferred when duplicates
order: 100 // Sort order
},
{
field: 'link',
display: 'cont3xtCopyLink' // Make it a copyable link
}
]
};
badge: Default, simple badgecont3xtField: Clickable with copy/pivot optionscont3xtCopyLink: Copyable linkprimaryGroup, successGroup, etc.: Colored group badges for arraysWhen multiple integrations provide the same type of information (e.g., "registered date"), use purpose and precedence to control which one is displayed:
{
field: 'createdDate',
purpose: 'registered', // Identifies this as registration date
precedence: 1 // Lower = less preferred (1-3 typical)
}
async fetchDomain (user, domain) {
try {
// NOTE: authentication options depend on your service
// check your service's API documentation for details and the Authentication section for examples
const result = await axios.get(`https://api.service.com/domain/${domain}`, {
// or it might be sent in the parameters and not the url
params: {
domain
},
headers: {
Accept: 'application/json', // or whatever content type the service supports
'User-Agent': this.userAgent() // Always include user agent
}
});
// Return data with _cont3xt count
result.data._cont3xt = { count: 1 };
return result.data;
} catch (err) {
// Suppress 404s in non-debug mode
if (Integration.debug <= 1 && err?.response?.status === 404) {
return null;
}
console.log(this.name, domain, err);
return null;
}
}
undefined: No data available (not an error)null: Error occurredobject: Success, must include _cont3xt: { count: N } property// Simple response - return as-is
result.data._cont3xt = { count: 1 };
return result.data;
// Array response - wrap it
const data = {
items: result.data,
count: result.data.length,
_cont3xt: { count: result.data.length }
};
return data;
// Add convenience fields
result.data.link = result.data.links?.[0]?.value;
result.data._cont3xt = { count: 1 };
return result.data;
async fetchDomain (user, domain) {
try {
const result = await axios.get(url, options);
if (!result.data) {
return undefined; // No data
}
result.data._cont3xt = { count: 1 };
return result.data;
} catch (err) {
// Don't log 404s unless debugging
if (Integration.debug <= 1 && err?.response?.status === 404) {
return null;
}
// Log detailed API errors
if (err?.response?.data) {
console.log(this.name, domain, 'Error:', err.response.status,
JSON.stringify(err.response.data));
} else {
console.log(this.name, domain, err);
}
return null;
}
}
const result = await axios.get(url, {
maxRedirects: 5, // Follow redirects
validateStatus: false, // Don't throw on non-2xx
headers: {
'Accept': 'application/json',
'User-Agent': this.userAgent()
}
});
// Check status manually
if (result.status === 200) {
// Process data
} else {
return null;
}
settings = {
disabled: {
help: 'Disable integration for all queries',
type: 'boolean'
},
apikey: {
help: 'Your API key from service.com',
password: true, // Hide value in UI
required: true // Must be set to use integration
},
username: {
help: 'Your username from service.com',
required: true // Must be set to use integration
}
};
async fetchDomain (user, domain) {
const apiKey = this.getUserConfig(user, 'apikey'); // fetch this data from this service's configuration
const username = this.getUserConfig(user, 'username'); // fetch this data from this service's configuration
if (!apiKey || !username) {
return undefined;
}
// Use API key in request - there are many ways to do this
// see the documentation for your service's API for details
const result = await axios.get(url, {
headers: {
'Authorization': `Bearer ${apiKey}`, // might be in a bearer token
'User-Agent': this.userAgent()
},
auth: { // might be in an auth object
key: apiKey,
username: username
}
// or a variety of other options
});
// ...
}
settings = {
disabled: {
help: 'Disable integration for all queries',
type: 'boolean'
},
user: {
help: 'Your API username',
required: true
},
key: {
help: 'Your API key',
password: true,
required: true
}
};
async fetchDomain (user, domain) {
const apiUser = this.getUserConfig(user, 'user');
const apiKey = this.getUserConfig(user, 'key');
if (!apiUser || !apiKey) {
return undefined;
}
// Query parameters authentication
const result = await axios.get(url, {
params: {
api_username: apiUser,
api_key: apiKey
},
headers: {
'User-Agent': this.userAgent()
}
});
// ...
}
If multiple integrations use the same service credentials:
class SecondIntegration extends Integration {
name = 'Service Feature B';
configName = 'Service Feature A'; // Use settings from this integration
// ...
}
Set debug level to see detailed logs:
# Start with debug logging
DEBUG=2 npm start
icon property or check itypes_cont3xt: { count: N } is setfield paths match API response structure - you can check this by examining the raw data within the integration cardgetUserConfig() calls and settingsfield path and add tooltipthis.userAgent() in headersundefined for no data, null for errors_cont3xt: { count: N }'registration.created'cont3xt/integrations/ for more examplescont3xt/integration.js for available methodsLook at existing integrations for patterns:
whois, crtshdomaintools, shodan, virustotalrdap, virustotalshodan, passivetotal