Skip to content
This repository has been archived by the owner on Sep 3, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2 from JupiterOne/int-add-device-users
Browse files Browse the repository at this point in the history
Add Device and Users Steps
  • Loading branch information
zemberdotnet authored Mar 8, 2023
2 parents c6247dc + d1c8a52 commit 394671c
Show file tree
Hide file tree
Showing 19 changed files with 1,730 additions and 114 deletions.
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
CLIENT_ID=
CLIENT_SECRET=
BASE_URL=
DEVICE_42_USERNAME=
PASSWORD=
19 changes: 4 additions & 15 deletions docs/jupiterone.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,10 @@ https://github.com/JupiterOne/sdk/blob/main/docs/integrations/development.md

The following entities are created:

| Resources | Entity `_type` | Entity `_class` |
| --------- | -------------- | --------------- |
| Account | `acme_account` | `Account` |
| User | `acme_user` | `User` |
| UserGroup | `acme_group` | `UserGroup` |

### Relationships

The following relationships are created:

| Source Entity `_type` | Relationship `_class` | Target Entity `_type` |
| --------------------- | --------------------- | --------------------- |
| `acme_account` | **HAS** | `acme_group` |
| `acme_account` | **HAS** | `acme_user` |
| `acme_group` | **HAS** | `acme_user` |
| Resources | Entity `_type` | Entity `_class` |
| --------- | ------------------ | ---------------- |
| Device | `device42_device` | `Host`, `Device` |
| End Users | `device42_enduser` | `User` |

<!--
********************************************************************************
Expand Down
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"access": "public"
},
"scripts": {
"start": "j1-integration collect",
"start": "j1-integration collect -V",
"graph": "j1-integration visualize",
"graph:types": "j1-integration visualize-types",
"graph:spec": "j1-integration visualize-types --project-path docs/spec --output-file ./.j1-integration/types-graph/index.spec.html",
Expand All @@ -32,13 +32,15 @@
"postversion": "cp package.json ./dist/package.json"
},
"peerDependencies": {
"@jupiterone/integration-sdk-core": "^8.22.0"
"@jupiterone/integration-sdk-core": "^8.30.5"
},
"dependencies": {
"gaxios": "^5.0.2"
},
"dependencies": {},
"devDependencies": {
"@jupiterone/integration-sdk-core": "^8.22.0",
"@jupiterone/integration-sdk-dev-tools": "^8.22.0",
"@jupiterone/integration-sdk-testing": "^8.22.0",
"@jupiterone/integration-sdk-core": "^8.30.5",
"@jupiterone/integration-sdk-dev-tools": "^8.30.5",
"@jupiterone/integration-sdk-testing": "^8.30.5",
"auto": "^10.36.5"
},
"auto": {
Expand Down
77 changes: 75 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { GaxiosOptions, request } from 'gaxios';
import { IntegrationConfig } from './config';
import {
Device42Device,
Device42DeviceResponse,
Device42EndUser,
} from './types';

export type ResourceIteratee<T> = (each: T) => Promise<void> | void;

Expand All @@ -13,8 +19,75 @@ export type ResourceIteratee<T> = (each: T) => Promise<void> | void;
export class APIClient {
constructor(readonly config: IntegrationConfig) {}

public verifyAuthentication() {
return;
public async verifyAuthentication() {
return Promise.resolve();
}

public async iterateEndUsers(iteratee: ResourceIteratee<Device42EndUser>) {
let finished = false;
let offset = 0;
do {
const response = await this.makeRequest<{ values: Device42EndUser[] }>({
url: '/api/1.0/endusers/',
params: {
offset,
limit: 100,
},
});

for (const v of response.data.values) {
await iteratee(v);
}
if (response.data.values.length === 0) {
finished = true;
}

offset += response.data.values.length;
} while (!finished);
}

public async iterateDevices(iteratee: ResourceIteratee<Device42Device>) {
const limit = 500;
let total = 0;
let offset = 0;
do {
const response = await this.makeRequest<Device42DeviceResponse>({
url: '/api/1.0/devices/all/',
params: {
limit: limit,
offset: offset,
blankasnull: 'yes',
},
});

for (const v of response.data.Devices) {
await iteratee(v);
}
offset += response.data.Devices.length;
total = response.data.total_count;
} while (offset < total);
}

private async makeRequest<T>(
opts: Pick<GaxiosOptions, 'url' | 'params' | 'method' | 'body'>,
) {
const auth = Buffer.from(
`${this.config.device42Username}:${this.config.password}`,
).toString('base64');
return await request<T>({
url: opts.url,
baseUrl: this.config.baseUrl,
params: opts.params,
headers: {
Authorization: `Basic ${auth}`,
},
retryConfig: {
retry: 3,
retryDelay: 3000,
},
method: opts.method,
body: opts.body,
});
}
}

Expand Down
23 changes: 10 additions & 13 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import { createAPIClient } from './client';
* `instance.config` in a UI.
*/
export const instanceConfigFields: IntegrationInstanceConfigFieldMap = {
clientId: {
baseUrl: {
type: 'string',
},
clientSecret: {
device42Username: {
type: 'string',
},
password: {
type: 'string',
mask: true,
},
Expand All @@ -35,25 +38,19 @@ export const instanceConfigFields: IntegrationInstanceConfigFieldMap = {
* same properties defined by `instanceConfigFields`.
*/
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;
device42Username: string;
password: string;
baseUrl: string;
}

export async function validateInvocation(
context: IntegrationExecutionContext<IntegrationConfig>,
) {
const { config } = context.instance;

if (!config.clientId || !config.clientSecret) {
if (!config.device42Username || !config.password || !config.baseUrl) {
throw new IntegrationValidationError(
'Config requires all of {clientId, clientSecret}',
'Config requires all of {device42Username, password, baseUrl}',
);
}

Expand Down
15 changes: 14 additions & 1 deletion src/steps/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,21 @@ import {

export const Steps = {
ACCOUNT: 'fetch-account',
END_USERS: 'fetch-end-users',
DEVICES: 'fetch-devices',
};

export const Entities: Record<string, StepEntityMetadata> = {};
export const Entities: Record<'END_USER' | 'DEVICE', StepEntityMetadata> = {
END_USER: {
resourceName: 'End Users',
_type: 'device42_enduser',
_class: ['User'],
},
DEVICE: {
resourceName: 'Device',
_type: 'device42_device',
_class: ['Host', 'Device'],
},
};

export const Relationships: Record<string, StepRelationshipMetadata> = {};

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/steps/devices/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createIntegrationEntity } from '@jupiterone/integration-sdk-core';
import { Device42Device } from '../../types';
import { Entities } from '../constants';

export function createDeviceEntity(device: Device42Device) {
return createIntegrationEntity({
entityData: {
source: device,
assign: {
_key: `device42_device:${device.id}`,
_type: Entities.DEVICE._type,
_class: Entities.DEVICE._class,
id: device.id.toString(),
name: device.name,
type: device.type,
serial: device.serial_no || undefined,
ipAddress: device.ip_addresses.map((o) => o.ip),
macAddress: device.mac_addresses.map((o) => o.mac),
active: device.in_service,
inService: device.in_service,
uuid: device.uuid || undefined,
osName: device.os,
osVersion: device.osver,
serviceLevel: device.service_level,
category: device.type,
make: device.manufacturer,
model: device.hw_model,
deviceId: device.uuid,
hostname: null,
},
},
});
}
23 changes: 23 additions & 0 deletions src/steps/devices/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { executeStepWithDependencies } from '@jupiterone/integration-sdk-testing';
import { buildStepTestConfigForStep } from '../../../test/config';
import { Recording, setupProjectRecording } from '../../../test/recording';
import { Steps } from '../constants';

// pagination takes some time for this test
jest.setTimeout(100_000);
// See test/README.md for details
let recording: Recording;
afterEach(async () => {
await recording.stop();
});

test('fetch-devices', async () => {
recording = setupProjectRecording({
directory: __dirname,
name: 'fetch-devices',
});

const stepConfig = buildStepTestConfigForStep(Steps.DEVICES);
const stepResult = await executeStepWithDependencies(stepConfig);
expect(stepResult.collectedEntities.length).toBeGreaterThan(0);
});
29 changes: 29 additions & 0 deletions src/steps/devices/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
IntegrationStep,
IntegrationStepExecutionContext,
} from '@jupiterone/integration-sdk-core';
import { createAPIClient } from '../../client';
import { IntegrationConfig } from '../../config';
import { Entities, Steps } from '../constants';
import { createDeviceEntity } from './converter';

export const devicesSteps: IntegrationStep<IntegrationConfig>[] = [
{
id: Steps.DEVICES,
name: 'Fetch Devices',
entities: [Entities.DEVICE],
relationships: [],
dependsOn: [],
executionHandler: fetchDevices,
},
];

async function fetchDevices({
instance,
jobState,
}: IntegrationStepExecutionContext<IntegrationConfig>) {
const client = createAPIClient(instance.config);
await client.iterateDevices(async (d) => {
await jobState.addEntity(createDeviceEntity(d));
});
}

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/steps/end-users/converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createIntegrationEntity } from '@jupiterone/integration-sdk-core';
import { Device42EndUser } from '../../types';
import { Entities } from '../constants';

export function createEndUserEntity(endUser: Device42EndUser) {
return createIntegrationEntity({
entityData: {
source: endUser,
assign: {
_key: `device42_enduser:${endUser.id}`,
_class: Entities.END_USER._class,
_type: Entities.END_USER._type,
id: endUser.id.toString(),
email: endUser.email || undefined,
contact: endUser.contact || undefined,
domain: endUser.domain || undefined,
name: endUser.name,
activeDirectoryUsername: endUser.adusername || undefined,
username: endUser.adusername || undefined,
notes: undefined,
},
},
});
}
21 changes: 21 additions & 0 deletions src/steps/end-users/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { executeStepWithDependencies } from '@jupiterone/integration-sdk-testing';
import { buildStepTestConfigForStep } from '../../../test/config';
import { Recording, setupProjectRecording } from '../../../test/recording';
import { Steps } from '../constants';

// See test/README.md for details
let recording: Recording;
afterEach(async () => {
await recording.stop();
});

test('fetch-end-users', async () => {
recording = setupProjectRecording({
directory: __dirname,
name: 'fetch-end-users',
});

const stepConfig = buildStepTestConfigForStep(Steps.END_USERS);
const stepResult = await executeStepWithDependencies(stepConfig);
expect(stepResult).toMatchStepMetadata(stepConfig);
});
29 changes: 29 additions & 0 deletions src/steps/end-users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
IntegrationStep,
IntegrationStepExecutionContext,
} from '@jupiterone/integration-sdk-core';
import { createAPIClient } from '../../client';
import { IntegrationConfig } from '../../config';
import { Entities, Steps } from '../constants';
import { createEndUserEntity } from './converters';

export const endUsersSteps: IntegrationStep<IntegrationConfig>[] = [
{
id: Steps.END_USERS,
name: 'Fetch End Users',
entities: [Entities.END_USER],
relationships: [],
dependsOn: [],
executionHandler: fetchEndUsers,
},
];

async function fetchEndUsers({
instance,
jobState,
}: IntegrationStepExecutionContext<IntegrationConfig>) {
const client = createAPIClient(instance.config);
await client.iterateEndUsers(async (e) => {
await jobState.addEntity(createEndUserEntity(e));
});
}
5 changes: 4 additions & 1 deletion src/steps/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const integrationSteps = [];
import { devicesSteps } from './devices';
import { endUsersSteps } from './end-users';

const integrationSteps = [...endUsersSteps, ...devicesSteps];

export { integrationSteps };
Loading

0 comments on commit 394671c

Please sign in to comment.