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

Add Device and Users Steps #2

Merged
merged 6 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
zemberdotnet marked this conversation as resolved.
Show resolved Hide resolved
}

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);
zemberdotnet marked this conversation as resolved.
Show resolved Hide resolved
}

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