@jupiterone/integration-sdk-cli 12.8.1 → 13.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/bocchi/actions/steps.d.ts +4 -0
- package/dist/src/bocchi/actions/steps.js +20 -0
- package/dist/src/bocchi/actions/steps.js.map +1 -0
- package/dist/src/bocchi/bocchi.d.ts +1 -0
- package/dist/src/bocchi/bocchi.js +260 -0
- package/dist/src/bocchi/bocchi.js.map +1 -0
- package/dist/src/bocchi/templates/partials/directRelationships.hbs +26 -0
- package/dist/src/bocchi/templates/partials/mappedRelationships.hbs +36 -0
- package/dist/src/bocchi/templates/partials/stepMap.hbs +47 -0
- package/dist/src/bocchi/templates/steps/child-singleton.ts.hbs +47 -0
- package/dist/src/bocchi/templates/steps/fetch-child-entities.ts.hbs +58 -0
- package/dist/src/bocchi/templates/steps/fetch-entities.ts.hbs +47 -0
- package/dist/src/bocchi/templates/steps/index.test.ts.hbs +41 -0
- package/dist/src/bocchi/templates/steps/singleton.ts.hbs +45 -0
- package/dist/src/bocchi/templates/steps/spec.ts.hbs +73 -0
- package/dist/src/bocchi/templates/top-level/.env.example.hbs +3 -0
- package/dist/src/bocchi/templates/top-level/.eslintignore.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/.eslintrc.hbs +6 -0
- package/dist/src/bocchi/templates/top-level/.github/workflows/build.yml.hbs +29 -0
- package/dist/src/bocchi/templates/top-level/.github/workflows/questions.yml.hbs +40 -0
- package/dist/src/bocchi/templates/top-level/.gitignore.hbs +8 -0
- package/dist/src/bocchi/templates/top-level/.node-version.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/.prettierignore.hbs +6 -0
- package/dist/src/bocchi/templates/top-level/CHANGELOG.md.hbs +9 -0
- package/dist/src/bocchi/templates/top-level/CODEOWNERS.hbs +3 -0
- package/dist/src/bocchi/templates/top-level/Dockerfile.hbs +25 -0
- package/dist/src/bocchi/templates/top-level/LICENSE.hbs +373 -0
- package/dist/src/bocchi/templates/top-level/README.md.hbs +114 -0
- package/dist/src/bocchi/templates/top-level/docs/development.md.hbs +28 -0
- package/dist/src/bocchi/templates/top-level/docs/jupiterone.md.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/docs/spec/index.ts.hbs +14 -0
- package/dist/src/bocchi/templates/top-level/husky.config.js.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/jest.config.js.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/jupiterone/questions/questions.yaml.hbs +16 -0
- package/dist/src/bocchi/templates/top-level/lint-staged.config.js.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/package.json.hbs +49 -0
- package/dist/src/bocchi/templates/top-level/prettier.config.js.hbs +1 -0
- package/dist/src/bocchi/templates/top-level/src/client.ts.hbs +116 -0
- package/dist/src/bocchi/templates/top-level/src/config.ts.hbs +41 -0
- package/dist/src/bocchi/templates/top-level/src/index.test.ts.hbs +6 -0
- package/dist/src/bocchi/templates/top-level/src/index.ts.hbs +14 -0
- package/dist/src/bocchi/templates/top-level/src/steps/constants.ts.hbs +60 -0
- package/dist/src/bocchi/templates/top-level/src/steps/converters.ts.hbs +37 -0
- package/dist/src/bocchi/templates/top-level/src/steps/index.ts.hbs +13 -0
- package/dist/src/bocchi/templates/top-level/src/steps/types.ts.hbs +6 -0
- package/dist/src/bocchi/templates/top-level/test/README.md.hbs +5 -0
- package/dist/src/bocchi/templates/top-level/test/config.ts.hbs +30 -0
- package/dist/src/bocchi/templates/top-level/test/recording.ts.hbs +74 -0
- package/dist/src/bocchi/templates/top-level/tsconfig.dist.json.hbs +13 -0
- package/dist/src/bocchi/templates/top-level/tsconfig.json.hbs +7 -0
- package/dist/src/bocchi/utils/types.d.ts +98 -0
- package/dist/src/bocchi/utils/types.js +10 -0
- package/dist/src/bocchi/utils/types.js.map +1 -0
- package/dist/src/commands/bocchi.d.ts +1 -0
- package/dist/src/commands/bocchi.js +29 -0
- package/dist/src/commands/bocchi.js.map +1 -0
- package/dist/src/commands/collect.js.map +1 -1
- package/dist/src/commands/diff.js.map +1 -1
- package/dist/src/commands/document.js.map +1 -1
- package/dist/src/commands/generate-ingestion-sources-config.js.map +1 -1
- package/dist/src/commands/generate-integration-graph-schema.js.map +1 -1
- 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/options.js.map +1 -1
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/validate-question-file.js.map +1 -1
- package/dist/src/commands/visualize-types.js.map +1 -1
- package/dist/src/config.js.map +1 -1
- package/dist/src/generator/actions.d.ts +2 -1
- package/dist/src/generator/actions.js +5 -1
- package/dist/src/generator/actions.js.map +1 -1
- package/dist/src/generator/entitiesFlow.js.map +1 -1
- package/dist/src/generator/newIntegration.js.map +1 -1
- package/dist/src/generator/relationshipsFlow.js.map +1 -1
- package/dist/src/generator/stepsFlow.js.map +1 -1
- package/dist/src/generator/util.js.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/log.js.map +1 -1
- package/dist/src/neo4j/neo4jGraphStore.js.map +1 -1
- package/dist/src/neo4j/neo4jUtilities.js.map +1 -1
- package/dist/src/neo4j/uploadToNeo4j.js.map +1 -1
- package/dist/src/neo4j/wipeNeo4j.js.map +1 -1
- package/dist/src/questions/managedQuestionFileValidator.js.map +1 -1
- package/dist/src/services/queryLanguage.js.map +1 -1
- package/dist/src/troubleshoot/utils.js.map +1 -1
- package/dist/src/utils/generateVisHTML.js.map +1 -1
- package/dist/src/utils/getSortedJupiterOneTypes.js.map +1 -1
- package/dist/src/visualization/createMappedRelationshipNodesAndEdges.js.map +1 -1
- package/dist/src/visualization/generateDependencyVisualization.js.map +1 -1
- package/dist/src/visualization/generateVisualization.js.map +1 -1
- package/dist/src/visualization/retrieveIntegrationData.js.map +1 -1
- package/dist/src/visualization/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/bocchi/README.md +95 -0
- package/src/bocchi/actions/steps.ts +17 -0
- package/src/bocchi/bocchi.ts +311 -0
- package/src/bocchi/docs/template/README.md +140 -0
- package/src/bocchi/docs/template/authentication.md +100 -0
- package/src/bocchi/docs/template/examples/example.json +127 -0
- package/src/bocchi/docs/template/examples/signalSciences.json +128 -0
- package/src/bocchi/docs/template/steps.md +656 -0
- package/src/bocchi/templates/partials/directRelationships.hbs +26 -0
- package/src/bocchi/templates/partials/mappedRelationships.hbs +36 -0
- package/src/bocchi/templates/partials/stepMap.hbs +47 -0
- package/src/bocchi/templates/steps/child-singleton.ts.hbs +47 -0
- package/src/bocchi/templates/steps/fetch-child-entities.ts.hbs +58 -0
- package/src/bocchi/templates/steps/fetch-entities.ts.hbs +47 -0
- package/src/bocchi/templates/steps/index.test.ts.hbs +41 -0
- package/src/bocchi/templates/steps/singleton.ts.hbs +45 -0
- package/src/bocchi/templates/steps/spec.ts.hbs +73 -0
- package/src/bocchi/templates/top-level/.env.example.hbs +3 -0
- package/src/bocchi/templates/top-level/.eslintignore.hbs +1 -0
- package/src/bocchi/templates/top-level/.eslintrc.hbs +6 -0
- package/src/bocchi/templates/top-level/.github/workflows/build.yml.hbs +29 -0
- package/src/bocchi/templates/top-level/.github/workflows/questions.yml.hbs +40 -0
- package/src/bocchi/templates/top-level/.gitignore.hbs +8 -0
- package/src/bocchi/templates/top-level/.node-version.hbs +1 -0
- package/src/bocchi/templates/top-level/.prettierignore.hbs +6 -0
- package/src/bocchi/templates/top-level/CHANGELOG.md.hbs +9 -0
- package/src/bocchi/templates/top-level/CODEOWNERS.hbs +3 -0
- package/src/bocchi/templates/top-level/Dockerfile.hbs +25 -0
- package/src/bocchi/templates/top-level/LICENSE.hbs +373 -0
- package/src/bocchi/templates/top-level/README.md.hbs +114 -0
- package/src/bocchi/templates/top-level/docs/development.md.hbs +28 -0
- package/src/bocchi/templates/top-level/docs/jupiterone.md.hbs +1 -0
- package/src/bocchi/templates/top-level/docs/spec/index.ts.hbs +14 -0
- package/src/bocchi/templates/top-level/husky.config.js.hbs +1 -0
- package/src/bocchi/templates/top-level/jest.config.js.hbs +1 -0
- package/src/bocchi/templates/top-level/jupiterone/questions/questions.yaml.hbs +16 -0
- package/src/bocchi/templates/top-level/lint-staged.config.js.hbs +1 -0
- package/src/bocchi/templates/top-level/package.json.hbs +49 -0
- package/src/bocchi/templates/top-level/prettier.config.js.hbs +1 -0
- package/src/bocchi/templates/top-level/src/client.ts.hbs +116 -0
- package/src/bocchi/templates/top-level/src/config.ts.hbs +41 -0
- package/src/bocchi/templates/top-level/src/index.test.ts.hbs +6 -0
- package/src/bocchi/templates/top-level/src/index.ts.hbs +14 -0
- package/src/bocchi/templates/top-level/src/steps/constants.ts.hbs +60 -0
- package/src/bocchi/templates/top-level/src/steps/converters.ts.hbs +37 -0
- package/src/bocchi/templates/top-level/src/steps/index.ts.hbs +13 -0
- package/src/bocchi/templates/top-level/src/steps/types.ts.hbs +6 -0
- package/src/bocchi/templates/top-level/test/README.md.hbs +5 -0
- package/src/bocchi/templates/top-level/test/config.ts.hbs +30 -0
- package/src/bocchi/templates/top-level/test/recording.ts.hbs +74 -0
- package/src/bocchi/templates/top-level/tsconfig.dist.json.hbs +13 -0
- package/src/bocchi/templates/top-level/tsconfig.json.hbs +7 -0
- package/src/bocchi/utils/types.ts +106 -0
- package/src/commands/bocchi.ts +28 -0
- package/src/commands/generate-ingestion-sources-config.ts +4 -5
- package/src/commands/index.ts +1 -0
- package/src/generator/actions.ts +13 -1
- package/src/index.ts +3 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# JupiterOne Integration
|
|
2
|
+
|
|
3
|
+
Learn about the data ingested, benefits of this integration, and how to use it
|
|
4
|
+
with JupiterOne in the [integration documentation](docs/jupiterone.md).
|
|
5
|
+
|
|
6
|
+
## Development
|
|
7
|
+
|
|
8
|
+
### Prerequisites
|
|
9
|
+
|
|
10
|
+
1. Install [Node.js](https://nodejs.org/) using the
|
|
11
|
+
[installer](https://nodejs.org/en/download/) or a version manager such as
|
|
12
|
+
[nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm).
|
|
13
|
+
2. Install [`yarn`](https://yarnpkg.com/getting-started/install) or
|
|
14
|
+
[`npm`](https://github.com/npm/cli#installation) to install dependencies.
|
|
15
|
+
3. Install dependencies with `yarn install`.
|
|
16
|
+
4. Register an account in the system this integration targets for ingestion and
|
|
17
|
+
obtain API credentials.
|
|
18
|
+
5. `cp .env.example .env` and add necessary values for runtime configuration.
|
|
19
|
+
|
|
20
|
+
When an integration executes, it needs API credentials and any other
|
|
21
|
+
configuration parameters necessary for its work (provider API credentials,
|
|
22
|
+
data ingestion parameters, etc.). The names of these parameters are defined
|
|
23
|
+
by the `IntegrationInstanceConfigFieldMap`in `src/config.ts`. When the
|
|
24
|
+
integration is executed outside the JupiterOne managed environment (local
|
|
25
|
+
development or on-prem), values for these parameters are read from Node's
|
|
26
|
+
`process.env` by converting config field names to constant case. For example,
|
|
27
|
+
`clientId` is read from `process.env.CLIENT_ID`.
|
|
28
|
+
|
|
29
|
+
The `.env` file is loaded into `process.env` before the integration code is
|
|
30
|
+
executed. This file is not required should you configure the environment
|
|
31
|
+
another way. `.gitignore` is configured to avoid committing the `.env` file.
|
|
32
|
+
|
|
33
|
+
### Running the integration
|
|
34
|
+
|
|
35
|
+
#### Running directly
|
|
36
|
+
|
|
37
|
+
1. `yarn start` to collect data
|
|
38
|
+
2. `yarn graph` to show a visualization of the collected data
|
|
39
|
+
3. `yarn j1-integration -h` for additional commands
|
|
40
|
+
|
|
41
|
+
#### Running with Docker
|
|
42
|
+
|
|
43
|
+
Create an integration instance for the integration in JupiterOne. With an
|
|
44
|
+
**JupiterOne API Key** scoped to the integration or an API Key with permissions
|
|
45
|
+
to synchronize data and the **Integration Instance ID**:
|
|
46
|
+
|
|
47
|
+
1. `docker build -t $IMAGE_NAME .`
|
|
48
|
+
2. `docker run -e "JUPITERONE_API_KEY=<JUPITERONE_API_KEY>" -e "JUPITERONE_ACCOUNT=<JUPITERONE_ACCOUNT> -e "INTEGRATION_INSTANCE_ID=<INTEGRATION_INSTANCE_ID>" "JUPITERONE_API_BASE_URL=<JUPITERONE_API_BASE_URL>" $IMAGE_NAME`
|
|
49
|
+
|
|
50
|
+
### Making Contributions
|
|
51
|
+
|
|
52
|
+
Start by taking a look at the source code. The integration is basically a set of
|
|
53
|
+
functions called steps, each of which ingests a collection of resources and
|
|
54
|
+
relationships. The goal is to limit each step to as few resource types as
|
|
55
|
+
possible so that should the ingestion of one type of data fail, it does not
|
|
56
|
+
necessarily prevent the ingestion of other, unrelated data. That should be
|
|
57
|
+
enough information to allow you to get started coding!
|
|
58
|
+
|
|
59
|
+
See the
|
|
60
|
+
[SDK development documentation](https://github.com/JupiterOne/sdk/blob/main/docs/integrations/development.md)
|
|
61
|
+
for a deep dive into the mechanics of how integrations work.
|
|
62
|
+
|
|
63
|
+
See [docs/development.md](docs/development.md) for any additional details about
|
|
64
|
+
developing this integration.
|
|
65
|
+
|
|
66
|
+
## Testing the integration
|
|
67
|
+
|
|
68
|
+
Ideally, all major calls to the API and converter functions would be tested. You
|
|
69
|
+
can run the tests with `yarn test`, and you can run the tests as they execute in
|
|
70
|
+
the CI/CD environment with `yarn test:ci` (adds linting and type-checking to
|
|
71
|
+
`yarn test`). If you have a valid runtime configuration, you can run the tests
|
|
72
|
+
with your credentials using `yarn test:env`.
|
|
73
|
+
|
|
74
|
+
For more details on setting up tests, and specifically on using recordings to
|
|
75
|
+
simulate API responses, see `test/README.md`.
|
|
76
|
+
|
|
77
|
+
### Changelog
|
|
78
|
+
|
|
79
|
+
The history of this integration's development can be viewed at
|
|
80
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
81
|
+
|
|
82
|
+
## Versioning this project
|
|
83
|
+
|
|
84
|
+
This project is versioned using [auto](https://intuit.github.io/auto/).
|
|
85
|
+
|
|
86
|
+
Versioning and publishing to NPM are now handled via adding GitHub labels to
|
|
87
|
+
pull requests. The following labels should be used for this process:
|
|
88
|
+
|
|
89
|
+
- patch
|
|
90
|
+
- minor
|
|
91
|
+
- major
|
|
92
|
+
- release
|
|
93
|
+
|
|
94
|
+
For each pull request, the degree of change should be registered by applying the
|
|
95
|
+
appropriate label of patch, minor, or major. This allows the repository to keep
|
|
96
|
+
track of the highest degree of change since the last release. When ready to
|
|
97
|
+
publish to NPM, the PR should have both its appropriate patch, minor, or major
|
|
98
|
+
label applied as well as a release label. The release label will denote to the
|
|
99
|
+
system that we need to publish to NPM and will correctly version based on the
|
|
100
|
+
highest degree of change since the last release, package the project, and
|
|
101
|
+
publish it to NPM.
|
|
102
|
+
|
|
103
|
+
In order to successfully version and publish to NPM we need access to two
|
|
104
|
+
secrets: a valid NPM token for publishing and a GitHub token for querying the
|
|
105
|
+
repo and pushing version changes. For JupiterOne projects please put in a ticket
|
|
106
|
+
with security to have the repository correctly granted access. For external
|
|
107
|
+
projects, please provide secrets with access to your own NPM and GitHub
|
|
108
|
+
accounts. The secret names should be set to NPM_AUTH_TOKEN and
|
|
109
|
+
AUTO_GITHUB_PAT_TOKEN respectively (or the action can be updated to accommodate
|
|
110
|
+
different naming conventions).
|
|
111
|
+
|
|
112
|
+
We are not currently using the functionality for auto to update the CHANGELOG.
|
|
113
|
+
As such, please remember to update CHANGELOG.md with the appropriate version,
|
|
114
|
+
date, and changes.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Add details here to give a brief overview of how to work with the provider APIs.
|
|
4
|
+
Please reference any SDKs or API docs used to help build the integration here.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
Supply details about software or tooling (like maybe Docker or Terraform) that
|
|
9
|
+
is needed for development here.
|
|
10
|
+
|
|
11
|
+
Please supply references to documentation that details how to install those
|
|
12
|
+
dependencies here.
|
|
13
|
+
|
|
14
|
+
Tools like Node.js and NPM are already covered in the [README](../README.md) so
|
|
15
|
+
don't bother documenting that here.
|
|
16
|
+
|
|
17
|
+
## Provider account setup
|
|
18
|
+
|
|
19
|
+
Please provide information about the steps needed to create an account with a
|
|
20
|
+
provider. Images and references to a provider's documentation is very helpful
|
|
21
|
+
for new developers picking up your work.
|
|
22
|
+
|
|
23
|
+
## Authentication
|
|
24
|
+
|
|
25
|
+
Supply details here for information on how to authenticate with a provider so
|
|
26
|
+
that developers have an idea of what's needed to hit APIs. It may be useful to
|
|
27
|
+
provide explanations for each value specified in the
|
|
28
|
+
[`IntegrationInstanceConfigFieldMap`](../src/config.ts).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# {{titleCase vendorName}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IntegrationSpecConfig } from '@jupiterone/integration-sdk-core';
|
|
2
|
+
import { IntegrationConfig } from '../../src/config';
|
|
3
|
+
{{#each template.steps}}
|
|
4
|
+
import { {{camelCase entity.name}}Spec } from './{{kebabCase id}}';
|
|
5
|
+
{{/each}}
|
|
6
|
+
|
|
7
|
+
export const invocationConfig: IntegrationSpecConfig<IntegrationConfig> =
|
|
8
|
+
{
|
|
9
|
+
integrationSteps: [
|
|
10
|
+
{{#each template.steps}}
|
|
11
|
+
...{{camelCase entity.name}}Spec,
|
|
12
|
+
{{/each}}
|
|
13
|
+
],
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('@jupiterone/integration-sdk-dev-tools/config/husky');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('@jupiterone/integration-sdk-dev-tools/config/jest');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
sourceId: managed:{{kebabCase vendorName}}
|
|
3
|
+
integrationDefinitionId: '${integration_definition_id}'
|
|
4
|
+
questions:
|
|
5
|
+
[]
|
|
6
|
+
# - id: integration-question-template-replace-me
|
|
7
|
+
# title: What kinds of questions will this integration support?
|
|
8
|
+
# description:
|
|
9
|
+
# TODO Every integration should contribute questions! Please be careful
|
|
10
|
+
# to replace this before deploying the integration to JupiterOne.
|
|
11
|
+
# queries:
|
|
12
|
+
# - name: good
|
|
13
|
+
# query: |
|
|
14
|
+
# find * with _integrationDefinitionId = '${integration_definition_id}'
|
|
15
|
+
# tags:
|
|
16
|
+
# - template
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('@jupiterone/integration-sdk-dev-tools/config/lint-staged');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "{{packageDescription}}",
|
|
5
|
+
"license": "MPL-2.0",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"jupiterone"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0 <20.x"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "j1-integration collect",
|
|
20
|
+
"graph": "j1-integration visualize",
|
|
21
|
+
"graph:types": "j1-integration visualize-types",
|
|
22
|
+
"graph:spec": "j1-integration visualize-types --project-path docs/spec --output-file ./.j1-integration/types-graph/index.spec.html",
|
|
23
|
+
"graph:dependencies": "j1-integration visualize-dependencies",
|
|
24
|
+
"validate:questions:dry": "j1-integration validate-question-file --dry-run",
|
|
25
|
+
"validate:questions": "j1-integration validate-question-file -a $MANAGED_QUESTIONS_JUPITERONE_ACCOUNT_ID -k $MANAGED_QUESTIONS_JUPITERONE_API_KEY",
|
|
26
|
+
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
|
27
|
+
"format": "prettier --write \"**/*.{ts,js,json,css,md,yml}\"",
|
|
28
|
+
"format:check": "prettier --check \"**/*.{ts,js,json,css,md,yml}\"",
|
|
29
|
+
"type-check": "tsc",
|
|
30
|
+
"test": "jest",
|
|
31
|
+
"test:env": "LOAD_ENV=1 yarn test",
|
|
32
|
+
"test:ci": "yarn format:check && yarn lint && yarn type-check && yarn test",
|
|
33
|
+
"build": "tsc -p tsconfig.dist.json --declaration && cp README.md dist/README.md && cp -r jupiterone/ dist/jupiterone/",
|
|
34
|
+
"build:docker": "tsc --declaration false --emitDeclarationOnly false -p tsconfig.dist.json",
|
|
35
|
+
"prepush": "yarn format:check && yarn lint && yarn type-check && jest --changedSince main",
|
|
36
|
+
"postversion": "cp package.json ./dist/package.json"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@jupiterone/integration-sdk-http-client": "^12.7.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@jupiterone/integration-sdk-core": "^12.7.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@jupiterone/integration-sdk-core": "^12.7.0",
|
|
46
|
+
"@jupiterone/integration-sdk-dev-tools": "^12.7.0",
|
|
47
|
+
"@jupiterone/integration-sdk-testing": "^12.7.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('@jupiterone/integration-sdk-dev-tools/config/prettier');
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { BaseAPIClient } from '@jupiterone/integration-sdk-http-client';
|
|
2
|
+
import { IntegrationConfig } from './config';
|
|
3
|
+
import {
|
|
4
|
+
{{#each template.steps}}
|
|
5
|
+
{{pascalCase entity.name}},
|
|
6
|
+
{{/each}}
|
|
7
|
+
} from './steps/types';
|
|
8
|
+
import { Entity, IntegrationLogger } from '@jupiterone/integration-sdk-core';
|
|
9
|
+
|
|
10
|
+
export type ResourceIteratee<T> = (each: T) => Promise<void> | void;
|
|
11
|
+
|
|
12
|
+
export class APIClient extends BaseAPIClient {
|
|
13
|
+
private authHeaders: Record<string, string>;
|
|
14
|
+
|
|
15
|
+
constructor(readonly config: IntegrationConfig, logger: IntegrationLogger) {
|
|
16
|
+
super({
|
|
17
|
+
baseUrl: '{{template.baseUrl}}',
|
|
18
|
+
logger,
|
|
19
|
+
{{#if template.tokenBucket}}
|
|
20
|
+
tokenBucket: {
|
|
21
|
+
maximumCapacity: {{template.tokenBucket.maximumCapacity}},
|
|
22
|
+
refillRate: {{template.tokenBucket.refillRate}}
|
|
23
|
+
}
|
|
24
|
+
{{/if}}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
{{#if (isNotEndpointAuth template.authentication.strategy)}}
|
|
29
|
+
protected getAuthorizationHeaders(): Record<string, string> {
|
|
30
|
+
return { {{#each (sanitizeAuthObject template.authentication.authHeaders)}}'{{@key}}': `{{this}}`,{{/each}} };
|
|
31
|
+
}
|
|
32
|
+
{{else}}
|
|
33
|
+
protected async getAuthorizationHeaders(): Promise<Record<string, string>> {
|
|
34
|
+
const response = await (await this.retryableRequest(
|
|
35
|
+
`${this.baseUrl}{{template.authentication.params.path}}`,
|
|
36
|
+
{
|
|
37
|
+
authorize: false, // needed only for authentication call
|
|
38
|
+
headers: { {{#each (sanitizeAuthObject template.authentication.params.headers)}}'{{@key}}': `{{this}}`,{{/each}} },
|
|
39
|
+
method: '{{sanitizeHttpMethod template.authentication.params.method}}',
|
|
40
|
+
{{#if request.params}}
|
|
41
|
+
body: { {{#each (sanitizeAuthObject template.authentication.params.body)}}{{@key}}: {{this}},{{/each}} }
|
|
42
|
+
{{/if}}
|
|
43
|
+
}
|
|
44
|
+
)).json();
|
|
45
|
+
return { {{#each (sanitizeAuthObject template.authentication.authHeaders)}}'{{@key}}': `{{this}}`,{{/each}} }
|
|
46
|
+
}
|
|
47
|
+
{{/if}}
|
|
48
|
+
|
|
49
|
+
public async verifyAuthentication(): Promise<void> {
|
|
50
|
+
{{#if (isNotEndpointAuth template.authentication.strategy)}}
|
|
51
|
+
// TODO: implement
|
|
52
|
+
await new Promise(() => { throw new Error('Unimplemented: Pick an API call to use for validation!') });
|
|
53
|
+
{{else}}
|
|
54
|
+
await this.getAuthorizationHeaders();
|
|
55
|
+
{{/if}}
|
|
56
|
+
}
|
|
57
|
+
{{#each template.steps}}
|
|
58
|
+
|
|
59
|
+
{{#if (isSingletonRequest response.responseType)}}
|
|
60
|
+
public async get{{pascalCase entity.name}}({{#if parentAssociation}}parentEntity: Entity{{/if}}): Promise<{{pascalCase entity.name}}> {
|
|
61
|
+
const url = `${this.baseUrl}{{{sanitizeUrlPath request.urlTemplate}}}`;
|
|
62
|
+
const response = await (await this.retryableRequest(
|
|
63
|
+
url,
|
|
64
|
+
{
|
|
65
|
+
method: '{{sanitizeHttpMethod request.method}}',
|
|
66
|
+
{{#if request.params}}
|
|
67
|
+
body: { {{#each (sanitizeHttpBody request.params)}}'{{@key}}': `{{this}}`,{{/each}} }
|
|
68
|
+
{{/if}}
|
|
69
|
+
}
|
|
70
|
+
)).json();
|
|
71
|
+
return response.{{response.dataPath}} as {{pascalCase entity.name}};
|
|
72
|
+
}
|
|
73
|
+
{{else}}
|
|
74
|
+
public async iterate{{pascalCase entity.name}}s(
|
|
75
|
+
{{#if parentAssociation}}
|
|
76
|
+
parentEntity: Entity,
|
|
77
|
+
{{/if}}
|
|
78
|
+
iteratee: ResourceIteratee<{{pascalCase entity.name}}>
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
{{#if response.nextTokenPath}}
|
|
81
|
+
let nextToken: string | undefined;
|
|
82
|
+
do {
|
|
83
|
+
{{/if}}
|
|
84
|
+
const url = `${this.baseUrl}{{{sanitizeUrlPath request.urlTemplate}}}`;
|
|
85
|
+
const response = await (await this.retryableRequest(
|
|
86
|
+
url,
|
|
87
|
+
{
|
|
88
|
+
method: '{{sanitizeHttpMethod request.method}}',
|
|
89
|
+
{{#if request.params}}
|
|
90
|
+
body: { {{#each (sanitizeHttpBody request.params)}}'{{@key}}': `{{this}}`,{{/each}} }
|
|
91
|
+
{{/if}}
|
|
92
|
+
}
|
|
93
|
+
)).json();
|
|
94
|
+
const resources = response.{{response.dataPath}} as {{pascalCase entity.name}}[];
|
|
95
|
+
for (const resource of resources) {
|
|
96
|
+
await iteratee(resource);
|
|
97
|
+
}
|
|
98
|
+
{{#if response.nextTokenPath}}
|
|
99
|
+
nextToken = response.{{response.nextTokenPath}};
|
|
100
|
+
} while (nextToken);
|
|
101
|
+
{{/if}}
|
|
102
|
+
}
|
|
103
|
+
{{/if}}
|
|
104
|
+
{{/each}}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let client: APIClient | undefined;
|
|
108
|
+
|
|
109
|
+
export function createAPIClient(
|
|
110
|
+
config: IntegrationConfig,
|
|
111
|
+
logger: IntegrationLogger
|
|
112
|
+
): APIClient {
|
|
113
|
+
return client
|
|
114
|
+
? client
|
|
115
|
+
: new APIClient(config, logger);
|
|
116
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{{#with template}}
|
|
2
|
+
import {
|
|
3
|
+
IntegrationExecutionContext,
|
|
4
|
+
IntegrationValidationError,
|
|
5
|
+
IntegrationInstanceConfigFieldMap,
|
|
6
|
+
} from '@jupiterone/integration-sdk-core';
|
|
7
|
+
import { createAPIClient } from './client';
|
|
8
|
+
|
|
9
|
+
export const instanceConfigFields = {
|
|
10
|
+
{{#each instanceConfigFields}}
|
|
11
|
+
{{@key}}: {
|
|
12
|
+
{{#if type}}type: '{{type}}',{{/if}}
|
|
13
|
+
{{#if mask}}mask: {{mask}},{{/if}}
|
|
14
|
+
{{#if optional}}optional: {{optional}},{{/if}}
|
|
15
|
+
},
|
|
16
|
+
{{/each}}
|
|
17
|
+
} satisfies IntegrationInstanceConfigFieldMap;
|
|
18
|
+
|
|
19
|
+
export interface IntegrationConfig {
|
|
20
|
+
{{#each instanceConfigFields}}
|
|
21
|
+
{{@key}}: {{configTypeToType type}};
|
|
22
|
+
{{/each}}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function validateInvocation(
|
|
26
|
+
context: IntegrationExecutionContext<IntegrationConfig>,
|
|
27
|
+
) {
|
|
28
|
+
const { config } = context.instance;
|
|
29
|
+
|
|
30
|
+
{{#with (requiredConfig instanceConfigFields)}}
|
|
31
|
+
{{#if this.length}}
|
|
32
|
+
if ({{#each this}}!config.{{this}}{{#unless @last}} || {{/unless}}{{/each}}) {
|
|
33
|
+
throw new IntegrationValidationError('Config requires all of{{#each this}} {{this}}{{/each}}');
|
|
34
|
+
}
|
|
35
|
+
{{/if}}
|
|
36
|
+
{{/with}}
|
|
37
|
+
|
|
38
|
+
const apiClient = createAPIClient(config);
|
|
39
|
+
await apiClient.verifyAuthentication();
|
|
40
|
+
}
|
|
41
|
+
{{/with}}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { invocationConfig as implementedConfig } from '.';
|
|
2
|
+
import { invocationConfig as specConfig } from '../docs/spec';
|
|
3
|
+
|
|
4
|
+
test('implemented integration should match spec', () => {
|
|
5
|
+
expect(implementedConfig).toImplementSpec(specConfig, { requireSpec: true });
|
|
6
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IntegrationInvocationConfig } from '@jupiterone/integration-sdk-core';
|
|
2
|
+
import { integrationSteps } from './steps';
|
|
3
|
+
import {
|
|
4
|
+
validateInvocation,
|
|
5
|
+
IntegrationConfig,
|
|
6
|
+
instanceConfigFields,
|
|
7
|
+
} from './config';
|
|
8
|
+
|
|
9
|
+
export const invocationConfig: IntegrationInvocationConfig<IntegrationConfig> =
|
|
10
|
+
{
|
|
11
|
+
instanceConfigFields,
|
|
12
|
+
validateInvocation,
|
|
13
|
+
integrationSteps,
|
|
14
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{{#with template}}
|
|
2
|
+
import {
|
|
3
|
+
RelationshipClass,
|
|
4
|
+
StepEntityMetadata,
|
|
5
|
+
StepRelationshipMetadata,
|
|
6
|
+
} from '@jupiterone/integration-sdk-core';
|
|
7
|
+
|
|
8
|
+
export const Steps = {
|
|
9
|
+
{{#each steps}}
|
|
10
|
+
{{constantCase id}}: '{{id}}',
|
|
11
|
+
{{#if directRelationships}}
|
|
12
|
+
{{#with (getDirectRelationships this)}}
|
|
13
|
+
{{#each this}}
|
|
14
|
+
BUILD_{{constantCase sourceStep.entity.name}}_{{constantCase targetStep.entity.name}}_RELATIONSHIPS: 'build-{{kebabCase sourceStep.entity.name}}-{{kebabCase targetStep.entity.name}}-relationships',
|
|
15
|
+
{{/each}}
|
|
16
|
+
{{/with}}
|
|
17
|
+
{{/if}}
|
|
18
|
+
{{#if mappedRelationships}}
|
|
19
|
+
BUILD_{{constantCase entity.name}}_MAPPED_RELATIONSHIPS: 'build-{{kebabCase entity.name}}-mapped-relationships',
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{/each}}
|
|
22
|
+
} satisfies Record<string, string>;
|
|
23
|
+
|
|
24
|
+
export const Entities = {
|
|
25
|
+
{{#each steps}}
|
|
26
|
+
{{#with entity}}
|
|
27
|
+
{{constantCase name}}: {
|
|
28
|
+
resourceName: '{{name}}',
|
|
29
|
+
_type: '{{_type}}',
|
|
30
|
+
_class: ['{{_class}}'],
|
|
31
|
+
},
|
|
32
|
+
{{/with}}
|
|
33
|
+
{{/each}}
|
|
34
|
+
} satisfies Record<string, StepEntityMetadata>;
|
|
35
|
+
|
|
36
|
+
export const Relationships = {
|
|
37
|
+
{{#each steps}}
|
|
38
|
+
{{#if parentAssociation}}
|
|
39
|
+
{{#with (getStepByType parentAssociation.parentEntityType)}}{{constantCase entity.name}}{{/with}}_{{constantCase parentAssociation.relationshipClass}}_{{constantCase entity.name}}: {
|
|
40
|
+
sourceType: Entities.{{#with (getStepByType parentAssociation.parentEntityType)}}{{constantCase entity.name}}{{/with}}._type,
|
|
41
|
+
_class: RelationshipClass.{{constantCase parentAssociation.relationshipClass}},
|
|
42
|
+
targetType: Entities.{{constantCase entity.name}}._type,
|
|
43
|
+
_type: '{{getRelationshipType parentAssociation.relationshipClass parentAssociation.parentEntityType entity._type}}',
|
|
44
|
+
},
|
|
45
|
+
{{/if}}
|
|
46
|
+
{{#if directRelationships}}
|
|
47
|
+
{{#with (getDirectRelationships this)}}
|
|
48
|
+
{{#each this}}
|
|
49
|
+
{{constantCase sourceStep.entity.name}}_{{constantCase relationshipClass}}_{{constantCase targetStep.entity.name}}: {
|
|
50
|
+
sourceType: Entities.{{constantCase sourceStep.entity.name}}._type,
|
|
51
|
+
_class: RelationshipClass.{{constantCase relationshipClass}},
|
|
52
|
+
targetType: Entities.{{constantCase targetStep.entity.name}}._type,
|
|
53
|
+
_type: '{{getRelationshipType relationshipClass sourceStep.entity._type targetStep.entity._type}}',
|
|
54
|
+
},
|
|
55
|
+
{{/each}}
|
|
56
|
+
{{/with}}
|
|
57
|
+
{{/if}}
|
|
58
|
+
{{/each}}
|
|
59
|
+
} satisfies Record<string, StepRelationshipMetadata>;
|
|
60
|
+
{{/with}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{{#with template}}
|
|
2
|
+
import { createIntegrationEntity } from '@jupiterone/integration-sdk-core';
|
|
3
|
+
import { Entities } from './constants';
|
|
4
|
+
import {
|
|
5
|
+
{{#each steps}}
|
|
6
|
+
{{pascalCase entity.name}},
|
|
7
|
+
{{/each}}
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
{{#each steps}}
|
|
11
|
+
{{#with entity}}
|
|
12
|
+
export function create{{pascalCase name}}Entity(data: {{pascalCase name}}) {
|
|
13
|
+
return createIntegrationEntity({
|
|
14
|
+
entityData: {
|
|
15
|
+
source: data,
|
|
16
|
+
assign: {
|
|
17
|
+
_key: data.{{_keyPath}},
|
|
18
|
+
_type: Entities.{{constantCase name}}._type,
|
|
19
|
+
_class: Entities.{{constantCase name}}._class,
|
|
20
|
+
{{#if staticFields}}
|
|
21
|
+
{{#each staticFields}}
|
|
22
|
+
{{@key}}: {{{escape this}}},
|
|
23
|
+
{{/each}}
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#if fieldMappings}}
|
|
26
|
+
{{#each fieldMappings}}
|
|
27
|
+
{{@key}}: data.{{this}},
|
|
28
|
+
{{/each}}
|
|
29
|
+
{{/if}}
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
{{/with}}
|
|
35
|
+
|
|
36
|
+
{{/each}}
|
|
37
|
+
{{/with}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{{#with template}}
|
|
2
|
+
{{#each steps}}
|
|
3
|
+
import { {{camelCase entity.name}}Steps } from './{{id}}';
|
|
4
|
+
{{/each}}
|
|
5
|
+
|
|
6
|
+
const integrationSteps = [
|
|
7
|
+
{{#each steps}}
|
|
8
|
+
...{{camelCase entity.name}}Steps,
|
|
9
|
+
{{/each}}
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export { integrationSteps };
|
|
13
|
+
{{/with}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IntegrationInvocationConfig } from '@jupiterone/integration-sdk-core';
|
|
2
|
+
import { StepTestConfig } from '@jupiterone/integration-sdk-testing';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { invocationConfig } from '../src';
|
|
6
|
+
import { IntegrationConfig } from '../src/config';
|
|
7
|
+
|
|
8
|
+
if (process.env.LOAD_ENV) {
|
|
9
|
+
dotenv.config({
|
|
10
|
+
path: path.join(__dirname, '../.env'),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
{{#each template.instanceConfigFields}}
|
|
15
|
+
const DEFAULT_{{constantCase @key}} = '';
|
|
16
|
+
{{/each}}
|
|
17
|
+
|
|
18
|
+
export const integrationConfig: IntegrationConfig = {
|
|
19
|
+
{{#each template.instanceConfigFields}}
|
|
20
|
+
{{camelCase @key}}: process.env.{{constantCase @key}} ?? DEFAULT_{{constantCase @key}},
|
|
21
|
+
{{/each}}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function buildStepTestConfigForStep(stepId: string): StepTestConfig {
|
|
25
|
+
return {
|
|
26
|
+
stepId,
|
|
27
|
+
instanceConfig: integrationConfig,
|
|
28
|
+
invocationConfig: invocationConfig as IntegrationInvocationConfig,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setupRecording,
|
|
3
|
+
Recording,
|
|
4
|
+
SetupRecordingInput,
|
|
5
|
+
mutations,
|
|
6
|
+
} from '@jupiterone/integration-sdk-testing';
|
|
7
|
+
|
|
8
|
+
export { Recording };
|
|
9
|
+
|
|
10
|
+
export function setupProjectRecording(
|
|
11
|
+
input: Omit<SetupRecordingInput, 'mutateEntry'>,
|
|
12
|
+
): Recording {
|
|
13
|
+
return setupRecording({
|
|
14
|
+
...input,
|
|
15
|
+
redactedRequestHeaders: ['Authorization'],
|
|
16
|
+
redactedResponseHeaders: ['set-cookie'],
|
|
17
|
+
mutateEntry: mutations.unzipGzippedRecordingEntry,
|
|
18
|
+
/*mutateEntry: (entry) => {
|
|
19
|
+
redact(entry);
|
|
20
|
+
},*/
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// a more sophisticated redaction example below:
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
function getRedactedOAuthResponse() {
|
|
28
|
+
return {
|
|
29
|
+
access_token: '[REDACTED]',
|
|
30
|
+
expires_in: 9999,
|
|
31
|
+
token_type: 'Bearer',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function redact(entry): void {
|
|
36
|
+
if (entry.request.postData) {
|
|
37
|
+
entry.request.postData.text = '[REDACTED]';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!entry.response.content.text) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//let's unzip the entry so we can modify it
|
|
45
|
+
mutations.unzipGzippedRecordingEntry(entry);
|
|
46
|
+
|
|
47
|
+
//we can just get rid of all response content if this was the token call
|
|
48
|
+
const requestUrl = entry.request.url;
|
|
49
|
+
if (requestUrl.match(/oauth\/token/)) {
|
|
50
|
+
entry.response.content.text = JSON.stringify(getRedactedOAuthResponse());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//if it wasn't a token call, parse the response text, removing any carriage returns or newlines
|
|
55
|
+
const responseText = entry.response.content.text;
|
|
56
|
+
const parsedResponseText = JSON.parse(responseText.replace(/\r?\n|\r/g, ''));
|
|
57
|
+
|
|
58
|
+
//now we can modify the returned object as desired
|
|
59
|
+
//in this example, if the return text is an array of objects that have the 'tenant' property...
|
|
60
|
+
if (parsedResponseText[0]?.tenant) {
|
|
61
|
+
for (let i = 0; i < parsedResponseText.length; i++) {
|
|
62
|
+
parsedResponseText[i].client_secret = '[REDACTED]';
|
|
63
|
+
parsedResponseText[i].jwt_configuration = '[REDACTED]';
|
|
64
|
+
parsedResponseText[i].signing_keys = '[REDACTED]';
|
|
65
|
+
parsedResponseText[i].encryption_key = '[REDACTED]';
|
|
66
|
+
parsedResponseText[i].addons = '[REDACTED]';
|
|
67
|
+
parsedResponseText[i].client_metadata = '[REDACTED]';
|
|
68
|
+
parsedResponseText[i].mobile = '[REDACTED]';
|
|
69
|
+
parsedResponseText[i].native_social_login = '[REDACTED]';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
entry.response.content.text = JSON.stringify(parsedResponseText);
|
|
74
|
+
} */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"include": ["src", "package.json"],
|
|
4
|
+
"exclude": [
|
|
5
|
+
"dist",
|
|
6
|
+
"**/*.test.ts",
|
|
7
|
+
"**/*.test.js",
|
|
8
|
+
"**/*/__tests__/**/*.ts",
|
|
9
|
+
"**/*/__tests__/**/*.js",
|
|
10
|
+
"**/*/__mocks__/**/*.ts",
|
|
11
|
+
"**/*/__mocks__/**/*.js"
|
|
12
|
+
]
|
|
13
|
+
}
|