@jupiterone/integration-sdk-cli 8.34.0 → 9.0.0-beta.1

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.
@@ -0,0 +1,185 @@
1
+ import {
2
+ IntegrationIngestionConfigFieldMap,
3
+ IntegrationStep,
4
+ IntegrationInstanceConfig,
5
+ } from '@jupiterone/integration-sdk-core';
6
+ import { generateIngestionSourcesConfig } from './generate-ingestion-sources-config';
7
+
8
+ describe('#generateIngestionSourcesConfig', () => {
9
+ const INGESTION_SOURCE_IDS = {
10
+ FINDING_ALERTS: 'finding-alerts',
11
+ FETCH_REPOS: 'fetch-repos',
12
+ TEST_SOURCE: 'test-source',
13
+ };
14
+
15
+ const ingestionConfig: IntegrationIngestionConfigFieldMap = {
16
+ [INGESTION_SOURCE_IDS.FINDING_ALERTS]: {
17
+ title: 'Finding Alerts',
18
+ description:
19
+ 'Dependabot vulnerability alert ingestion and Code scanning alerts',
20
+ defaultsToDisabled: true,
21
+ },
22
+ [INGESTION_SOURCE_IDS.FETCH_REPOS]: {
23
+ title: 'Fetch repos',
24
+ description: 'This is an ingestion source created for test purposes',
25
+ defaultsToDisabled: false,
26
+ },
27
+ };
28
+
29
+ it('should return the ingestionConfig with empty childIngestionSources', () => {
30
+ const integrationSteps: IntegrationStep<IntegrationInstanceConfig>[] = [
31
+ {
32
+ id: 'fetch-vulnerability-alerts',
33
+ name: 'Fetch Vulnerability Alerts',
34
+ entities: [
35
+ {
36
+ resourceName: 'GitHub Vulnerability Alerts',
37
+ _type: 'github_finding',
38
+ _class: ['Finding'],
39
+ },
40
+ ],
41
+ relationships: [],
42
+ dependsOn: ['fetch-repos'],
43
+ ingestionSourceId: INGESTION_SOURCE_IDS.FINDING_ALERTS,
44
+ executionHandler: jest.fn(),
45
+ },
46
+ ];
47
+ const ingestionSourcesConfig = generateIngestionSourcesConfig(
48
+ ingestionConfig,
49
+ integrationSteps,
50
+ );
51
+ expect(
52
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.FINDING_ALERTS],
53
+ ).toMatchObject(ingestionConfig[INGESTION_SOURCE_IDS.FINDING_ALERTS]);
54
+ // childIngestionSources is empty because there are no steps that depends on fetch-vulnerability-alerts
55
+ expect(
56
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.FINDING_ALERTS]
57
+ .childIngestionSources,
58
+ ).toBeEmpty();
59
+ // ingestionSourcesConfig[INGESTION_SOURCE_IDS.FETCH_REPOS] is undefined because there are no steps using that ingestionSourceId
60
+ expect(
61
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.FETCH_REPOS],
62
+ ).toBeUndefined();
63
+ });
64
+
65
+ it('should return the ingestionConfig with childIngestionSources', () => {
66
+ const integrationSteps: IntegrationStep<IntegrationInstanceConfig>[] = [
67
+ {
68
+ id: 'fetch-repos',
69
+ name: 'Fetch Repos',
70
+ entities: [
71
+ {
72
+ resourceName: 'Github Repo',
73
+ _type: 'github_repo',
74
+ _class: ['CodeRepo'],
75
+ },
76
+ ],
77
+ relationships: [],
78
+ dependsOn: ['fetch-account'],
79
+ ingestionSourceId: INGESTION_SOURCE_IDS.FETCH_REPOS,
80
+ executionHandler: jest.fn(),
81
+ },
82
+ {
83
+ id: 'fetch-vulnerability-alerts',
84
+ name: 'Fetch Vulnerability Alerts',
85
+ entities: [
86
+ {
87
+ resourceName: 'GitHub Vulnerability Alerts',
88
+ _type: 'github_finding',
89
+ _class: ['Finding'],
90
+ },
91
+ ],
92
+ relationships: [],
93
+ dependsOn: ['fetch-repos'],
94
+ ingestionSourceId: INGESTION_SOURCE_IDS.FINDING_ALERTS,
95
+ executionHandler: jest.fn(),
96
+ },
97
+ {
98
+ id: 'fetch-issues',
99
+ name: 'Fetch Issues',
100
+ entities: [
101
+ {
102
+ resourceName: 'GitHub Issue',
103
+ _type: 'github_issue',
104
+ _class: ['Issue'],
105
+ },
106
+ ],
107
+ relationships: [],
108
+ dependsOn: ['fetch-repos', 'fetch-users', 'fetch-collaborators'],
109
+ executionHandler: jest.fn(),
110
+ },
111
+ {
112
+ id: 'fetch-teams',
113
+ name: 'Fetch Teams',
114
+ entities: [
115
+ {
116
+ resourceName: 'GitHub Team',
117
+ _type: 'github_team',
118
+ _class: ['UserGroup'],
119
+ },
120
+ ],
121
+ relationships: [],
122
+ dependsOn: ['fetch-account'],
123
+ executionHandler: jest.fn(),
124
+ },
125
+ ];
126
+ const ingestionSourcesConfig = generateIngestionSourcesConfig(
127
+ ingestionConfig,
128
+ integrationSteps,
129
+ );
130
+ // Original object doesn't change
131
+ expect(
132
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.FETCH_REPOS],
133
+ ).toMatchObject(ingestionConfig[INGESTION_SOURCE_IDS.FETCH_REPOS]);
134
+ // New property added
135
+ expect(
136
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.FETCH_REPOS]
137
+ .childIngestionSources,
138
+ ).toEqual(['fetch-vulnerability-alerts', 'fetch-issues']);
139
+ // For FINDING_ALERTS the ingestionConfig keep exactly the same
140
+ expect(
141
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.FINDING_ALERTS],
142
+ ).toMatchObject(ingestionConfig[INGESTION_SOURCE_IDS.FINDING_ALERTS]);
143
+ });
144
+
145
+ it('should not add the source if it does not exist in the ingestionConfig', () => {
146
+ const integrationSteps: IntegrationStep<IntegrationInstanceConfig>[] = [
147
+ {
148
+ id: 'fetch-repos',
149
+ name: 'Fetch Repos',
150
+ entities: [
151
+ {
152
+ resourceName: 'Github Repo',
153
+ _type: 'github_repo',
154
+ _class: ['CodeRepo'],
155
+ },
156
+ ],
157
+ relationships: [],
158
+ dependsOn: ['fetch-account'],
159
+ ingestionSourceId: INGESTION_SOURCE_IDS.TEST_SOURCE,
160
+ executionHandler: jest.fn(),
161
+ },
162
+ {
163
+ id: 'fetch-issues',
164
+ name: 'Fetch Issues',
165
+ entities: [
166
+ {
167
+ resourceName: 'GitHub Issue',
168
+ _type: 'github_issue',
169
+ _class: ['Issue'],
170
+ },
171
+ ],
172
+ relationships: [],
173
+ dependsOn: ['fetch-repos', 'fetch-users', 'fetch-collaborators'],
174
+ executionHandler: jest.fn(),
175
+ },
176
+ ];
177
+ const ingestionSourcesConfig = generateIngestionSourcesConfig(
178
+ ingestionConfig,
179
+ integrationSteps,
180
+ );
181
+ expect(
182
+ ingestionSourcesConfig[INGESTION_SOURCE_IDS.TEST_SOURCE],
183
+ ).toBeUndefined();
184
+ });
185
+ });
@@ -0,0 +1,112 @@
1
+ import {
2
+ IntegrationIngestionConfigField,
3
+ IntegrationIngestionConfigFieldMap,
4
+ IntegrationSourceId,
5
+ Step,
6
+ StepExecutionContext,
7
+ } from '@jupiterone/integration-sdk-core';
8
+ import { createCommand } from 'commander';
9
+ import { loadConfigFromTarget } from '../config';
10
+ import { promises as fs } from 'fs';
11
+ import * as log from '../log';
12
+
13
+ /* eslint-disable no-console */
14
+ export function generateIngestionSourcesConfigCommand() {
15
+ return createCommand('generate-ingestion-sources-config')
16
+ .description(
17
+ 'generate ingestion sources config from ingestion config and steps data',
18
+ )
19
+ .option(
20
+ '-o, --output-file <path>',
21
+ 'project relative path to generated ingestion sources config file',
22
+ )
23
+ .option(
24
+ '-p, --project-path <directory>',
25
+ 'path to integration project directory',
26
+ process.cwd(),
27
+ )
28
+ .action(async (options) => {
29
+ const { projectPath, outputFile } = options;
30
+
31
+ log.info(
32
+ `Generating ingestion sources config (projectPath=${projectPath}, outputFile=${outputFile})`,
33
+ );
34
+ const config = await loadConfigFromTarget(projectPath);
35
+ if (!config.ingestionConfig) {
36
+ log.info(
37
+ 'Skipping the generation of ingestion sources config file as there is no ingestionConfig present.',
38
+ );
39
+ } else {
40
+ const ingestionSourcesConfig = generateIngestionSourcesConfig(
41
+ config.ingestionConfig,
42
+ config.integrationSteps,
43
+ );
44
+ if (outputFile) {
45
+ await fs.writeFile(
46
+ outputFile,
47
+ JSON.stringify(ingestionSourcesConfig),
48
+ {
49
+ encoding: 'utf-8',
50
+ },
51
+ );
52
+ } else {
53
+ console.log(JSON.stringify(ingestionSourcesConfig, null, 2));
54
+ }
55
+ log.info('Successfully generated ingestion sources config file');
56
+ }
57
+ });
58
+ }
59
+
60
+ export type EnhancedIntegrationIngestionConfigFieldMap = Record<
61
+ IntegrationSourceId,
62
+ IntegrationIngestionConfigField & { childIngestionSources?: string[] }
63
+ >;
64
+
65
+ /**
66
+ * Generates an ingestionConfig with childIngestionSources taking into account
67
+ * the integration steps that come as argument.
68
+ * The childIngestionSources will be the list of stepIds that have any dependencies
69
+ * on the steps that match the ingestion sources specified.
70
+ *
71
+ * @export
72
+ * @template TStepExecutionContext
73
+ * @param {IntegrationIngestionConfigData} ingestionConfigData ingestionData without childIngestionSources
74
+ * @param {Step<TStepExecutionContext>[]} integrationSteps total list of integration steps
75
+ * @return {*} {IntegrationIngestionConfigFieldMap} ingestionData with childIngestionSources
76
+ */
77
+ export function generateIngestionSourcesConfig<
78
+ TStepExecutionContext extends StepExecutionContext,
79
+ >(
80
+ ingestionConfig: IntegrationIngestionConfigFieldMap,
81
+ integrationSteps: Step<TStepExecutionContext>[],
82
+ ): EnhancedIntegrationIngestionConfigFieldMap {
83
+ const newIngestionConfig: EnhancedIntegrationIngestionConfigFieldMap = {};
84
+ Object.keys(ingestionConfig).forEach((key) => {
85
+ if (ingestionConfig[key]) {
86
+ // Get the stepIds that match the current ingestionSourceId
87
+ const matchedIntegrationStepIds = integrationSteps
88
+ .filter((step) => step.ingestionSourceId === key)
89
+ .map(({ id }) => id);
90
+ if (!matchedIntegrationStepIds.length) {
91
+ // Skip iteration if there are no steps pointing to the current ingestionSourceId
92
+ return;
93
+ }
94
+ // Get the stepIds that have any dependencies on the matched step ids
95
+ const childIngestionSources = integrationSteps
96
+ .filter((step) =>
97
+ step.dependsOn?.some((value) =>
98
+ matchedIntegrationStepIds.includes(value),
99
+ ),
100
+ )
101
+ .map(({ id }) => id);
102
+ // Generate ingestionConfig with the childIngestionSources
103
+ newIngestionConfig[key] = {
104
+ ...ingestionConfig[key],
105
+ childIngestionSources,
106
+ };
107
+ } else {
108
+ log.warn(`The key ${key} does not exist in the ingestionConfig`);
109
+ }
110
+ });
111
+ return newIngestionConfig;
112
+ }
@@ -7,8 +7,7 @@ import {
7
7
  StepRelationshipMetadata,
8
8
  } from '@jupiterone/integration-sdk-core';
9
9
  import { createCommand } from 'commander';
10
- import path from 'path';
11
- import { IntegrationInvocationConfigLoadError, loadConfig } from '../config';
10
+ import { loadConfigFromTarget } from '../config';
12
11
  import { promises as fs } from 'fs';
13
12
  import * as log from '../log';
14
13
 
@@ -51,48 +50,6 @@ export function generateIntegrationGraphSchemaCommand() {
51
50
  });
52
51
  }
53
52
 
54
- function loadConfigFromSrc(projectPath: string) {
55
- return loadConfig(path.join(projectPath, 'src'));
56
- }
57
-
58
- function loadConfigFromDist(projectPath: string) {
59
- return loadConfig(path.join(projectPath, 'dist'));
60
- }
61
-
62
- /**
63
- * The way that integration npm packages are distributed has changed over time.
64
- * This function handles different cases where the invocation config has
65
- * traditionally lived to support backwards compatibility and make adoption
66
- * easier.
67
- */
68
- async function loadConfigFromTarget(projectPath: string) {
69
- let configFromSrcErr: Error | undefined;
70
- let configFromDistErr: Error | undefined;
71
-
72
- try {
73
- const configFromSrc = await loadConfigFromSrc(projectPath);
74
- return configFromSrc;
75
- } catch (err) {
76
- configFromSrcErr = err;
77
- }
78
-
79
- try {
80
- const configFromDist = await loadConfigFromDist(projectPath);
81
- return configFromDist;
82
- } catch (err) {
83
- configFromDistErr = err;
84
- }
85
-
86
- const combinedError = configFromDistErr
87
- ? configFromSrcErr + ', ' + configFromDistErr
88
- : configFromSrcErr;
89
-
90
- throw new IntegrationInvocationConfigLoadError(
91
- 'Error loading integration invocation configuration. Ensure "invocationConfig" is exported from src/index or dist/index. Additional details: ' +
92
- combinedError,
93
- );
94
- }
95
-
96
53
  type IntegrationGraphSchemaEntityMetadata = {
97
54
  resourceName: string;
98
55
  _class: string | string[];
@@ -10,3 +10,4 @@ export * from './neo4j';
10
10
  export * from './visualize-dependencies';
11
11
  export * from './generate-integration-graph-schema';
12
12
  export * from './troubleshoot';
13
+ export * from './generate-ingestion-sources-config';
package/src/config.ts CHANGED
@@ -51,6 +51,48 @@ export function loadInvocationConfig(
51
51
  return integrationModule.invocationConfig as IntegrationInvocationConfig;
52
52
  }
53
53
 
54
+ function loadConfigFromSrc(projectPath: string) {
55
+ return loadConfig(path.join(projectPath, 'src'));
56
+ }
57
+
58
+ function loadConfigFromDist(projectPath: string) {
59
+ return loadConfig(path.join(projectPath, 'dist'));
60
+ }
61
+
62
+ /**
63
+ * The way that integration npm packages are distributed has changed over time.
64
+ * This function handles different cases where the invocation config has
65
+ * traditionally lived to support backwards compatibility and make adoption
66
+ * easier.
67
+ */
68
+ export async function loadConfigFromTarget(projectPath: string) {
69
+ let configFromSrcErr: Error | undefined;
70
+ let configFromDistErr: Error | undefined;
71
+
72
+ try {
73
+ const configFromSrc = await loadConfigFromSrc(projectPath);
74
+ return configFromSrc;
75
+ } catch (err) {
76
+ configFromSrcErr = err;
77
+ }
78
+
79
+ try {
80
+ const configFromDist = await loadConfigFromDist(projectPath);
81
+ return configFromDist;
82
+ } catch (err) {
83
+ configFromDistErr = err;
84
+ }
85
+
86
+ const combinedError = configFromDistErr
87
+ ? configFromSrcErr + ', ' + configFromDistErr
88
+ : configFromSrcErr;
89
+
90
+ throw new IntegrationInvocationConfigLoadError(
91
+ 'Error loading integration invocation configuration. Ensure "invocationConfig" is exported from src/index or dist/index. Additional details: ' +
92
+ combinedError,
93
+ );
94
+ }
95
+
54
96
  async function isTypescriptPresent(
55
97
  projectSourceDirectory: string = path.join(process.cwd(), 'src'),
56
98
  ) {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  neo4j,
13
13
  visualizeDependencies,
14
14
  generateIntegrationGraphSchemaCommand,
15
+ generateIngestionSourcesConfigCommand,
15
16
  troubleshootLocalExecution,
16
17
  } from './commands';
17
18
 
@@ -28,5 +29,6 @@ export function createCli() {
28
29
  .addCommand(neo4j())
29
30
  .addCommand(visualizeDependencies())
30
31
  .addCommand(generateIntegrationGraphSchemaCommand())
32
+ .addCommand(generateIngestionSourcesConfigCommand())
31
33
  .addCommand(troubleshootLocalExecution());
32
34
  }