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

create mapped relationships and hardware entities #2

Merged
merged 3 commits into from
Jun 2, 2020
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jupiterone/graph-snipe-it",
"version": "0.1.0",
"version": "0.2.0",
"description": "A graph conversion tool for https://snipeitapp.com/",
"license": "MPL-2.0",
"main": "dist/index.js",
Expand All @@ -25,7 +25,7 @@
"prepack": "yarn build"
},
"dependencies": {
"@jupiterone/integration-sdk": "^2.1.2",
"@jupiterone/integration-sdk": "^3.1.0",
"@lifeomic/attempt": "^3.0.0",
"base-64": "^0.1.0",
"node-fetch": "^2.6.0"
Expand Down
13 changes: 5 additions & 8 deletions src/__tests__/validateInvocation.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { createMockExecutionContext } from '@jupiterone/integration-sdk/testing';

import validateInvocation from '../validateInvocation';

import fetchMock from 'jest-fetch-mock';
import { IntegrationConfig } from 'src/types';

beforeEach(() => {
fetchMock.doMock();
Expand All @@ -11,7 +10,7 @@ beforeEach(() => {
test('rejects if apiToken is not present', async () => {
fetchMock.mockResponse('{}');

const context = createMockExecutionContext();
const context = createMockExecutionContext<IntegrationConfig>();
context.instance.config['apiToken'] = undefined;

await expect(validateInvocation(context)).rejects.toThrow(
Expand All @@ -27,10 +26,8 @@ test('rejects if unable to hit provider apis', async () => {
}),
);

const context = createMockExecutionContext();
context.instance.config = {
apiToken: 'test',
};
const context = createMockExecutionContext<IntegrationConfig>();
context.instance.config.apiToken = 'test';

await expect(validateInvocation(context)).rejects.toThrow(
/Provider authentication failed/,
Expand All @@ -40,7 +37,7 @@ test('rejects if unable to hit provider apis', async () => {
test('performs sample api call to ensure api can be hit', async () => {
fetchMock.mockResponse(JSON.stringify({ result: [] }));

const context = createMockExecutionContext();
const context = createMockExecutionContext<IntegrationConfig>();
context.instance.config = {
hostname: 'test',
apiToken: 'test',
Expand Down
8 changes: 2 additions & 6 deletions src/collector/ServicesClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import nodeFetch, { Request } from 'node-fetch';
import { retryableRequestError, fatalRequestError } from './error';
import { PaginatedResponse } from './types';
import { URLSearchParams } from 'url';

export interface ServicesClientInput {
hostname: string;
apiToken: string;
}
import { IntegrationConfig } from 'src/types';

/**
* Services Api
Expand All @@ -19,7 +15,7 @@ export class ServicesClient {
readonly hostname: string;
readonly apiToken: string;

constructor(config: ServicesClientInput) {
constructor(config: IntegrationConfig) {
this.hostname = config.hostname.toLowerCase().replace(/^https?:\/\//, '');
this.apiToken = config.apiToken;
}
Expand Down
7 changes: 4 additions & 3 deletions src/collector/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IntegrationInstance } from '@jupiterone/integration-sdk';
import { ServicesClient, ServicesClientInput } from './ServicesClient';
import { ServicesClient } from './ServicesClient';
import { IntegrationConfig } from 'src/types';

export * from './types';

Expand All @@ -8,9 +9,9 @@ export * from './types';
* api key.
*/
export function createServicesClient(
instance: IntegrationInstance,
instance: IntegrationInstance<IntegrationConfig>,
): ServicesClient {
const { hostname, apiToken } = instance.config as ServicesClientInput;
const { hostname, apiToken } = instance.config;

if (!hostname || !apiToken) {
throw new Error(
Expand Down
1 change: 1 addition & 0 deletions src/converter/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './entities';
export * from './relationships';
52 changes: 52 additions & 0 deletions src/converter/relationships.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
createIntegrationRelationship,
Entity,
Relationship,
RelationshipDirection,
convertProperties,
} from '@jupiterone/integration-sdk';

export const DEVICE_MANAGEMENT_RELATIONSHIP =
'snipeit_account_manages_hardware';

export const mapHardwareRelationship = (
account: Entity,
hardware: Entity,
filterKey: string,
): Relationship =>
createIntegrationRelationship({
_key: `${account._key}|manages|${hardware._key}`,
_class: 'MANAGES',
_type: DEVICE_MANAGEMENT_RELATIONSHIP,
_mapping: {
relationshipDirection: RelationshipDirection.FORWARD,
sourceEntityKey: account._key,
targetFilterKeys: [['_class', filterKey]],
targetEntity: {
...convertProperties(hardware),
_key: hardware._key,
_type: hardware._type,
_class: hardware._class,
},
},
});

export const mapHardwareLocationRelationship = (
hardware: Entity,
): Relationship =>
createIntegrationRelationship({
_key: `location:${hardware.locationId}|manages|${hardware._key}`,
_class: 'HAS',
_type: DEVICE_MANAGEMENT_RELATIONSHIP,
_mapping: {
relationshipDirection: RelationshipDirection.FORWARD,
sourceEntityKey: `location:${hardware.locationId}`,
targetFilterKeys: [['_class', 'id', 'locationId']],
targetEntity: {
_class: 'Device',
id: hardware.id,
locationId: hardware.locationId,
},
},
skipTargetCreation: true,
});
3 changes: 2 additions & 1 deletion src/steps/fetch-account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getAccountEntity,
getServiceEntity,
} from '../../converter';
import { IntegrationConfig } from 'src/types';

const step: IntegrationStep = {
id: 'fetch-account',
Expand All @@ -18,7 +19,7 @@ const step: IntegrationStep = {
async executionHandler({
instance,
jobState,
}: IntegrationStepExecutionContext) {
}: IntegrationStepExecutionContext<IntegrationConfig>) {
const client = createServicesClient(instance);

const accountEntity = getAccountEntity(instance);
Expand Down
24 changes: 17 additions & 7 deletions src/steps/fetch-hardware/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,25 @@ test('should process hardware entities', async () => {
const context = createStepContext();
await step.executionHandler(context);

expect(context.jobState.collectedEntities).toHaveLength(1328);
expect(context.jobState.collectedEntities).toEqual(
expect(context.jobState.collectedRelationships).toHaveLength(2651);
expect(context.jobState.collectedRelationships).toEqual(
expect.arrayContaining([
expect.objectContaining({
_type: 'hardware',
_class: ['Device'],
id: expect.any(String),
displayName: expect.any(String),
createdOn: expect.any(Number),
_key: expect.any(String),
_class: 'MANAGES',
_type: 'snipeit_account_manages_hardware',
_mapping: {
relationshipDirection: 'FORWARD',
sourceEntityKey: expect.any(String),
targetFilterKeys: [['_class', 'serial']],
targetEntity: expect.objectContaining({
_type: 'hardware',
_class: ['Device'],
id: expect.any(String),
displayName: expect.any(String),
createdOn: expect.any(Number),
}),
},
}),
]),
);
Expand Down
46 changes: 24 additions & 22 deletions src/steps/fetch-hardware/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
import {
IntegrationStep,
IntegrationStepExecutionContext,
createIntegrationRelationship,
} from '@jupiterone/integration-sdk';

import { IntegrationConfig } from 'src/types';
import { createServicesClient } from '../../collector';
import { convertHardware, getAccountEntity } from '../../converter';
import {
convertHardware,
getAccountEntity,
DEVICE_MANAGEMENT_RELATIONSHIP,
mapHardwareRelationship,
mapHardwareLocationRelationship,
} from '../../converter';

const step: IntegrationStep = {
id: 'fetch-hardware',
name: 'Fetch Snipe-IT listing of hardware assets',
types: ['hardware'],
types: ['hardware', DEVICE_MANAGEMENT_RELATIONSHIP],
async executionHandler({
instance,
jobState,
}: IntegrationStepExecutionContext) {
}: IntegrationStepExecutionContext<IntegrationConfig>) {
const client = createServicesClient(instance);
const accountEntity = getAccountEntity(instance);

const hardware = await client.listHardware();

const hardwareEntities = hardware.map(convertHardware);
await jobState.addEntities(hardwareEntities);

const relationships = [];
hardwareEntities.forEach((hardwareEntity) => {
relationships.push(
createIntegrationRelationship({
from: accountEntity,
to: hardwareEntity,
_class: 'MANAGES',
}),
);
if (hardwareEntity.locationId) {
if (hardwareEntity.macAddress) {
relationships.push(
mapHardwareRelationship(accountEntity, hardwareEntity, 'macAddress'),
);
} else if (hardwareEntity.serial) {
relationships.push(
createIntegrationRelationship({
fromType: 'location',
fromKey: `location:${hardwareEntity.locationId}`,
toType: hardwareEntity._type,
toKey: hardwareEntity._key,
_class: 'HAS',
}),
mapHardwareRelationship(accountEntity, hardwareEntity, 'serial'),
);
} else {
relationships.push(
mapHardwareRelationship(accountEntity, hardwareEntity, 'hardwareId'),
);
}

if (hardwareEntity.locationId) {
relationships.push(mapHardwareLocationRelationship(hardwareEntity));
}
});
await jobState.addRelationships(relationships);
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IntegrationConfig {
hostname: string;
apiToken: string;
}
3 changes: 2 additions & 1 deletion src/validateInvocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
} from '@jupiterone/integration-sdk';

import { createServicesClient } from './collector';
import { IntegrationConfig } from './types';

export default async function validateInvocation(
context: IntegrationExecutionContext,
context: IntegrationExecutionContext<IntegrationConfig>,
): Promise<void> {
try {
const client = createServicesClient(context.instance);
Expand Down
21 changes: 11 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -443,19 +443,19 @@
"@types/yargs" "^15.0.0"
chalk "^3.0.0"

"@jupiterone/data-model@^0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@jupiterone/data-model/-/data-model-0.5.2.tgz#0df52c376bc8517cc7986efd80ef11d07a4b62fe"
integrity sha512-pN3zDAhTbmqsQcTOet42aQ7yducMCt2/akj2k/Xfuh0oVflSG68EycXyqPKa+I+16FYBUtXDqjQ54Oc89XS4jg==
"@jupiterone/data-model@^0.6.0":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@jupiterone/data-model/-/data-model-0.6.2.tgz#6820fd0823589881cc9cb97b7347fb8e10b85d9d"
integrity sha512-GBkdmoTWbnA431GwFo3EhRdMqtPWeg+KcoMqPJytmMgyaDe+tOyJVRwU2YZ9cm4SSD3CEwZvePnhIUlcjTaBbQ==
dependencies:
ajv "^6.12.0"

"@jupiterone/integration-sdk@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@jupiterone/integration-sdk/-/integration-sdk-2.1.2.tgz#a33c7c5fe58b375303a4f56802bdbb3aa8f0a254"
integrity sha512-/3K6qaU0tzfON6+oemrArctJkaIxgDkxpE0s627LbHHe3QaEcO2rzL9etaO7cNjsaR8Q1tbiz6eLaX/2AgWJ5Q==
"@jupiterone/integration-sdk@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jupiterone/integration-sdk/-/integration-sdk-3.1.0.tgz#5b64c4f67ca822f1a68f4692ce07fe90a18a3789"
integrity sha512-L8IdGb+JW6SZ0cR1FJL2o6S9MheBGBLlpKoAnZFDZAMbb74Rk8hXlDG9xxKqknHzVKNxMNfDxCZrwc9j84z83Q==
dependencies:
"@jupiterone/data-model" "^0.5.2"
"@jupiterone/data-model" "^0.6.0"
"@lifeomic/alpha" "^1.1.3"
"@pollyjs/adapter-node-http" "^4.0.4"
"@pollyjs/core" "^4.0.4"
Expand All @@ -466,6 +466,7 @@
"@types/pollyjs__core" "^4.0.0"
"@types/pollyjs__persister" "^2.0.1"
async-sema "^3.1.0"
axios "^0.19.2"
bunyan "^1.8.12"
bunyan-format "^0.2.1"
chalk "^4.0.0"
Expand Down Expand Up @@ -1059,7 +1060,7 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==

axios@^0.19.1:
axios@^0.19.1, axios@^0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
Expand Down