Skip to main content

Developing an integration

Getting Started

In this development guide, we will walk you through the initial steps of getting your integration up and running. Along the way, we will provide tips and tricks to ensure your success. JupiterOne has many open-source projects that provide an easy-to-use framework for creating a new integration, including the code found in this SDK project.

Requirements

To get started, you'll need the following:

  1. Node.js

    note

    We recommended using a Node version manager. Both fnm and nvm are great choices.

  1. yarn
      npm install --global yarn

Setup

With our tooling squared away, we'll need to setup our project:

Through the GitHub CLI

gh repo create graph-$INTEGRATION_NAME --public \
--clone \
--template=https://github.com/jupiterone/integration-template
cd graph-$INTEGRATION_NAME
yarn install

Through the GitHub UI

  1. Use the integration-template to create a new repository
  2. Clone your repository and run yarn install
git clone https://github.com/$USERNAME/$REPO_NAME`
cd $REPO_NAME
yarn install

That's it! Your integration project is ready for development!

Developing the integration

In this guide, we will create a small integration with DigitalOcean using examples which you can apply to the integration you are building.

Integration configuration

Every integration builds and exports an InvocationConfig that is used to execute the integration.

In the new integration that you created, you can see the InvocationConfig exported in src/index.ts

📁 src/index.ts

export const invocationConfig: IntegrationInvocationConfig<IntegrationConfig> =
{
instanceConfigFields,
validateInvocation,
integrationSteps,
};

Let's work from the top to bottom. We'll start by defining instanceConfigFields, next we'll implement validateInvocation, and finally define our integrationSteps.

1. Creating InstanceConfigFields

The first object in our InvocationConfig is instanceConfigFields with type IntegrationInstanceConfigFieldsMap. You'll find this defined in your project in src/config.ts.

📁 src/config.ts

/**
* A type describing the configuration fields required to execute the
* integration for a specific account in the data provider.
*
* When executing the integration in a development environment, these values may
* be provided in a `.env` file with environment variables. For example:
*
* - `CLIENT_ID=123` becomes `instance.config.clientId = '123'`
* - `CLIENT_SECRET=abc` becomes `instance.config.clientSecret = 'abc'`
*
* Environment variables are NOT used when the integration is executing in a
* managed environment. For example, in JupiterOne, users configure
* `instance.config` in a UI.
*/
export const instanceConfigFields: IntegrationInstanceConfigFieldMap = {
clientId: {
type: 'string',
},
clientSecret: {
type: 'string',
mask: true,
},
};

The instanceConfigFields object lets us control how the integration will execute. A common use is to provide credentials to authenticate requests. For example, DigitalOcean requires a Personal Access Token (see below). Other common config values include a Client ID, API Key, or API URL. Any outside information the integration needs at runtime can be defined here.

DigitalOcean requires a Person Access Token, so we'll edit the fields to reflect that.

📁 src/config.ts

export const instanceConfigFields: IntegrationInstanceConfigFieldMap = {
- clientId: {
- type: 'string',
- },
- clientSecret: {
- type: 'string',
- mask: true,
- },
+ accessToken: {
+ type: 'string',
+ mask: true,
+ }
};
danger

The mask property should be set to true any time a property is secret or sensitive.

We should also edit IntegrationConfig in the same file to match instanceConfigFields. IntegrationConfig is used to add and provide type information throughout the project.

📁 src/config.ts

export interface IntegrationConfig extends IntegrationInstanceConfig {
- /**
- * The provider API client ID used to authenticate requests.
- */
- clientId: string;
-
- /**
- * The provider API client secret used to authenticate requests.
- */
- clientSecret: string;
+ /**
+ * The accessToken to use when authenticating with the API.
+ */
+ accessToken: string;
}

Lastly, we will want to create a .env file with our configuration. Let's edit .env.example to match our project:

📁 .env.example

- CLIENT_ID=
- CLIENT_SECRET=
+ ACCESS_TOKEN=<access token goes here>
cp .env.example .env

In the .env file, we can put our ACCESS_TOKEN. Make sure not to put real secrets in the .env.example!

note

The .env file should NEVER be committed. The integration-template has .env in the .gitignore, but always be sure not to add and commit it.

Awesome! We have created our instanceConfigFields and IntegrationConfig. Let's go to the next step.

2. Creating ValidateInvocation

Next, we will create our validateInvocation function. The basic contract for validateInvocation is as follows:

  • The function receives the execution context and configuration we set in instanceConfigFields.
  • The function will validate that all configuration is present and valid or throw an error otherwise.

Let's create a validateInvocation for DigitalOcean.

Running the integration

We've now:

✅ Created a new integration project
✅ Installed dependencies with yarn install
✅ Created our instanceConfigFields
✅ Setup a .env file
✅ Created our validateInvocation
✅ Added our API Client and authenticated request
✅ Created our first IntegrationStep

Looks like we're ready to run our integration! We can collect data using:

yarn start

You should now see the collected data in the .j1-integration and you can visualize the results with yarn graph.