@jupiterone/integration-sdk-cli 7.4.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.js +1 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/neo4j.d.ts +2 -0
- package/dist/src/commands/neo4j.js +70 -0
- package/dist/src/commands/neo4j.js.map +1 -0
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/neo4j/index.d.ts +2 -0
- package/dist/src/neo4j/index.js +15 -0
- package/dist/src/neo4j/index.js.map +1 -0
- package/dist/src/neo4j/neo4jGraphStore.d.ts +23 -0
- package/dist/src/neo4j/neo4jGraphStore.js +134 -0
- package/dist/src/neo4j/neo4jGraphStore.js.map +1 -0
- package/dist/src/neo4j/neo4jUtilities.d.ts +4 -0
- package/dist/src/neo4j/neo4jUtilities.js +46 -0
- package/dist/src/neo4j/neo4jUtilities.js.map +1 -0
- package/dist/src/neo4j/uploadToNeo4j.d.ts +9 -0
- package/dist/src/neo4j/uploadToNeo4j.js +33 -0
- package/dist/src/neo4j/uploadToNeo4j.js.map +1 -0
- package/dist/src/neo4j/wipeNeo4j.d.ts +14 -0
- package/dist/src/neo4j/wipeNeo4j.js +41 -0
- package/dist/src/neo4j/wipeNeo4j.js.map +1 -0
- package/dist/tsconfig.dist.tsbuildinfo +4073 -1228
- package/package.json +6 -4
- package/src/commands/index.ts +1 -0
- package/src/commands/neo4j.ts +63 -0
- package/src/index.ts +3 -1
- package/src/neo4j/README.md +42 -0
- package/src/neo4j/__tests__/neo4jGraphStore.test.ts +99 -0
- package/src/neo4j/__tests__/neo4jUtilities.test.ts +25 -0
- package/src/neo4j/index.ts +2 -0
- package/src/neo4j/neo4jGraphStore.ts +139 -0
- package/src/neo4j/neo4jUtilities.ts +43 -0
- package/src/neo4j/uploadToNeo4j.ts +47 -0
- package/src/neo4j/wipeNeo4j.ts +58 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupiterone/integration-sdk-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "The SDK for developing JupiterOne integrations",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -22,19 +22,20 @@
|
|
|
22
22
|
"prepack": "yarn build:dist"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@jupiterone/integration-sdk-runtime": "^
|
|
25
|
+
"@jupiterone/integration-sdk-runtime": "^8.0.0",
|
|
26
26
|
"commander": "^5.0.0",
|
|
27
27
|
"globby": "^11.0.0",
|
|
28
28
|
"js-yaml": "^4.1.0",
|
|
29
29
|
"json-diff": "^0.5.4",
|
|
30
30
|
"lodash": "^4.17.19",
|
|
31
31
|
"markdown-table": "^2.0.0",
|
|
32
|
+
"neo4j-driver": "^4.3.3",
|
|
32
33
|
"runtypes": "5.1.0",
|
|
33
34
|
"upath": "^1.2.0",
|
|
34
35
|
"vis": "^4.21.0-EOL"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@jupiterone/integration-sdk-private-test-utils": "^
|
|
38
|
+
"@jupiterone/integration-sdk-private-test-utils": "^8.0.0",
|
|
38
39
|
"@pollyjs/adapter-node-http": "^5.1.1",
|
|
39
40
|
"@pollyjs/core": "^5.1.1",
|
|
40
41
|
"@pollyjs/persister-fs": "^5.1.1",
|
|
@@ -46,7 +47,8 @@
|
|
|
46
47
|
"@types/pollyjs__persister": "^4.3.1",
|
|
47
48
|
"@types/vis": "^4.21.20",
|
|
48
49
|
"memfs": "^3.2.0",
|
|
50
|
+
"neo-forgery": "^2.0.0",
|
|
49
51
|
"uuid": "^8.2.0"
|
|
50
52
|
},
|
|
51
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "855a502aabe8b12c6373dc2cef6add7a2106c153"
|
|
52
54
|
}
|
package/src/commands/index.ts
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as commander from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import dotenvExpand from 'dotenv-expand';
|
|
6
|
+
|
|
7
|
+
import * as log from '../log';
|
|
8
|
+
import { uploadToNeo4j, wipeNeo4jByID, wipeAllNeo4j } from '../neo4j';
|
|
9
|
+
|
|
10
|
+
export function neo4j() {
|
|
11
|
+
dotenvExpand(dotenv.config());
|
|
12
|
+
|
|
13
|
+
const program = new commander.Command();
|
|
14
|
+
program.description(`Suite of neo4j commands. Options are currently 'neo4j push', 'neo4j wipe', and 'neo4j wipe-all'`);
|
|
15
|
+
const neo4jCommand = program.command('neo4j');
|
|
16
|
+
neo4jCommand
|
|
17
|
+
.command('push')
|
|
18
|
+
.description('upload collected entities and relationships to local Neo4j')
|
|
19
|
+
.option(
|
|
20
|
+
'-d, --data-dir <directory>',
|
|
21
|
+
'path to collected entities and relationships',
|
|
22
|
+
path.resolve(process.cwd(), '.j1-integration'),
|
|
23
|
+
)
|
|
24
|
+
.option(
|
|
25
|
+
'-i, --integration-instance-id <id>',
|
|
26
|
+
'_integrationInstanceId assigned to uploaded entities',
|
|
27
|
+
'defaultLocalInstanceID'
|
|
28
|
+
)
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
log.info(`Beginning data upload to local neo4j`);
|
|
31
|
+
// Point `fileSystem.ts` functions to expected location relative to
|
|
32
|
+
// integration project path.
|
|
33
|
+
const finalDir = path.resolve(process.cwd(), options.dataDir);
|
|
34
|
+
process.env.JUPITERONE_INTEGRATION_STORAGE_DIRECTORY = finalDir;
|
|
35
|
+
|
|
36
|
+
await uploadToNeo4j({
|
|
37
|
+
pathToData: finalDir,
|
|
38
|
+
integrationInstanceID: options.integrationInstanceId
|
|
39
|
+
});
|
|
40
|
+
log.info(`Data uploaded to local neo4j`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
neo4jCommand
|
|
44
|
+
.command('wipe')
|
|
45
|
+
.description('wipe entities and relationships for a given integrationInstanceID in the Neo4j database')
|
|
46
|
+
.option(
|
|
47
|
+
'-i, --integration-instance-id <id>',
|
|
48
|
+
'_integrationInstanceId assigned to uploaded entities',
|
|
49
|
+
'defaultLocalInstanceID'
|
|
50
|
+
)
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
await wipeNeo4jByID({integrationInstanceID: options.integrationInstanceId});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
neo4jCommand
|
|
56
|
+
.command('wipe-all')
|
|
57
|
+
.description('wipe all entities and relationships in the Neo4j database')
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
await wipeAllNeo4j({});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return neo4jCommand;
|
|
63
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
visualize,
|
|
10
10
|
visualizeTypes,
|
|
11
11
|
validateQuestionFile,
|
|
12
|
+
neo4j,
|
|
12
13
|
} from './commands';
|
|
13
14
|
|
|
14
15
|
export function createCli() {
|
|
@@ -20,5 +21,6 @@ export function createCli() {
|
|
|
20
21
|
.addCommand(run())
|
|
21
22
|
.addCommand(visualizeTypes())
|
|
22
23
|
.addCommand(document())
|
|
23
|
-
.addCommand(validateQuestionFile())
|
|
24
|
+
.addCommand(validateQuestionFile())
|
|
25
|
+
.addCommand(neo4j());
|
|
24
26
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Neo4j JupiterOne CLI Command
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
This command assumes you have three additional values stored in your
|
|
6
|
+
local .env file:
|
|
7
|
+
NEO4J_URI
|
|
8
|
+
NEO4J_USER
|
|
9
|
+
NEO4J_PASSWORD
|
|
10
|
+
|
|
11
|
+
This can be used for uploading to local or remote Neo4j databases. If
|
|
12
|
+
SSL is needed for a remote connection, specify `bolt+s` or `bolt+ssc`
|
|
13
|
+
in the URI. For easy access to a local Neo4j instance, you can launch
|
|
14
|
+
one via a Neo4j provided Docker image with the command:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
docker run \
|
|
18
|
+
-p 7474:7474 -p 7687:7687 \
|
|
19
|
+
-d \
|
|
20
|
+
-v $PWD/.neo4j/data:/data \
|
|
21
|
+
-v $PWD/.neo4j/logs:/logs \
|
|
22
|
+
-v $PWD/.neo4j/import:/var/lib/neo4j/import \
|
|
23
|
+
-v $PWD/.neo4j/plugins:/plugins \
|
|
24
|
+
--env NEO4J_AUTH=neo4j/devpass \
|
|
25
|
+
neo4j:latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you would like to use a different username and password, the NEO4J_AUTH
|
|
29
|
+
value can be modified to whatever username/password you prefer.
|
|
30
|
+
|
|
31
|
+
NOTE: Future updates are planned to streamline this without removing
|
|
32
|
+
the option of pushing to an external Neo4j database.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Data is still collected in the same way as before with a call to `yarn start`.
|
|
37
|
+
|
|
38
|
+
Once data has been collected, you can run `j1-integration neo4j push`. This will
|
|
39
|
+
push data to the Neo4j server listed in the NEO4J_URI .env parameter. If
|
|
40
|
+
running locally, you can then access data in the Neo4j database by visiting
|
|
41
|
+
http://localhost:7474. Alternatively, you can download the full Neo4j
|
|
42
|
+
Desktop application at https://neo4j.com/download/.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mockDriver,
|
|
3
|
+
mockSessionFromQuerySet,
|
|
4
|
+
QuerySpec
|
|
5
|
+
} from 'neo-forgery';
|
|
6
|
+
import * as neo4j from 'neo4j-driver';
|
|
7
|
+
import { Neo4jGraphStore } from '../neo4jGraphStore';
|
|
8
|
+
import { Entity, Relationship } from '@jupiterone/integration-sdk-core';
|
|
9
|
+
|
|
10
|
+
const testInstanceID = 'testInstanceID';
|
|
11
|
+
const testEntityData: Entity[] = [{
|
|
12
|
+
_type: "testType",
|
|
13
|
+
_class: "testClass",
|
|
14
|
+
_key: "testKey",
|
|
15
|
+
}];
|
|
16
|
+
const testRelationshipData: Relationship[] = [{
|
|
17
|
+
_fromEntityKey: "testKey1",
|
|
18
|
+
_toEntityKey: "testKey2",
|
|
19
|
+
_type: "testRelType",
|
|
20
|
+
_key: "relKey",
|
|
21
|
+
_class: "testRelationshipClass",
|
|
22
|
+
}];
|
|
23
|
+
const constraintCall = 'CREATE CONSTRAINT unique_testType IF NOT EXISTS ON (n:testType) ASSERT n._key IS UNIQUE;'
|
|
24
|
+
const addEntityCall = `MERGE (n:testType {_key: 'testKey', _integrationInstanceID: '${testInstanceID}'}) SET n._type = 'testType', n._class = 'testClass';`;
|
|
25
|
+
const addRelationshipCall = `
|
|
26
|
+
MATCH (start {_key: 'testKey1', _integrationInstanceID: '${testInstanceID}'})
|
|
27
|
+
MATCH (end {_key: 'testKey2', _integrationInstanceID: '${testInstanceID}'})
|
|
28
|
+
MERGE (start)-[relationship:testRelType]->(end);`
|
|
29
|
+
const wipeByIDCall = `MATCH (n {_integrationInstanceID: '${testInstanceID}'}) DETACH DELETE n`;
|
|
30
|
+
const wipeAllCall = 'MATCH (n) DETACH DELETE n';
|
|
31
|
+
const querySet: QuerySpec[] = [{
|
|
32
|
+
name: 'addConstraint',
|
|
33
|
+
query: constraintCall,
|
|
34
|
+
params: undefined,
|
|
35
|
+
output: {records:[]}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'addEntity',
|
|
39
|
+
query: addEntityCall,
|
|
40
|
+
params: undefined,
|
|
41
|
+
output: {records:[]}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'addRelationship',
|
|
45
|
+
query: addRelationshipCall,
|
|
46
|
+
params: undefined,
|
|
47
|
+
output: {records:[]}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'wipeByID',
|
|
51
|
+
query: wipeByIDCall,
|
|
52
|
+
params: undefined,
|
|
53
|
+
output: {records:[]}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'wipeAll',
|
|
57
|
+
query: wipeAllCall,
|
|
58
|
+
params: undefined,
|
|
59
|
+
output: {records:[]}
|
|
60
|
+
}];
|
|
61
|
+
|
|
62
|
+
describe('#neo4jGraphStore', () => {
|
|
63
|
+
const mockDriverResp = mockDriver();
|
|
64
|
+
const mockSession = mockSessionFromQuerySet(querySet);
|
|
65
|
+
const store = new Neo4jGraphStore({
|
|
66
|
+
uri: '',
|
|
67
|
+
username: '',
|
|
68
|
+
password: '',
|
|
69
|
+
integrationInstanceID: testInstanceID,
|
|
70
|
+
session: mockSession,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should generate call to create a driver connection', () => {
|
|
74
|
+
const spy = jest.spyOn(neo4j, 'driver').mockReturnValue(mockDriverResp);
|
|
75
|
+
|
|
76
|
+
const emptyStore = new Neo4jGraphStore({
|
|
77
|
+
uri: '',
|
|
78
|
+
username: '',
|
|
79
|
+
password: '',
|
|
80
|
+
integrationInstanceID: testInstanceID,
|
|
81
|
+
});
|
|
82
|
+
expect(async () => await emptyStore.close()).toReturn;
|
|
83
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should generate call to create an Entity', () => {
|
|
87
|
+
expect(async () => await store.addEntities(testEntityData)).toReturn;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should generate call to create a Relationship', () => {
|
|
91
|
+
|
|
92
|
+
expect(async () => await store.addRelationships(testRelationshipData)).toReturn;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should generate call to wipe by ID', () => {
|
|
96
|
+
expect(async () => await store.wipeInstanceIdData()).toReturn;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { startsWithNumeric, sanitizePropertyName, sanitizeValue, buildPropertyParameters } from '../neo4jUtilities';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe('#neo4jUtilities', () => {
|
|
5
|
+
test('should return true for string starting with a numeric', () => {
|
|
6
|
+
const testNameResults: boolean = startsWithNumeric('1testname');
|
|
7
|
+
expect(testNameResults).toEqual(true);
|
|
8
|
+
});
|
|
9
|
+
test('should return false for string not starting with a numeric', () => {
|
|
10
|
+
const testTrailingNumeric: boolean = startsWithNumeric('another1testname');
|
|
11
|
+
expect(testTrailingNumeric).toEqual(false);
|
|
12
|
+
});
|
|
13
|
+
test('should sanitize property name properly', () => {
|
|
14
|
+
const testSanitize: string = sanitizePropertyName(`1a!b@c#d$e%f^g&h*i(j)k-l=m+n\\o|p'q"r;s:t/u?v.w,x>y<z\`1~2\t3\n4[5]6{7}8 90`);
|
|
15
|
+
expect(testSanitize).toEqual('n1a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z_1_2_3_4_5_6_7_8_90');
|
|
16
|
+
});
|
|
17
|
+
test('should sanitize value properly', () => {
|
|
18
|
+
const testSanitize: string = sanitizeValue('1a!b@c#d$e%f^g&h*i(j)k-l=m+n\\o|p\'q"r;s:t/u?v.w,x>y<z`1~2\t3\n4[5]6{7}8 90');
|
|
19
|
+
expect(testSanitize).toEqual('1a!b@c#d$e%f^g&h*i(j)k-l=m+n\\o|p\'q\\"r;s:t/u?v.w,x>y<z`1~2\t3\n4[5]6{7}8 90');
|
|
20
|
+
});
|
|
21
|
+
test('should build property string correctly including sanitization', () => {
|
|
22
|
+
const testPropResults: Object = buildPropertyParameters({test: '123', '1sanitize1hi&$abc d': '1h"i&$abc d'});
|
|
23
|
+
expect(testPropResults).toEqual({test:'123', n1sanitize1hi__abc_d:'1h\\"i&$abc d'});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Entity, Relationship } from '@jupiterone/integration-sdk-core';
|
|
2
|
+
import { sanitizeValue, buildPropertyParameters } from './neo4jUtilities';
|
|
3
|
+
|
|
4
|
+
import * as neo4j from 'neo4j-driver';
|
|
5
|
+
|
|
6
|
+
export interface Neo4jGraphObjectStoreParams {
|
|
7
|
+
uri: string;
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
integrationInstanceID: string,
|
|
11
|
+
session?: neo4j.Session
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Neo4jGraphStore {
|
|
15
|
+
private neo4jDriver: neo4j.Driver;
|
|
16
|
+
private persistedSession: neo4j.Session;
|
|
17
|
+
private databaseName = 'neo4j';
|
|
18
|
+
private typeList = new Set<string>();
|
|
19
|
+
private integrationInstanceID: string;
|
|
20
|
+
|
|
21
|
+
constructor(params: Neo4jGraphObjectStoreParams) {
|
|
22
|
+
if(params.session) {
|
|
23
|
+
this.persistedSession = params.session;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.neo4jDriver = neo4j.driver(
|
|
27
|
+
params.uri,
|
|
28
|
+
neo4j.auth.basic(params.username, params.password),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
this.integrationInstanceID = params.integrationInstanceID;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async runCypherCommand(cypherCommand: string, cypherParameters?: any): Promise<neo4j.Result> {
|
|
35
|
+
if(this.persistedSession) {
|
|
36
|
+
const result = await this.persistedSession.run(cypherCommand);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const session = this.neo4jDriver.session({
|
|
41
|
+
database: this.databaseName,
|
|
42
|
+
defaultAccessMode: neo4j.session.WRITE,
|
|
43
|
+
});
|
|
44
|
+
const result = await session.run(cypherCommand, cypherParameters);
|
|
45
|
+
await session.close();
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async addEntities(newEntities: Entity[]) {
|
|
51
|
+
const nodeAlias: string = 'entityNode';
|
|
52
|
+
for (const entity of newEntities) {
|
|
53
|
+
//Add index if not already in types. This will optimize future
|
|
54
|
+
//MATCH/MERGE calls.
|
|
55
|
+
if (!this.typeList.has(entity._type)) {
|
|
56
|
+
await this.runCypherCommand(
|
|
57
|
+
`CREATE INDEX index_${entity._type} IF NOT EXISTS FOR (n:${entity._type}) ON (n._key, n._integrationInstanceID);`,
|
|
58
|
+
);
|
|
59
|
+
this.typeList.add(entity._type);
|
|
60
|
+
}
|
|
61
|
+
const propertyParameters = buildPropertyParameters(entity);
|
|
62
|
+
const finalKeyValue = sanitizeValue(entity._key.toString());
|
|
63
|
+
const buildCommand = `
|
|
64
|
+
MERGE (${nodeAlias} {_key: $finalKeyValue, _integrationInstanceID: $integrationInstanceID})
|
|
65
|
+
SET ${nodeAlias} += $propertyParameters
|
|
66
|
+
SET ${nodeAlias}:${entity._type};`;
|
|
67
|
+
await this.runCypherCommand(buildCommand, {
|
|
68
|
+
propertyParameters: propertyParameters,
|
|
69
|
+
finalKeyValue: finalKeyValue,
|
|
70
|
+
integrationInstanceID: this.integrationInstanceID
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async addRelationships(newRelationships: Relationship[]) {
|
|
76
|
+
for (const relationship of newRelationships) {
|
|
77
|
+
const relationshipAlias: string = 'relationship';
|
|
78
|
+
const propertyParameters = buildPropertyParameters(relationship);
|
|
79
|
+
|
|
80
|
+
let startEntityKey = '';
|
|
81
|
+
let endEntityKey = '';
|
|
82
|
+
//Get start and end _keys. Will be overwritten if we're
|
|
83
|
+
//working with a mapped relationship.
|
|
84
|
+
if (relationship._fromEntityKey) {
|
|
85
|
+
startEntityKey = sanitizeValue(relationship._fromEntityKey.toString());
|
|
86
|
+
}
|
|
87
|
+
if(relationship._toEntityKey) {
|
|
88
|
+
endEntityKey = sanitizeValue(relationship._toEntityKey.toString());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if(relationship._mapping) { //Mapped Relationship
|
|
92
|
+
if(relationship._mapping['skipTargetCreation'] === false) {
|
|
93
|
+
//Create target entity first
|
|
94
|
+
const tempEntity: Entity = {
|
|
95
|
+
_class: relationship._mapping['targetEntity']._class,
|
|
96
|
+
//TODO, I think this key is wrong, but not sure what else to use
|
|
97
|
+
_key: sanitizeValue(relationship._key.replace(relationship._mapping['sourceEntityKey'], '')),
|
|
98
|
+
_type: relationship._mapping['targetEntity']._type,
|
|
99
|
+
}
|
|
100
|
+
await this.addEntities([tempEntity]);
|
|
101
|
+
}
|
|
102
|
+
startEntityKey = sanitizeValue(relationship._mapping['sourceEntityKey']);
|
|
103
|
+
// TODO, see above. This key might also be an issue for the same reason
|
|
104
|
+
endEntityKey = sanitizeValue(relationship._key.replace(relationship._mapping['sourceEntityKey'], ''));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const buildCommand = `
|
|
108
|
+
MERGE (start {_key: $startEntityKey, _integrationInstanceID: $integrationInstanceID})
|
|
109
|
+
MERGE (end {_key: $endEntityKey, _integrationInstanceID: $integrationInstanceID})
|
|
110
|
+
MERGE (start)-[${relationshipAlias}:${relationship._type}]->(end)
|
|
111
|
+
SET ${relationshipAlias} += $propertyParameters;`;
|
|
112
|
+
await this.runCypherCommand(buildCommand, {
|
|
113
|
+
propertyParameters: propertyParameters,
|
|
114
|
+
startEntityKey: startEntityKey,
|
|
115
|
+
endEntityKey: endEntityKey,
|
|
116
|
+
integrationInstanceID: this.integrationInstanceID
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// TODO, if we get to very large databases we could reach a size where
|
|
122
|
+
// one or both both of the below wipe commands can't be easily executed
|
|
123
|
+
// in memory. At that time, we should consider requiring/using the APOC
|
|
124
|
+
// library so we can use apoc.periodic.iterate. Leaving out for now,
|
|
125
|
+
// since that would further complicate the Neo4j database setup.
|
|
126
|
+
async wipeInstanceIdData() {
|
|
127
|
+
const wipeCypherCommand = `MATCH (n {_integrationInstanceID: '${this.integrationInstanceID}'}) DETACH DELETE n`;
|
|
128
|
+
await this.runCypherCommand(wipeCypherCommand);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async wipeDatabase() {
|
|
132
|
+
const wipeCypherCommand = `MATCH (n) DETACH DELETE n`;
|
|
133
|
+
await this.runCypherCommand(wipeCypherCommand);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async close() {
|
|
137
|
+
await this.neo4jDriver.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
export function startsWithNumeric(str: string): boolean{
|
|
3
|
+
return /^\d/.test(str);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function sanitizePropertyName(propertyName: string): string {
|
|
7
|
+
let sanitizedName = '';
|
|
8
|
+
if(startsWithNumeric(propertyName)) {
|
|
9
|
+
sanitizedName += 'n';
|
|
10
|
+
}
|
|
11
|
+
sanitizedName += propertyName;
|
|
12
|
+
sanitizedName = sanitizedName.replace(/[\s!@#$%^&*()\-=+\\|'";:/?.,><`~\t\n[\]{}]/g, "_");
|
|
13
|
+
return sanitizedName;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sanitizeValue(value: string): string {
|
|
17
|
+
return value.replace(/"/gi, '\\"')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildPropertyParameters(propList: Object) {
|
|
21
|
+
const propertyParameters = {};
|
|
22
|
+
for (const key in propList) {
|
|
23
|
+
if (key === '_rawData') {
|
|
24
|
+
//stringify JSON in rawData so we can store it.
|
|
25
|
+
propertyParameters[key] = `"${JSON.stringify(propList[key])}"`;
|
|
26
|
+
} else {
|
|
27
|
+
// Sanitize out characters that aren't allowed in property names
|
|
28
|
+
const propertyName = sanitizePropertyName(key);
|
|
29
|
+
|
|
30
|
+
//If we're dealing with a number or boolean, leave alone, otherwise
|
|
31
|
+
//wrap in single quotes to convert to a string and escape all
|
|
32
|
+
//other single quotes so they don't terminate strings prematurely.
|
|
33
|
+
if(typeof propList[key] == 'number' || typeof propList[key] == 'boolean') {
|
|
34
|
+
propertyParameters[propertyName] = propList[key];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
propertyParameters[propertyName] = sanitizeValue(propList[key].toString());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return propertyParameters;
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Neo4jGraphStore } from './neo4jGraphStore';
|
|
2
|
+
import { iterateParsedGraphFiles, isDirectoryPresent } from '@jupiterone/integration-sdk-runtime';
|
|
3
|
+
import { FlushedGraphObjectData } from '@jupiterone/integration-sdk-runtime/src/storage/types';
|
|
4
|
+
|
|
5
|
+
type UploadToNeo4jParams = {
|
|
6
|
+
pathToData: string;
|
|
7
|
+
integrationInstanceID: string;
|
|
8
|
+
neo4jUri?: string;
|
|
9
|
+
neo4jUser?: string;
|
|
10
|
+
neo4jPassword?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function uploadToNeo4j({
|
|
14
|
+
pathToData,
|
|
15
|
+
integrationInstanceID,
|
|
16
|
+
neo4jUri = process.env.NEO4J_URI,
|
|
17
|
+
neo4jUser = process.env.NEO4J_USER,
|
|
18
|
+
neo4jPassword = process.env.NEO4J_PASSWORD,
|
|
19
|
+
}: UploadToNeo4jParams) {
|
|
20
|
+
if (!neo4jUri || !neo4jUser || !neo4jPassword) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'ERROR: must provide login information in function call or include NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD files in your .env file!',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (!isDirectoryPresent(pathToData)) {
|
|
26
|
+
throw new Error('ERROR: graph directory does not exist!');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const store = new Neo4jGraphStore({
|
|
30
|
+
uri: neo4jUri,
|
|
31
|
+
username: neo4jUser,
|
|
32
|
+
password: neo4jPassword,
|
|
33
|
+
integrationInstanceID: integrationInstanceID,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function handleGraphObjectFile(parsedData: FlushedGraphObjectData) {
|
|
37
|
+
if (parsedData.entities) await store.addEntities(parsedData.entities);
|
|
38
|
+
if (parsedData.relationships)
|
|
39
|
+
await store.addRelationships(parsedData.relationships);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await iterateParsedGraphFiles(handleGraphObjectFile, pathToData);
|
|
44
|
+
} finally {
|
|
45
|
+
await store.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Neo4jGraphStore } from './neo4jGraphStore';
|
|
2
|
+
|
|
3
|
+
type WipeNeo4jParams = {
|
|
4
|
+
integrationInstanceID: string;
|
|
5
|
+
neo4jUri?: string;
|
|
6
|
+
neo4jUser?: string;
|
|
7
|
+
neo4jPassword?: string;
|
|
8
|
+
}
|
|
9
|
+
export async function wipeNeo4jByID({
|
|
10
|
+
integrationInstanceID,
|
|
11
|
+
neo4jUri = process.env.NEO4J_URI,
|
|
12
|
+
neo4jUser = process.env. NEO4J_USER,
|
|
13
|
+
neo4jPassword = process.env. NEO4J_PASSWORD,
|
|
14
|
+
}: WipeNeo4jParams) {
|
|
15
|
+
if(!neo4jUri || !neo4jUser || !neo4jPassword) {
|
|
16
|
+
throw new Error('ERROR: must provide login information in function call or include NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD files in your .env file!');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const store = new Neo4jGraphStore({
|
|
20
|
+
uri: neo4jUri,
|
|
21
|
+
username: neo4jUser,
|
|
22
|
+
password: neo4jPassword,
|
|
23
|
+
integrationInstanceID: integrationInstanceID,
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
await store.wipeInstanceIdData();
|
|
27
|
+
} finally {
|
|
28
|
+
await store.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type WipeAllNeo4jParams = {
|
|
33
|
+
neo4jUri?: string;
|
|
34
|
+
neo4jUser?: string;
|
|
35
|
+
neo4jPassword?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function wipeAllNeo4j({
|
|
39
|
+
neo4jUri = process.env.NEO4J_URI,
|
|
40
|
+
neo4jUser = process.env. NEO4J_USER,
|
|
41
|
+
neo4jPassword = process.env. NEO4J_PASSWORD,
|
|
42
|
+
}: WipeAllNeo4jParams) {
|
|
43
|
+
if(!neo4jUri || !neo4jUser || !neo4jPassword) {
|
|
44
|
+
throw new Error('ERROR: must provide login information in function call or include NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD files in your .env file!');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const store = new Neo4jGraphStore({
|
|
48
|
+
uri: neo4jUri,
|
|
49
|
+
username: neo4jUser,
|
|
50
|
+
password: neo4jPassword,
|
|
51
|
+
integrationInstanceID: '',
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
await store.wipeDatabase();
|
|
55
|
+
} finally {
|
|
56
|
+
await store.close();
|
|
57
|
+
}
|
|
58
|
+
}
|