├── npm-inventory ├── .eslintignore ├── .prettierignore ├── .gitignore ├── tsconfig.dist.json ├── tsconfig.json ├── .eslintrc ├── LICENSE ├── package.json ├── README.md └── src │ └── index.ts ├── ingest-cyclonedx-sbom ├── .eslintignore ├── .prettierignore ├── .gitignore ├── tsconfig.dist.json ├── tsconfig.json ├── .eslintrc ├── LICENSE ├── src │ ├── types.ts │ └── index.ts ├── package.json └── README.md ├── software-bill-of-materials ├── .eslintignore ├── .gitignore ├── tsconfig.dist.json ├── tsconfig.json ├── .eslintrc ├── README.md ├── package.json └── src │ └── index.ts ├── CODEOWNERS ├── security-assessment-report ├── .gitignore ├── package.json ├── README.md └── generate-assessment-report.js ├── github-codeowners ├── .gitignore ├── .env.example ├── package.json ├── README.md ├── index.js └── package-lock.json ├── summary-relationships ├── tsconfig.json ├── tslint.json ├── tools │ └── bin │ │ ├── mapids.js │ │ └── delete-summary-relationships ├── src │ ├── wait-for-job.ts │ ├── get-client.ts │ ├── index-executes.ts │ ├── index-queue.ts │ ├── workload-access-execute-query.ts │ ├── build-payload.ts │ └── workload-queue-workload-query.ts ├── package.json └── README.md ├── .gitignore ├── vendor-management ├── publish.sh ├── invision.yml ├── README.md └── apple.yml ├── security-assessment ├── publish.sh ├── README.md └── assessment-objects │ ├── 2019-pentest.yml │ ├── 2018-risk-assessment.yml │ ├── 2019-pentest-findings.yml │ └── 2018-risk-assessment-risks.yml ├── yarn2npm ├── package.json ├── README.md ├── yarn2npm.js └── package-lock.json ├── ingest-log4j-vulns ├── package.json ├── Dockerfile ├── scan-for-log4j.sh ├── log4shell_vulns_schema.json ├── ingest-log4j-vulns.js └── README.md ├── package.json ├── LICENSE ├── .github └── workflows │ ├── build-ingest-log4j-vulns.yml │ └── peril.yml ├── playbooks └── risk-management.md ├── security-privacy-design └── rfc-template.md └── README.md /npm-inventory/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /npm-inventory/.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist -------------------------------------------------------------------------------- /software-bill-of-materials/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /software-bill-of-materials/.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | *.json 3 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jupiterone/security 2 | 3 | CODEOWNERS @jupiterone/security -------------------------------------------------------------------------------- /security-assessment-report/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store -------------------------------------------------------------------------------- /npm-inventory/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | local 3 | .env 4 | dist 5 | 6 | .eslintcache -------------------------------------------------------------------------------- /github-codeowners/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | local 3 | .env 4 | dist 5 | 6 | .eslintcache 7 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | local 3 | .env 4 | dist 5 | 6 | .eslintcache 7 | *.json 8 | -------------------------------------------------------------------------------- /summary-relationships/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["node"] 4 | } 5 | } -------------------------------------------------------------------------------- /npm-inventory/tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["dist", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["dist", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /software-bill-of-materials/tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["dist", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store 4 | dist 5 | 6 | j1-scope-key 7 | results 8 | 9 | results.json 10 | bulkDelete.json 11 | 12 | test.js -------------------------------------------------------------------------------- /vendor-management/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export $(grep -v '^#' ../.env | xargs) 4 | find . -name \*.yml | while read yml; do j1 -o create --entity -a $J1_ACCOUNT_ID -f $yml; done -------------------------------------------------------------------------------- /security-assessment/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export $(grep -v '^#' ../.env | xargs) 4 | find assessment-objects -name \*.yml | while read yml; do j1 -o create --entity -a $J1_ACCOUNT_ID -f $yml; done -------------------------------------------------------------------------------- /summary-relationships/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": {}, 8 | "rulesDirectory": [] 9 | } -------------------------------------------------------------------------------- /security-assessment-report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assessments", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@jupiterone/jupiterone-client-nodejs": "^0.23.0", 8 | "markdown-pdf": "^10.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /npm-inventory/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2018", 5 | "lib": ["es2018", "dom"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "pretty": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["dist"] 13 | } 14 | -------------------------------------------------------------------------------- /summary-relationships/tools/bin/mapids.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const data = JSON.parse(fs.readFileSync('./results.json', 'utf8')); 3 | 4 | const modified = data.map(i => { 5 | return { 6 | relationship: { 7 | _id: i._id 8 | } 9 | }; 10 | }); 11 | fs.writeFileSync('bulkDelete.json', JSON.stringify(modified, null, 2)); -------------------------------------------------------------------------------- /github-codeowners/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_AUTH_TOKEN="" # your GitHub PAT here. Must be associated with an Owner-level account 2 | ORG="jupiterone" 3 | OWNER="jupiterone" 4 | DEFAULT_TEAM="engineering" 5 | RUNMODE="open_pulls" 6 | # DEBUG=true ## uncomment to enable debug output 7 | # SILENT=true ## uncomment to suppress all log output 8 | ERRLOG="error.log" 9 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2018", 5 | "lib": ["es2018", "dom"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "pretty": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["dist"] 13 | } 14 | -------------------------------------------------------------------------------- /software-bill-of-materials/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2018", 5 | "lib": ["es2018", "dom"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "pretty": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["dist"] 13 | } 14 | -------------------------------------------------------------------------------- /yarn2npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn2npm", 3 | "version": "1.0.0", 4 | "description": "convert yarn to npm", 5 | "main": "yarn2npm.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Cameron Griffin", 10 | "dependencies": { 11 | "@actions/github": "^5.1.1", 12 | "isomorphic-git": "^1.21.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /npm-inventory/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-var-requires": 1, 14 | "@typescript-eslint/no-use-before-define": 1 15 | } 16 | } -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-var-requires": 1, 14 | "@typescript-eslint/no-use-before-define": 1 15 | } 16 | } -------------------------------------------------------------------------------- /software-bill-of-materials/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-var-requires": 1, 14 | "@typescript-eslint/no-use-before-define": 1 15 | } 16 | } -------------------------------------------------------------------------------- /ingest-log4j-vulns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ingest-log4j-vulns", 3 | "author": "JupiterOne", 4 | "version": "0.0.1", 5 | "description": "A tool to ingest log4shell_sentinel output into the JupiterOne graph for vulnerability remediation", 6 | "main": "ingest-log4j-vulns.js", 7 | "scripts": { 8 | "start": "node ingest-log4j-vulns.js" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "@jupiterone/jupiterone-client-nodejs": "^0.25.0", 13 | "uuid": "^8.3.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secops-automation-examples", 3 | "version": "0.0.0", 4 | "description": "Automatically generated package.json, please edit manually", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/JupiterOne/secops-automation-examples" 8 | }, 9 | "license": "UNLICENSED", 10 | "dependencies": { 11 | "@jupiterone/jupiterone-client-nodejs": "^0.11.3", 12 | "commander": "^2.20.0", 13 | "dotenv": "^8.0.0", 14 | "markdown-pdf": "^10.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /github-codeowners/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeowners-automation", 3 | "version": "1.0.0", 4 | "description": "automate CODEOWNERS adoption", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@octokit/auth-token": "^2.5.0", 14 | "@octokit/plugin-throttling": "^3.6.1", 15 | "@octokit/rest": "^18.12.0", 16 | "dotenv": "^16.0.0", 17 | "isomorphic-git": "^1.13.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ingest-log4j-vulns/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.2 2 | 3 | FROM node:14-alpine 4 | 5 | RUN apk update && apk upgrade && apk add wget bash 6 | 7 | COPY . . 8 | 9 | RUN wget https://github.com/ossie-git/log4shell_sentinel/releases/download/v1.0.0/log4shell_sentinel_v1.0.0-linux-amd64.tar.gz 10 | 11 | # This should produce just the binary 12 | RUN tar -zxf log4shell_sentinel_v1.0.0-linux-amd64.tar.gz 13 | 14 | # This puts it on our $PATH so our shell script works as expected 15 | RUN mv log4shell_sentinel /bin 16 | 17 | RUN npm i 18 | 19 | CMD ["./scan-for-log4j.sh", "/scan"] -------------------------------------------------------------------------------- /ingest-log4j-vulns/scan-for-log4j.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | if [ -z "$J1_ACCOUNT" ] || [ -z "$J1_ACCESS_TOKEN" ]; then 6 | echo "You must export both J1_ACCOUNT and J1_ACCESS_TOKEN vars!" 7 | exit 2 8 | fi 9 | 10 | SCANPATH=${1:-/} 11 | if [ "$SCANPATH" = "/scan" ]; then 12 | # relative scanpath hard-coded by Docker, ensure we're within volume-mounted dir 13 | # so persistent scoping works across runs... 14 | cd /scan 15 | fi 16 | echo "Scanning for Log4j vulnerabilities..." 17 | log4shell_sentinel -p $SCANPATH -nb -nh -nm > ./results 18 | node ./ingest-log4j-vulns.js -------------------------------------------------------------------------------- /security-assessment-report/README.md: -------------------------------------------------------------------------------- 1 | # Generating a PDF report from an assessment 2 | 3 | `/security-assessment-report` 4 | 5 | Run the following command from the above directory to generate a Markdown and a 6 | PDF report of a security assessment by name, including all findings/risks 7 | identified by the assessment. 8 | 9 | ```bash 10 | export $(grep -v '^#' ../.env | xargs) 11 | node generate-assessment-report.js --assessment 'name-of-the-assessment' 12 | ``` 13 | 14 | The `name-of-the-assessment` should match the value of `name` property of an 15 | existing `Assessment` entity in your J1 account. 16 | -------------------------------------------------------------------------------- /summary-relationships/src/wait-for-job.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupiterOneClient, 3 | SyncJobResponse, 4 | SyncJobStatus, 5 | } from "@jupiterone/jupiterone-client-nodejs"; 6 | import { sleep } from "@lifeomic/attempt"; 7 | 8 | export const waitForJobFinalization = async ( 9 | j1Client: JupiterOneClient, 10 | id: string 11 | ): Promise => { 12 | let status: SyncJobResponse; 13 | 14 | do { 15 | status = await j1Client.fetchSyncJobStatus({ 16 | syncJobId: id, 17 | }); 18 | 19 | await sleep(1000); 20 | } while (status.job.status !== SyncJobStatus.FINISHED); 21 | return true; 22 | }; 23 | -------------------------------------------------------------------------------- /summary-relationships/tools/bin/delete-summary-relationships: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | [ -f .env ] && source .env 5 | [ ! -f results.json ] && yarn j1 -a "$J1_ACCOUNT" -k "$J1_API_TOKEN" \ 6 | -q "Find (Function|Task|Workload) THAT (EXECUTES|ACCESSES|ASYNC_NOTIFIES) as rx (Function|Task|Workload|Database|Queue|Channel) WHERE rx.summaryRelationship = true 7 | RETURN 8 | rx._id as _id, 9 | rx._type as type" 10 | 11 | # create bulkDelete.json 12 | node ./tools/bin/mapids.js 13 | 14 | yarn j1 -a "$J1_ACCOUNT" -k "$J1_API_TOKEN" -o bulk-delete --relationship -f ./bulkDelete.json 15 | 16 | rm results.json bulkDelete.json -------------------------------------------------------------------------------- /security-assessment/README.md: -------------------------------------------------------------------------------- 1 | # Documenting security assessment findings 2 | 3 | `/security-assessment` 4 | 5 | - Write your lightweight assessment report and findings in YAML 6 | - Run `publish.sh` to upload the entities to your JupiterOne account 7 | - See the results with a J1QL query like this: 8 | 9 | ```j1ql 10 | Find Assessment that identified (Risk|Finding) return tree 11 | ``` 12 | 13 | More information: 14 | 15 | - https://support.jupiterone.io/hc/en-us/articles/360022721954-SecOps-Artifacts-as-Code 16 | 17 | You can then use an automated script to generate PDF reports for each assessment 18 | and its findings. See [`/security-assessment-report`][1] in this repo. 19 | 20 | [1]: ../security-assessment-report/README.md 21 | -------------------------------------------------------------------------------- /security-assessment/assessment-objects/2019-pentest.yml: -------------------------------------------------------------------------------- 1 | - entityKey: assessment:pentest:2019q1 2 | entityType: penetration_test 3 | entityClass: Assessment 4 | properties: 5 | name: internal-pen-test-2019q1 6 | displayName: Company Internal Penetration Test 2019Q1 7 | summary: Company Internal Penetration Test Q1 2019 conducted between Mar 18th - Mar 29th 8 | description: 9 | (sample text) 10 | Performed a thorough security assessment of the company product line. 11 | Scope includes product A, B and C. 12 | details: 13 | additional report details 14 | category: penetration-testing 15 | status: complete 16 | assessors: 17 | - pen.tester1@yourcompany.com 18 | - pen.tester2@yourcompany.com 19 | open: false 20 | classification: confidential 21 | completedOn: '2019‑04‑05' -------------------------------------------------------------------------------- /summary-relationships/src/get-client.ts: -------------------------------------------------------------------------------- 1 | import { JupiterOneClient } from "@jupiterone/jupiterone-client-nodejs"; 2 | 3 | // const JupiterOneClient = require('@jupiterone/jupiterone-client-nodejs'); 4 | 5 | export async function getClient(clientInput: { 6 | account: string; 7 | accessToken: string; 8 | }) { 9 | const account = 10 | clientInput.account === "" ? process.env.J1_ACCOUNT : clientInput.account; 11 | const accessToken = 12 | clientInput.accessToken === "" 13 | ? process.env.J1_API_TOKEN 14 | : clientInput.accessToken; 15 | if (accessToken === undefined || account === undefined) { 16 | throw console.error("ERROR: MISSING CREDENTIALS"); 17 | } 18 | const j1Client = await new JupiterOneClient({ 19 | account, 20 | accessToken, 21 | dev: !!process.env.J1_DEV_ENABLED, 22 | }).init(); 23 | 24 | return j1Client; 25 | } 26 | -------------------------------------------------------------------------------- /security-assessment/assessment-objects/2018-risk-assessment.yml: -------------------------------------------------------------------------------- 1 | - entityKey: assessment:risk:2018 2 | entityType: risk_assessment 3 | entityClass: Assessment 4 | properties: 5 | name: company-hipaa-risk-assessment-2018 6 | displayName: HIPAA Risk Assessment 2018 7 | summary: 2018 Annual HIPAA Risk Assessment 8 | description: 9 | (sample text) 10 | Organization's security and compliance team assessed policies, controls 11 | and procedures to ensure they meet and exceed 12 | the requirements specified by HIPAA privacy rule and security rule. 13 | details: 14 | additional report details 15 | category: risk-assessment 16 | status: complete 17 | assessors: 18 | - security.staff@yourcompany.com 19 | - internal.audit@yourcompany.com 20 | open: false 21 | classification: confidential 22 | completedOn: '2018-07-23' 23 | reportURL: https://link.to/finding-report 24 | webLink: https://link.to/finding-report -------------------------------------------------------------------------------- /summary-relationships/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secops-automation-examples_summary-relationships", 3 | "description": "simplify complex graph chains with summary relationships", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "scripts": { 7 | "start:executes": "ts-node src/index-executes.ts", 8 | "start:notifies": "ts-node src/index-queue.ts" 9 | }, 10 | "dependencies": { 11 | "@jupiterone/jupiterone-client-nodejs": "^0.23.4", 12 | "@lifeomic/attempt": "^3.0.0", 13 | "@types/lodash.uniqby": "^4.7.6", 14 | "commander": "^2.20.0", 15 | "dotenv": "^8.0.0", 16 | "esm": "^3.2.25", 17 | "graphql": "^14.4.2", 18 | "graphql-tag": "^2.10.1", 19 | "lodash.uniqby": "^4.7.0", 20 | "read-yaml": "^1.1.0", 21 | "readline-sync": "^1.4.10" 22 | }, 23 | "devDependencies": { 24 | "@types/graphql": "^14.2.3", 25 | "@types/node": "^16.9.6", 26 | "prettier": "1.18.2", 27 | "ts-node": "^10.2.1", 28 | "tslint": "^5.18.0", 29 | "typescript": "^3.5.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /yarn2npm/README.md: -------------------------------------------------------------------------------- 1 | # Yarn to NPM script 2 | 3 | This script will convert a project from yarn to npm. 4 | 5 | ## Building 6 | 7 | Install dependencies and build with: 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | ## Running the script 14 | 15 | You must set the following ENV vars: 16 | 17 | ``` 18 | GITHUB_AUTH_TOKEN= 19 | ``` 20 | 21 | Run the script from within a temp directory 22 | 23 | ``` 24 | cameron@laptop Code % git clone git@github.com:JupiterOne/secops-automation-examples.git 25 | cameron@laptop Code % cd secops-automation-examples/yarn2npm/ && npm install 26 | cameron@laptop Code % cd ../../ 27 | cameron@laptop Code % mkdir tmpDir 28 | cameron@laptop Code % cd tmpDir 29 | cgriffin@cgriffin-MBP tmpDir % node ../secops-automation-examples/yarn2npm/yarn2npm.js test-aut-web-juno 30 | ``` 31 | 32 | A new branch will be created in the repo specified. Log files will be stored in a yarn2npm folder. Please remove these before 33 | pushing changes to main. If there was a problem running npm install, a failure.lock will be present in the yarn2npm folder. 34 | -------------------------------------------------------------------------------- /security-assessment/assessment-objects/2019-pentest-findings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - entityKey: finding:assessment:2019q1:appcode-1 3 | entityType: pentest_finding 4 | entityClass: Finding 5 | properties: 6 | name: XSS in application {appname} 7 | displayName: XSS in application {appname} 8 | summary: Stored cross side scripting identified in application {appname} 9 | targets: 10 | - appname 11 | description: 12 | description of the finding 13 | stepsToReproduce: 14 | - '1 - Sign in to application... navigate to page...' 15 | - '2 - Enter in textbox...' 16 | - '3 - Hit save...' 17 | impact: 18 | Attacker may store malicious javascript... 19 | recommendation: 20 | Perform input validation in the code... 21 | severity: high 22 | priority: 2 23 | remediationSLA: 30 24 | status: open 25 | assessment: internal-pen-test-2019q1 26 | open: true 27 | classification: confidential 28 | jiraKey: SEC-99 29 | webLink: https://yourcompany.atlassian.net/browse/SEC-99 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 JupiterOne | LifeOmic Security LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /npm-inventory/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 JupiterOne, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /summary-relationships/src/index-executes.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from "./get-client"; 2 | import { buildPayload } from "./build-payload"; 3 | import { waitForJobFinalization } from "./wait-for-job"; 4 | import { WorkloadAccessExecute } from "./workload-access-execute-query"; 5 | 6 | require("dotenv").config(); 7 | 8 | (async () => { 9 | const j1Client = await getClient({ 10 | accessToken: process.env.J1_API_TOKEN!, 11 | account: process.env.J1_ACCOUNT!, 12 | }); 13 | 14 | const results = await WorkloadAccessExecute.query(j1Client); 15 | 16 | const payload = buildPayload({ 17 | data: results, 18 | verbCb: WorkloadAccessExecute.makeVerb, 19 | relationshipPropsCb: WorkloadAccessExecute.relationshipPropsCb, 20 | }); 21 | 22 | console.log(payload); 23 | 24 | const jobState = await j1Client.bulkUpload({ 25 | scope: "summary-relationships-workload-role-policy-workload", 26 | relationships: payload, 27 | }); 28 | 29 | console.log("Polling for job finalization"); 30 | await waitForJobFinalization(j1Client, jobState.syncJobId); 31 | })().catch(err => { 32 | console.error("", err); 33 | }); 34 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 JupiterOne, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /software-bill-of-materials/README.md: -------------------------------------------------------------------------------- 1 | # NPM Software Bill-of-Materials (SBOM) Script 2 | 3 | This script may be used to query J1 for `npm_package` entities (produced for example via the `npm-inventory` script in this repo), and from these generate a [CycloneDX-format](https://cyclonedx.org) SBOM file. 4 | 5 | ## Building 6 | 7 | Install dependencies and build with: 8 | 9 | ``` 10 | yarn install 11 | yarn build 12 | ``` 13 | 14 | ## Running the script 15 | 16 | You must set the following ENV vars: 17 | 18 | ``` 19 | J1_API_TOKEN= 20 | J1_ACCOUNT= 21 | ``` 22 | 23 | Invoke the script and it will query J1 for all CodeModules, generating a local 'sbom.json' file. 24 | 25 | ``` 26 | node ./dist/index.js 27 | ``` 28 | 29 | Alternately, you may override the J1QL query that is used to find the `CodeModules` by passing it as a quoted argument, like so: 30 | 31 | ``` 32 | node ./dist/index.js "Find npm_package with version!=undefined and ..." 33 | ``` 34 | 35 | ^^ This example invocation uses J1QL that limits modules to the type `npm_package`... 36 | 37 | The script will produce a `sbom.json` file with zero or more components, based on the found packages in J1. 38 | -------------------------------------------------------------------------------- /summary-relationships/src/index-queue.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from "./get-client"; 2 | import { buildPayload } from "./build-payload"; 3 | import { waitForJobFinalization } from "./wait-for-job"; 4 | import { WorkloadQueueWorkload } from "./workload-queue-workload-query"; 5 | const uniqBy = require("lodash.uniqby"); 6 | 7 | require("dotenv").config(); 8 | 9 | (async () => { 10 | const j1Client = await getClient({ 11 | accessToken: process.env.J1_API_TOKEN!, 12 | account: process.env.J1_ACCOUNT!, 13 | }); 14 | 15 | const results = await WorkloadQueueWorkload.query(j1Client); 16 | 17 | console.log(results); 18 | console.log(results.length); 19 | 20 | const payload = buildPayload({ 21 | data: results, 22 | verbCb: WorkloadQueueWorkload.makeVerb, 23 | relationshipPropsCb: WorkloadQueueWorkload.relationshipPropsCb, 24 | }); 25 | 26 | console.log(payload); 27 | 28 | const jobState = await j1Client.bulkUpload({ 29 | scope: "summary-relationships-workload-queue-workload", 30 | relationships: uniqBy(payload, "_key"), 31 | }); 32 | 33 | console.log("Polling for job finalization"); 34 | await waitForJobFinalization(j1Client, jobState.syncJobId); 35 | })().catch(err => { 36 | console.error("", err); 37 | }); 38 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface J1Dependency { 2 | displayName: string; 3 | fullName: string; 4 | name: string; 5 | version: string; 6 | license: string; 7 | purl?: string; 8 | direct?: boolean; 9 | scope?: string; 10 | from?: string; 11 | } 12 | 13 | export interface J1UsesRelationship { 14 | displayName: string; 15 | version: string; 16 | devDependency?: boolean; 17 | directDependency?: boolean; 18 | } 19 | 20 | export interface SBOM { 21 | bomFormat: string; 22 | specVersion: string; 23 | version: number; 24 | metadata: SBOMMeta; 25 | components: SBOMComponent[]; 26 | } 27 | 28 | type SBOMMeta = { 29 | tools: { 30 | vendor: string; 31 | name: string; 32 | version: string; 33 | }[]; 34 | }; 35 | 36 | type ExternalReference = { 37 | type: string; 38 | url: string; 39 | }; 40 | 41 | export interface SBOMComponent { 42 | type: string; 43 | 'bom-ref': string; 44 | name: string; 45 | version: string; 46 | description: string; 47 | licenses: License[]; 48 | purl: string; 49 | externalReferences: ExternalReference[]; 50 | group?: string; 51 | } 52 | 53 | export type License = { 54 | license: { 55 | id: string; 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /vendor-management/invision.yml: -------------------------------------------------------------------------------- 1 | - entityKey: vendor:invision 2 | entityType: invision 3 | entityClass: Vendor 4 | properties: 5 | name: Invision 6 | displayName: Invision 7 | category: 8 | - software 9 | description: design 10 | validated: true 11 | approved: true 12 | approvalPRLink: https://github.com/yourorg/security-artifacts/pull-requests/2 13 | approvalPRName: security-artifacts/2 14 | website: https://www.invisionapp.com 15 | owners: 16 | mainContactName: 17 | mainContactEmail: 18 | mainContactPhone: 19 | mainContactAddress: 20 | breachResponseDays: 21 | linkToNDA: 22 | linkToMSA: 23 | linkToSLA: 24 | linkToBAA: 25 | linkToDPA: 26 | linkToVTR: 27 | linkToISA: 28 | statusPage: 29 | criticality: 5 30 | risk: 5 31 | tag.PHI: false 32 | tag.PII: false 33 | tag.PCI: false 34 | notes: 35 | 36 | - entityKey: account:invision 37 | entityType: invision_account 38 | entityClass: Account 39 | properties: 40 | name: Invision 41 | displayName: Invision 42 | category: 43 | - software 44 | description: design 45 | validated: true 46 | webLink: https://www.invisionapp.com 47 | owners: 48 | admins: 49 | vendor: Invision 50 | notes: -------------------------------------------------------------------------------- /summary-relationships/src/workload-access-execute-query.ts: -------------------------------------------------------------------------------- 1 | import { JupiterOneClient } from "@jupiterone/jupiterone-client-nodejs"; 2 | import { BuildPayloadInput } from "./build-payload"; 3 | 4 | export interface IWorkloadAccessExecute extends BuildPayloadInput { 5 | roleName: string; 6 | policyName: string; 7 | } 8 | 9 | export class WorkloadAccessExecute { 10 | public static async query( 11 | client: JupiterOneClient, 12 | ): Promise { 13 | const results = await client.queryV1(` 14 | Find (Function|Task) as f1 15 | THAT ASSIGNED AccessRole as ar 16 | THAT ASSIGNED AccessPolicy as ap 17 | THAT ALLOWS (Function|Task|Database) as f2 18 | RETURN 19 | f1.displayName as sourceName, f1._id as sourceId, f1._class as sourceClass, f1._type as sourceType, f1._key as sourceKey, 20 | f2.displayName as sinkName, f2._id as sinkId, f2._class as sinkClass, f2._type as sinkType, f2._key as sinkKey, 21 | ar.displayName as roleName, 22 | ap.displayName as policyName`); 23 | 24 | return results; 25 | } 26 | 27 | public static makeVerb(input: IWorkloadAccessExecute): string { 28 | return [input.sinkClass].flat().includes("Database") 29 | ? "ACCESSES" 30 | : "EXECUTES"; 31 | } 32 | public static relationshipPropsCb({ 33 | roleName, 34 | policyName, 35 | }: IWorkloadAccessExecute): Record { 36 | return { 37 | roleName, 38 | policyName, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /summary-relationships/src/build-payload.ts: -------------------------------------------------------------------------------- 1 | export interface BuildPayloadInput { 2 | sourceId: string; 3 | sourceKey: string; 4 | sourceType: string; 5 | sourceClass: string; 6 | sinkId: string; 7 | sinkKey: string; 8 | sinkType: string; 9 | sinkClass: string; 10 | } 11 | 12 | export interface BulkUploadOutput { 13 | _key: string; 14 | _type: string; 15 | _class: string; 16 | _fromEntityId: string; 17 | _toEntityId: string; 18 | [key: string]: string | boolean; 19 | } 20 | 21 | export const buildPayload = ({ 22 | data, 23 | verbCb, 24 | relationshipPropsCb, 25 | }: { 26 | data: T[]; 27 | verbCb: (input: T) => string; 28 | relationshipPropsCb: (input: T) => Record; 29 | }): BulkUploadOutput[] => { 30 | return data.map(input => { 31 | const { 32 | sourceId, 33 | sourceKey, 34 | sourceType, 35 | sinkId, 36 | sinkKey, 37 | sinkType, 38 | } = input; 39 | const relVerb = verbCb(input); 40 | const relationshipKey = 41 | sourceKey + "|" + relVerb.toLowerCase() + "|" + sinkKey; 42 | const relationshipType = 43 | sourceType + "_" + relVerb.toLowerCase() + "_" + sinkType; 44 | 45 | const payload = { 46 | _key: relationshipKey, 47 | _type: relationshipType, 48 | _class: relVerb, 49 | _fromEntityId: sourceId, 50 | _toEntityId: sinkId, 51 | summaryRelationship: true, 52 | ...relationshipPropsCb(input), 53 | }; 54 | return payload; 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /security-assessment/assessment-objects/2018-risk-assessment-risks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - entityKey: risk:endpoint-management-gaps 3 | entityType: technical_risk 4 | entityClass: Risk 5 | properties: 6 | name: Endpoint management gaps 7 | displayName: Endpoint management gaps 8 | summary: Lack of visibility on how user endpoint systems/devices are configured 9 | description: 10 | (sample text) 11 | Endpoint systems should be configured according to the company's IT and 12 | security standards. Because currently all enduser systems (e.g. laptops) 13 | are self managed, there is a lack of centralized visibility into how 14 | each system is configured and if they meet the compliance requirements 15 | 16 | details: 17 | 'Systems should be configured with at least the following:' 18 | 19 | 1. Disk encryption enabled 20 | 2. Screensaver protection/screen lock on 21 | 3. Local firewall enabled 22 | 4. Remote login disabled 23 | 5. Auto install OS security patches enabled 24 | 6. (if it is Windows) Has Windows Defender or equivalent malware protection running 25 | 26 | category: technical 27 | threats: malware 28 | targets: enduser devices 29 | probability: 2 30 | impact: 2 31 | score: 4 32 | status: open 33 | assessment: company-hipaa-risk-assessment-2018 34 | reporter: security@yourcompany.com 35 | open: true 36 | mitigation: 37 | jiraKey: SEC-112 38 | webLink: https://yourcompany.atlassian.net/browse/SEC-112 -------------------------------------------------------------------------------- /.github/workflows/build-ingest-log4j-vulns.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out source code 13 | uses: actions/checkout@v2 14 | 15 | - name: Detect Dockerfile changes 16 | uses: dorny/paths-filter@v2 17 | id: filter 18 | with: 19 | filters: | 20 | projectchanged: 21 | - 'ingest-log4j-vulns/**' 22 | 23 | - name: Should Build? 24 | if: steps.filter.outputs.projectchanged == 'true' 25 | run: | 26 | echo "Project changed. Need to update Docker image." 27 | echo "need_docker_build=true" >> $GITHUB_ENV 28 | 29 | - name: Login to DockerHub Registry 30 | if: env.need_docker_build 31 | run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin docker.io 32 | 33 | - name: Build the latest Docker image 34 | if: env.need_docker_build 35 | run: | 36 | cd ingest-log4j-vulns 37 | pkgver="$(jq -r .version package.json)" 38 | echo "pkgver=$pkgver" >> $GITHUB_ENV 39 | docker build --file Dockerfile --tag jupiterone/ingest-log4j-vulns:latest --tag jupiterone/ingest-log4j-vulns:$pkgver . 40 | 41 | - name: Push the latest Docker image 42 | if: env.need_docker_build 43 | run: | 44 | docker push jupiterone/ingest-log4j-vulns:latest 45 | docker push jupiterone/ingest-log4j-vulns:${{ env.pkgver }} 46 | -------------------------------------------------------------------------------- /software-bill-of-materials/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-software-bill-of-materials", 3 | "version": "0.1.0", 4 | "description": "Generate CycloneDX-format SBOM file from NPM dependencies found in J1.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": "Erich Smith ", 8 | "license": "MIT", 9 | "files": [ 10 | "LICENSE", 11 | "dist", 12 | "bin" 13 | ], 14 | "scripts": { 15 | "env": "export $(grep -v '^#' .env | xargs);", 16 | "format": "yarn prettier --write '**/*.{ts,js,json,css,md,yml}'", 17 | "type-check": "tsc --noEmit", 18 | "lint": "eslint . --cache --fix --ext .ts,.tsx", 19 | "pretest": "yarn lint && yarn type-check", 20 | "test": "echo OK", 21 | "copydist": "cp -R LICENSE *.md yarn.lock package.json ./dist/", 22 | "distpackage": "(cd ./dist && sed -ibak -e 's#dist/inde[x]#index#g' package.json && rm package.jsonbak)", 23 | "prebuild": "yarn test", 24 | "build": "tsc -p tsconfig.dist.json --declaration", 25 | "prepack": "yarn build" 26 | }, 27 | "dependencies": { 28 | "@jupiterone/jupiterone-client-nodejs": "^0.22.7", 29 | "@lifeomic/attempt": "^3.0.0", 30 | "dotenv": "^8.2.0", 31 | "fs-extra": "^9.0.1", 32 | "glob": "^7.1.6" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^13.9.8", 36 | "@typescript-eslint/eslint-plugin": "^4.1.1", 37 | "@typescript-eslint/parser": "^4.1.1", 38 | "eslint": "^7.14.0", 39 | "eslint-plugin-jest": "^23.8.2", 40 | "eslint-plugin-prettier": "^3.1.4", 41 | "husky": "^2.4.0", 42 | "lint-staged": "^8.2.0", 43 | "prettier": "^2.0.2", 44 | "ts-node": "^9.0.0", 45 | "typescript": "^4.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ingest-log4j-vulns/log4shell_vulns_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "id": "cf62f5ac-0127-41cb-8672-c8ae539856c4", 5 | "title": "Sorted List of Vulnerable Hosts", 6 | "type": "table", 7 | "config": { 8 | "queries": [ 9 | { 10 | "name": "query1", 11 | "query": "Find log4j_vulnerability as v return v.hostname, count(v) as vulns ORDER BY vulns DESC" 12 | } 13 | ] 14 | } 15 | }, 16 | { 17 | "id": "74571ecc-c811-471d-bffc-3ad6c7dda6cd", 18 | "title": "Total vulns", 19 | "type": "number", 20 | "config": { 21 | "queries": [ 22 | { 23 | "name": "query1", 24 | "query": "Find log4j_vulnerability as v return count(v) as value" 25 | } 26 | ] 27 | } 28 | }, 29 | { 30 | "id": "b2d96eb7-c442-4985-8fb0-ed57f11f31d4", 31 | "title": "Oldest vulnerable hosts", 32 | "type": "table", 33 | "config": { 34 | "queries": [ 35 | { 36 | "name": "query1", 37 | "query": "Find log4j_vulnerability as v ORDER BY v._createdOn ASC" 38 | } 39 | ] 40 | } 41 | } 42 | ], 43 | "layouts": { 44 | "xs": [], 45 | "sm": [], 46 | "md": [], 47 | "lg": [ 48 | { 49 | "w": 12, 50 | "h": 2, 51 | "x": 0, 52 | "y": 0, 53 | "i": "cf62f5ac-0127-41cb-8672-c8ae539856c4", 54 | "moved": false, 55 | "static": false 56 | }, 57 | { 58 | "w": 3, 59 | "h": 2, 60 | "x": 0, 61 | "y": 2, 62 | "i": "74571ecc-c811-471d-bffc-3ad6c7dda6cd", 63 | "moved": false, 64 | "static": false 65 | }, 66 | { 67 | "w": 9, 68 | "h": 2, 69 | "x": 3, 70 | "y": 2, 71 | "i": "b2d96eb7-c442-4985-8fb0-ed57f11f31d4", 72 | "moved": false, 73 | "static": false 74 | } 75 | ], 76 | "xl": [] 77 | } 78 | } -------------------------------------------------------------------------------- /summary-relationships/README.md: -------------------------------------------------------------------------------- 1 | # Summary Relationships Script 2 | 3 | This example looks for existing IAM policy ALLOWS relationships in the JupiterOne graph to create certain "summary" or shortcut relationships that highlight interesting inter-service and service source-to-sink connectivity that might otherwise be cumbersome to reason about or query for. 4 | 5 | After running this script, instead of a query like: 6 | 7 | ``` 8 | FIND Function 9 | THAT ASSIGNED AccessRole 10 | THAT ASSIGNED AccessPolicy 11 | THAT ALLOWS Function 12 | ``` 13 | 14 | You can instead write this query: 15 | 16 | ``` 17 | FIND Function 18 | THAT EXECUTES Function 19 | ``` 20 | 21 | ## Data Model 22 | 23 | Function|Task --EXECUTES--> Function|Task 24 | 25 | Function|Task --ACCESSES--> Database 26 | 27 | Function|Task|Workload --ASYNC_NOTIFIES--> Function|Task|Workload (via intermediate Queue|Channel) 28 | 29 | ## Prerequisites 30 | 31 | You will need to either `export` the following shell variables: 32 | 33 | ```bash 34 | J1_API_TOKEN 35 | J1_ACCOUNT 36 | ``` 37 | 38 | or add these variables to a `.env` file 39 | 40 | If you are interacting with the dev environment (\*.apps.dev.jupiterone.io), also set an environment variable `J1_DEV_ENABLED`. 41 | 42 | ## Running the script 43 | 44 | For EXECUTES and ACCESSES summary relationships, run `yarn start:executes`. 45 | 46 | For ASYNC_NOTIFIES summary relationships, run `yarn start:notifies`. 47 | 48 | ## Re-running the script 49 | 50 | These scripts make use of our bulk-upload api, and so these scripts can each be used safely to update the relationships in the graph based on the latest underlying IAM policies by re-running them periodically. 51 | 52 | ## Cleaning Up 53 | 54 | To remove all summary relationships from your graph, run `./tools/bin/delete-summary-relationships`. 55 | -------------------------------------------------------------------------------- /npm-inventory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-inventory", 3 | "version": "0.1.1", 4 | "description": "Recursively gather information on NPM dependencies and report to J1", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": "Erich Smith ", 8 | "license": "MIT", 9 | "files": [ 10 | "LICENSE", 11 | "dist", 12 | "bin" 13 | ], 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "bin": { 18 | "inventory": "bin/inventory" 19 | }, 20 | "scripts": { 21 | "env": "export $(grep -v '^#' .env | xargs);", 22 | "format": "yarn prettier --write '**/*.{ts,js,json,css,md,yml}'", 23 | "type-check": "tsc --noEmit", 24 | "lint": "eslint . --cache --fix --ext .ts,.tsx", 25 | "pretest": "yarn lint && yarn type-check", 26 | "test": "echo OK", 27 | "copydist": "cp -R LICENSE *.md yarn.lock package.json ./dist/", 28 | "distpackage": "(cd ./dist && sed -ibak -e 's#dist/inde[x]#index#g' package.json && rm package.jsonbak)", 29 | "prebuild": "yarn test", 30 | "build": "tsc -p tsconfig.dist.json --declaration", 31 | "prepack": "yarn build" 32 | }, 33 | "dependencies": { 34 | "@jupiterone/jupiterone-client-nodejs": "^0.22.7", 35 | "@lifeomic/attempt": "^3.0.0", 36 | "dotenv": "^8.2.0", 37 | "fs-extra": "^9.0.1", 38 | "glob": "^7.1.6" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^13.9.8", 42 | "@typescript-eslint/eslint-plugin": "^2.26.0", 43 | "@typescript-eslint/parser": "^2.26.0", 44 | "eslint": "^7.14.0", 45 | "eslint-plugin-jest": "^23.8.2", 46 | "eslint-plugin-prettier": "^3.1.4", 47 | "husky": "^2.4.0", 48 | "lint-staged": "^8.2.0", 49 | "prettier": "^2.0.2", 50 | "ts-node": "^9.0.0", 51 | "typescript": "^4.1.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ingest-cyclonedx-sbom", 3 | "version": "0.0.1", 4 | "description": "Ingest CycloneDX SBOM files (JSON format) into JupiterOne", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": "JupiterOne Security ", 8 | "license": "MIT", 9 | "files": [ 10 | "LICENSE", 11 | "dist", 12 | "bin" 13 | ], 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "bin": { 18 | "inventory": "bin/inventory" 19 | }, 20 | "scripts": { 21 | "env": "export $(grep -v '^#' .env | xargs);", 22 | "format": "yarn prettier --write '**/*.{ts,js,json,css,md,yml}'", 23 | "type-check": "tsc --noEmit", 24 | "lint": "eslint . --cache --fix --ext .ts,.tsx", 25 | "pretest": "yarn lint && yarn type-check", 26 | "test": "echo OK", 27 | "copydist": "cp -R LICENSE *.md yarn.lock package.json ./dist/", 28 | "distpackage": "(cd ./dist && sed -ibak -e 's#dist/inde[x]#index#g' package.json && rm package.jsonbak)", 29 | "prebuild": "yarn test", 30 | "build": "tsc -p tsconfig.dist.json --declaration", 31 | "prepack": "yarn build" 32 | }, 33 | "dependencies": { 34 | "@cyclonedx/bom": "^3.3.1", 35 | "@jupiterone/jupiterone-client-nodejs": "^0.25.0", 36 | "@lifeomic/attempt": "^3.0.0", 37 | "dotenv": "^8.2.0", 38 | "fs-extra": "^9.0.1", 39 | "glob": "^7.1.6", 40 | "minimist": "^1.2.6" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^13.9.8", 44 | "@typescript-eslint/eslint-plugin": "^2.26.0", 45 | "@typescript-eslint/parser": "^2.26.0", 46 | "eslint": "^7.14.0", 47 | "eslint-plugin-jest": "^23.8.2", 48 | "eslint-plugin-prettier": "^3.1.4", 49 | "husky": "^2.4.0", 50 | "lint-staged": "^8.2.0", 51 | "prettier": "^2.0.2", 52 | "ts-node": "^9.0.0", 53 | "typescript": "^4.1.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vendor-management/README.md: -------------------------------------------------------------------------------- 1 | # Maintaining details about third-party vendors and accounts 2 | 3 | `/vendor-management` 4 | 5 | As part of your organization's third-party vendor management process, you will 6 | most likely need to keep track of the details of each vendor, links to 7 | contracts and agreements, and the accounts and users associated with each vendor. 8 | 9 | If you use SAML Single Sign On (SSO) for your SaaS accounts and your SSO 10 | provider has an integration with JupiterOne (e.g. Okta, OneLogin), the vendors 11 | are mapped/inferred through the SSO app configuration. 12 | 13 | For additional vendors, you can provide the details in a YAML file as shown in 14 | the examples in this repo, and publish them to your JupiterOne account. 15 | 16 | This way, you can leverage pull requests to serve as the vendor review/approval 17 | process. The PR can also be the trigger for a security team member to conduct 18 | more detailed vendor risk assessment, and provide a link to the 19 | report/questionnaire as part of the vendor entity YAML. 20 | 21 | **Entities:** 22 | 23 | Each example vendor YAML file contains two "classes" of entity objects: 24 | 25 | - `Vendor`: an entity representing the vendor itself. 26 | - `Account`: one or more entities that represent the account(s) hosted by 27 | this vendor that users can log in to. 28 | 29 | **Relationships:** 30 | 31 | `Vendor -HOSTS-> Account` 32 | 33 | When these entities are published to JupiterOne, a relationship between the 34 | vendor and its accounts are automatically created, as long as the value of the 35 | `vendor` property on the `Account` entity object matches the `name` of the 36 | `Vendor`. 37 | 38 | `Person -MANAGES-> Vendor` 39 | 40 | Additionally, if there are `Person` entities within your JupiterOne account, 41 | and their email addresses match the ones configured as `owners` on the `Vendor` 42 | entity, a relationship will be automatically created between them. 43 | -------------------------------------------------------------------------------- /npm-inventory/README.md: -------------------------------------------------------------------------------- 1 | # NPM Inventory Script 2 | 3 | The intent of this script is to capture the NPM module dependency graph from the local `node_modules` folder of a code repository that is already represented in JupiterOne (say, via a GitHub or Bitbucket integration). 4 | 5 | The script is intended to be used after `npm install` or `yarn install` has occurred locally, and will collect all of the direct dependencies for the coderepo, and insert them into the J1 graph like so: 6 | 7 | ``` 8 | CodeRepo --USES--> npm_package 9 | ``` 10 | 11 | Where the `USES` relationship will contain metadata about the dependency, like package version, and the `npm_package` entity will contain metadata including the license used by the dependency. 12 | 13 | This script can be used to collect an inventory of NPM module dependencies seen and their licenses. 14 | 15 | ## Building 16 | 17 | Install dependencies in the `npm-inventory` dir and build with: 18 | 19 | ``` 20 | yarn install 21 | yarn build 22 | ``` 23 | 24 | ## Running the script 25 | 26 | You must set the following ENV vars: 27 | 28 | ``` 29 | J1_API_TOKEN= 30 | J1_ACCOUNT= 31 | ``` 32 | 33 | Invoke the script with a fully-qualified path to the code repository directory you want to scan: 34 | 35 | ``` 36 | node ./dist/index.js /path/to/your/target/repo 37 | ``` 38 | 39 | ### Running the script over a collection of repos 40 | 41 | If you want to perform an exhaustive inventory, clone all of your target repos locally under `~/clone`, then consider using a simple script like: 42 | 43 | ``` 44 | #!/bin/bash 45 | set -x 46 | ## NOTE: set this appropriately vv 47 | NPMINV=/path/to/secops-automation-examples/npm-inventory 48 | 49 | WORKDIR=~/clone 50 | DONEDIR=~/clone-processed 51 | REJECTDIR=~/clone-rejected 52 | 53 | mkdir -p $DONEDIR $REJECTDIR 54 | 55 | run() { 56 | while :; do 57 | cd $WORKDIR || exit 2 58 | REPOPATH="$(find $PWD -type d -maxdepth 1 -not -path $PWD | head -1)" 59 | if [ "$REPOPATH" = "" ]; then 60 | echo DONE 61 | exit 0 62 | fi 63 | cd $REPOPATH 64 | echo "Processing $REPOPATH..." 65 | yarn install 66 | if [ $? -ne 0 ]; then 67 | rejectrepo 68 | continue 69 | fi 70 | cd $NPMINV 71 | node ./dist/index.js $REPOPATH || exit 2 72 | rm -rf ${REPOPATH}/node_modules 73 | mv $REPOPATH $DONEDIR/$(basename $REPOPATH) 74 | done 75 | } 76 | 77 | rejectrepo() { 78 | cd $WORKDIR 79 | echo Rejecting failed repo: $REPOPATH 80 | mv $REPOPATH $REJECTDIR/$(basename $REPOPATH) 81 | } 82 | 83 | run 84 | ``` 85 | -------------------------------------------------------------------------------- /software-bill-of-materials/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra"; 2 | import path from "path"; 3 | 4 | const JupiterOneClient = require("@jupiterone/jupiterone-client-nodejs"); 5 | 6 | if (!process.env.J1_API_TOKEN) { 7 | throw new Error('Missing J1_API_TOKEN ENV Var!'); 8 | } 9 | 10 | if (!process.env.J1_ACCOUNT) { 11 | throw new Error('Missing J1_ACCOUNT ENV Var!'); 12 | } 13 | 14 | export type Component = { 15 | type: string; 16 | 'bom-ref': string; 17 | name: string; 18 | version: string; 19 | description: string; 20 | licenses: { 21 | license: { 22 | id: string; 23 | } 24 | }[]; 25 | purl: string; 26 | externalReferences: []; 27 | scope: string; 28 | }; 29 | 30 | export type CodeModuleEntity = { 31 | entity: { 32 | _type: string[]; 33 | _class: string[]; 34 | } 35 | properties: { 36 | name: string; 37 | license: string; 38 | version: string; 39 | purl?: string; 40 | } 41 | }; 42 | 43 | async function run (): Promise { 44 | const j1Client = await new JupiterOneClient({ 45 | account: process.env.J1_ACCOUNT, 46 | accessToken: process.env.J1_API_TOKEN, 47 | dev: false 48 | }).init(); 49 | 50 | const j1ql = process.argv[2] || 'Find CodeModule with license!=undefined and version!=undefined THAT USES CodeRepo'; 51 | console.log(`searching J1 with: "${j1ql}"...`); 52 | const modules: CodeModuleEntity[] = await j1Client.queryV1(j1ql); 53 | console.log(`found ${modules.length} CodeModules in J1...`); 54 | 55 | const components: Component[] = []; 56 | 57 | for (const module of modules) { 58 | const { name, version, license, purl } = module?.properties; 59 | 60 | // account for possibly missing purl data 61 | const moduleType = module.entity._type[0].split('_')[0]; // e.g. ['npm_package'] -> 'npm' 62 | const fallBackPurl = `pkg:${moduleType}/${name}@${version}`; 63 | 64 | components.push({ 65 | type: 'library', 66 | 'bom-ref': purl || fallBackPurl, 67 | name, 68 | version, 69 | description: '', 70 | licenses: [{ 71 | license: { 72 | id: license 73 | } 74 | }], 75 | purl: purl || fallBackPurl, 76 | externalReferences: [], 77 | scope: 'required' 78 | }); 79 | } 80 | 81 | const bom = JSON.parse(await fs.readFile(path.join(__dirname, '../bom-skeleton.json'), 'utf8')); 82 | bom.components = components; 83 | 84 | await fs.writeFile('sbom.json', JSON.stringify(bom, null, 2)); 85 | console.log('wrote software bill-of-materials to "sbom.json"'); 86 | } 87 | 88 | run().catch(console.error); 89 | -------------------------------------------------------------------------------- /vendor-management/apple.yml: -------------------------------------------------------------------------------- 1 | - entityKey: vendor:apple 2 | entityType: apple 3 | entityClass: Vendor 4 | properties: 5 | name: Apple 6 | displayName: Apple 7 | category: 8 | - software 9 | - mobile 10 | - development 11 | description: > 12 | Provides Developer account and App Store Connect account for mobile apps development 13 | validated: true 14 | approved: true 15 | approvalPRLink: https://github.com/yourorg/security-artifacts/pull-requests/2 16 | approvalPRName: security-artifacts/2 17 | website: https://www.apple.com 18 | owners: 19 | - jon.smith@yourcompany.com 20 | - owner.two@yourcompany.com 21 | mainContactName: 22 | mainContactEmail: 23 | mainContactPhone: 24 | mainContactAddress: 25 | breachResponseDays: 26 | linkToNDA: https://developer.apple.com/terms/apple-developer-agreement/Apple-Developer-Agreement-English.pdf 27 | linkToMSA: https://developer.apple.com/programs/whats-included/ 28 | linkToSLA: 29 | linkToBAA: 30 | linkToDPA: 31 | linkToVTR: 32 | linkToISA: 33 | criticality: 10 34 | risk: 5 35 | tag.PHI: false 36 | tag.PII: true 37 | tag.PCI: false 38 | statusPage: 39 | notes: 40 | 41 | - entityKey: account:apple:developer 42 | entityType: apple_developer_account 43 | entityClass: Account 44 | properties: 45 | name: Apple Developer Program 46 | displayName: Apple Developer Program 47 | category: 48 | - software 49 | - mobile 50 | - development 51 | description: > 52 | Used to provision iOS apps for distribution though App Store, 53 | create and download development and distribution certificates, 54 | submit apps to app store, manage beta test campaigns and app store meta data 55 | 56 | validated: true 57 | webLink: 58 | - https://developer.apple.com 59 | - https://appstoreconnect.apple.com 60 | owners: 61 | - jon.smith@yourcompany.com 62 | - owner.two@yourcompany.com 63 | admins: 64 | - jon.smith@yourcompany.com 65 | - admin.two@yourcompany.com 66 | users: 67 | - user.one@yourcompany.com 68 | - user.two@yourcompany.com 69 | vendor: Apple 70 | notes: 71 | - "Some additional notes about this vendor" 72 | 73 | - entityKey: account:apple:business 74 | entityType: apple_business_account 75 | entityClass: Account 76 | properties: 77 | name: Apple Business 78 | displayName: Apple Business 79 | category: 80 | - procurement 81 | description: purchasing 82 | validated: true 83 | webLink: https://www.apple.com 84 | owners: 85 | admins: 86 | vendor: Apple 87 | notes: 88 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/README.md: -------------------------------------------------------------------------------- 1 | # CycloneDX Ingestion Script 2 | 3 | The intent of this script is to ingest `CodeRepo -USES-> CodeModule` graph data into JupiterOne. 4 | 5 | The script is intended to be used after a `cyclonedx-bom` tool has generated a [CycloneDX](https://cyclonedx.org/)-format SBOM file representing the package dependencies for a given code repository. This script should work with any of the following projects: 6 | 7 | * [CycloneDX NodeJS](https://github.com/CycloneDX/cyclonedx-node-module) 8 | * [CycloneDX Go](https://github.com/CycloneDX/cyclonedx-gomod) 9 | * [CycloneDX Python](https://github.com/CycloneDX/cyclonedx-python) 10 | * [CycloneDX Rust](https://github.com/CycloneDX/cyclonedx-rust-cargo) 11 | * [CycloneDX Java](https://github.com/CycloneDX/cyclonedx-core-java) 12 | * [CycloneDX Ruby](https://github.com/CycloneDX/cyclonedx-ruby-gem) 13 | * [CycloneDX PHP](https://github.com/CycloneDX/cyclonedx-php-composer) 14 | * [CycloneDX .NET](https://github.com/CycloneDX/cyclonedx-dotnet) 15 | 16 | Where the `USES` relationship will contain metadata about the dependency, like package version, and the `CodeModule` entity will contain metadata including the license used by the dependency. 17 | 18 | This script can be used to collect an inventory of module dependencies seen and their licenses. 19 | 20 | ## Building 21 | 22 | Install dependencies in the `ingest-cyclonedx-sbom` dir and build with: 23 | 24 | ``` 25 | yarn install 26 | yarn build 27 | ``` 28 | 29 | ## Running the script 30 | 31 | You must set the following ENV vars: 32 | 33 | ``` 34 | J1_API_TOKEN= 35 | J1_ACCOUNT= 36 | ``` 37 | 38 | ### Suggested shell alias 39 | 40 | `alias ingest-cyclonedx-sbom='node /path/to/security-automation-examples/ingest-cyclonedx-sbom/dist/index.js'` 41 | 42 | ### Usage 43 | 44 | ``` 45 | ingest-cyclonedx-sbom --sbom --repo [--devDeps ] [--directDeps ] [--directOnly] 46 | --sbom path to sbom file to ingest 47 | --repo name of coderepo ingest 48 | (optional) --devDeps path to json file array of dev dependencies 49 | (optional) --directDeps path to json file array of direct dependencies 50 | (optional) --directOnly only ingest direct dependencies (requires --directDeps) 51 | ``` 52 | 53 | ### Example Invocation for an NPM project 54 | 55 | ``` 56 | cd /my/cloned/project/repo 57 | yarn install # install dependencies 58 | cyclonedx-bom -d -o bom.json # generate full sbom including dev dependencies 59 | # create optional hint arrays for ingestion 60 | jq '.dependencies | keys' package.json > directDeps.json 61 | jq '.devDependencies | keys' package.json > devDeps.json 62 | ingest-cyclonedx-sbom --sbom ./bom.json --repo my-project --devDeps ./devDeps.json --directDeps ./directDeps.json 63 | ``` 64 | -------------------------------------------------------------------------------- /summary-relationships/src/workload-queue-workload-query.ts: -------------------------------------------------------------------------------- 1 | import { JupiterOneClient } from "@jupiterone/jupiterone-client-nodejs"; 2 | import { BuildPayloadInput } from "./build-payload"; 3 | 4 | export interface IWorkloadQueueWorkload extends BuildPayloadInput { 5 | queueName: string; 6 | queueId: string; 7 | queueClass: string; 8 | queueKey: string; 9 | queueType: string; 10 | } 11 | 12 | export class WorkloadQueueWorkload { 13 | 14 | public static async query( 15 | client: JupiterOneClient, 16 | ): Promise { 17 | const results: IWorkloadQueueWorkload[] = await client.queryV1(` 18 | Find (Function|Task|Workload) as f1 19 | THAT ASSIGNED AccessRole THAT ASSIGNED AccessPolicy 20 | THAT ALLOWS as a1 (Queue|Channel) as q 21 | THAT ALLOWS AccessPolicy THAT ASSIGNED AccessRole 22 | THAT ASSIGNED (Function|Task|Workload) as f2 23 | WHERE a1.execute=true AND a1.read=false 24 | RETURN 25 | f1.displayName as sourceName, f1._id as sourceId, f1._class as sourceClass, f1._type as sourceType, f1._key as sourceKey, 26 | f2.displayName as sinkName, f2._id as sinkId, f2._class as sinkClass, f2._type as sinkType, f2._key as sinkKey, 27 | q.displayName as queueName, q._id as queueId, q._class as queueClass, q._type as queueType, q._key as queueKey`); 28 | 29 | const ret: IWorkloadQueueWorkload[] = results.flatMap(original => { 30 | const { 31 | queueClass, 32 | queueId, 33 | queueKey, 34 | queueName, 35 | queueType, 36 | sinkClass, 37 | sinkId, 38 | sinkKey, 39 | sinkType, 40 | sourceId, 41 | sourceKey, 42 | sourceType, 43 | sourceClass, 44 | } = original; 45 | 46 | const one: IWorkloadQueueWorkload = { 47 | queueClass, 48 | queueId, 49 | queueKey, 50 | queueName, 51 | queueType, 52 | sinkClass: queueClass, 53 | sinkId: queueId, 54 | sinkKey: queueKey, 55 | sinkType: queueType, 56 | sourceId, 57 | sourceKey, 58 | sourceType, 59 | sourceClass, 60 | }; 61 | 62 | const two: IWorkloadQueueWorkload = { 63 | queueClass, 64 | queueId, 65 | queueKey, 66 | queueName, 67 | queueType, 68 | sinkClass, 69 | sinkId, 70 | sinkKey, 71 | sinkType, 72 | sourceId: queueId, 73 | sourceKey: queueKey, 74 | sourceType: queueType, 75 | sourceClass: queueClass, 76 | }; 77 | 78 | return [one, two]; 79 | }); 80 | 81 | return ret; 82 | } 83 | 84 | public static makeVerb(input: IWorkloadQueueWorkload): string { 85 | return "ASYNC_NOTIFIES"; 86 | } 87 | public static relationshipPropsCb({ 88 | queueName, 89 | queueType, 90 | }: IWorkloadQueueWorkload): Record { 91 | return { 92 | name: queueName, 93 | type: queueType, 94 | }; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/peril.yml: -------------------------------------------------------------------------------- 1 | name: "Peril" 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | TRANSPONDER_DOCKER_IMAGE: 081157560428.dkr.ecr.us-east-1.amazonaws.com/transponder:1 8 | SECURITY_SCAN_IMAGE: ghcr.io/jupiterone/security-scan:latest 9 | 10 | jobs: 11 | Peril: 12 | name: Peril 13 | permissions: 14 | id-token: write 15 | contents: read 16 | packages: read 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 14.x 27 | 28 | - name: Run build 29 | run: yarn install 30 | 31 | - name: Get Variables 32 | id: get-vars 33 | run: | 34 | if [[ "${GITHUB_REF}" == 'ref/head/main' && "${GITHUB_EVENT_NAME}" == 'push' ]]; 35 | then 36 | echo ::set-output name=aws-oidc-role::arn:aws:iam::081157560428:role/github-main-role 37 | else 38 | echo ::set-output name=aws-oidc-role::arn:aws:iam::081157560428:role/github-pull-request-role 39 | fi 40 | 41 | - name: Configure aws credentials 42 | uses: aws-actions/configure-aws-credentials@v1 43 | with: 44 | role-to-assume: ${{ steps.get-vars.outputs.aws-oidc-role }} 45 | role-session-name: pr-role-session 46 | aws-region: us-east-1 47 | 48 | - name: ECR login 49 | uses: aws-actions/amazon-ecr-login@v1 50 | id: amazon-ecr-login 51 | 52 | - name: Login to GHCR 53 | uses: docker/login-action@v2 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.actor }} 57 | password: ${{ secrets.PACKAGE_TOKEN }} 58 | 59 | - name: Pull security-scan 60 | run: | 61 | docker pull $SECURITY_SCAN_IMAGE 62 | 63 | - name: Run security-scan 64 | run: | 65 | docker run \ 66 | --user root \ 67 | -v /var/run/docker.sock:/var/run/docker.sock \ 68 | -v `pwd`:`pwd` \ 69 | -e AWS_ACCESS_KEY_ID=${{ env.AWS_ACCESS_KEY_ID }} \ 70 | -e AWS_SECRET_ACCESS_KEY=${{ env.AWS_SECRET_ACCESS_KEY }} \ 71 | -e AWS_SESSION_TOKEN=${{ env.AWS_SESSION_TOKEN }} \ 72 | -e GITHUB_REPOSITORY=$GITHUB_REPOSITORY \ 73 | -e GITHUB_REF_NAME=$GITHUB_REF_NAME \ 74 | -e GITHUB_RUN_NUMBER=$GITHUB_RUN_NUMBER \ 75 | -e GITHUB_SERVER_URL=$GITHUB_SERVER_URL \ 76 | -e GITHUB_RUN_ID=$GITHUB_RUN_ID \ 77 | -e MODE=ci \ 78 | -w `pwd` $SECURITY_SCAN_IMAGE 79 | 80 | - name: Pull transponder 81 | run: | 82 | docker pull $TRANSPONDER_DOCKER_IMAGE 83 | 84 | - name: Run transponder 85 | run: | 86 | docker run --rm -v `pwd`:`pwd` -w `pwd` \ 87 | -e J1_API_KEY=${{ secrets.J1_API_KEY_TRANSPONDER }} \ 88 | -e J1_API_DOMAIN=${{ secrets.J1_API_DOMAIN_TRANSPONDER }} \ 89 | -e J1_ACCOUNT_ID=${{ secrets.J1_ACCOUNT_ID_TRANSPONDER }} \ 90 | $TRANSPONDER_DOCKER_IMAGE -------------------------------------------------------------------------------- /ingest-log4j-vulns/ingest-log4j-vulns.js: -------------------------------------------------------------------------------- 1 | const { JupiterOneClient } = require("@jupiterone/jupiterone-client-nodejs") 2 | const { v4: uuid } = require('uuid'); 3 | const fs = require("fs") 4 | 5 | 6 | const authenticate = async () => { 7 | console.log('Authenticating...'); 8 | 9 | const input = { 10 | accessToken: process.env.J1_ACCESS_TOKEN, 11 | account: process.env.J1_ACCOUNT, 12 | dev: process.env.J1_DEV_ENABLED 13 | }; 14 | 15 | const j1 = new JupiterOneClient(input); 16 | 17 | await j1.init(); 18 | 19 | console.log('Successfully authenticated...'); 20 | 21 | return j1; 22 | }; 23 | 24 | const j1UniqueKeyFileLocation = `${process.cwd()}/j1-scope-key` 25 | 26 | const getScope = () => { 27 | let scope; 28 | if (!fs.existsSync(j1UniqueKeyFileLocation)) { 29 | scope = uuid() 30 | fs.writeFileSync(j1UniqueKeyFileLocation, scope, 'utf8') 31 | } else { 32 | scope = fs.readFileSync(j1UniqueKeyFileLocation, 'utf8') 33 | } 34 | 35 | return scope; 36 | } 37 | 38 | // TODO: make this as a config value so shell script and node program reference same file location 39 | const inputDataFilePath = "./results" 40 | 41 | const sentinelDictionary = { 42 | 0: "ip", 43 | 1: "hostname", 44 | 2: "appname", 45 | 3: "team", 46 | 4: "ignore (y/n)", 47 | 5: "comments", 48 | 6: "md5hash", 49 | 7: "timestamp", 50 | 8: "container", 51 | 9: "image", 52 | 10: "fullpath", 53 | 11: "version", 54 | } 55 | 56 | const main = async () => { 57 | // Bail early if file doesn't exist 58 | // This indicates a problem upstream 59 | if (!fs.existsSync(inputDataFilePath)) return 60 | 61 | // utf8 guarantees our output is returned to us as a string 62 | const input = fs.readFileSync(inputDataFilePath, "utf8")?.trim() 63 | 64 | const j1 = await authenticate() 65 | const scope = getScope(); 66 | 67 | // Ensure at least we have an array contains empty string 68 | const lines = input?.length ? input.split(/\n/) : [] 69 | 70 | // Create entities to upload to J1 71 | const entities = lines.map((line) => { 72 | const sentinelDataProps = line?.toString().split(",") ?? [] 73 | 74 | return { 75 | _key: uuid(), 76 | _type: 'log4j_vulnerability', 77 | _class: 'Finding', 78 | displayName: sentinelDataProps[11], 79 | [sentinelDictionary[0]]: process.env.HOST_IP || sentinelDataProps[0], 80 | [sentinelDictionary[1]]: process.env.HOST_IDENTIFIER || sentinelDataProps[1], 81 | [sentinelDictionary[6]]: sentinelDataProps[6], 82 | [sentinelDictionary[7]]: sentinelDataProps[7], 83 | [sentinelDictionary[8]]: sentinelDataProps[8] === 'true', 84 | [sentinelDictionary[9]]: sentinelDataProps[9], 85 | [sentinelDictionary[10]]: sentinelDataProps[10], 86 | [sentinelDictionary[11]]: sentinelDataProps[11], 87 | } 88 | }) 89 | 90 | // Note: entities of 0 isn't necessarily a bad thing.. 91 | // It's still needed to clear out existing data in the event 92 | // that there were previous vulnerabilities and they have since 93 | // been remediated. 94 | console.log('Entities Uploading :>> ', entities.length); 95 | 96 | await j1.bulkUpload({syncJobOptions: {scope}, entities}) 97 | if (entities.length) { 98 | console.log('Entities may be found with a J1QL query like "Find log4j_vulnerability"') 99 | } 100 | } 101 | 102 | main().catch(console.error) -------------------------------------------------------------------------------- /ingest-log4j-vulns/README.md: -------------------------------------------------------------------------------- 1 | # JupiterOne - Script to Ingest Log4J Vulnerabilities 2 | 3 | This automation example ingests the output of [log4shell_sentinel][1], a 4 | cross-platform tool that scans local filesystems and emits CSV output. This 5 | ingestion script is intended for distribution/deployment to all hosts in your 6 | environment that you would like to scan and remediate for log4j vulnerabilities. 7 | 8 | ## Dependencies / Installation 9 | 10 | This tool is distributed as a Docker image for your convenience (see [Usage With Docker][3] below). 11 | 12 | For non-Docker execution, you will need to: 13 | 1. Clone this repo, containing the shell and Node scripts in this example 14 | 2. Run `npm install` to install dependencies 15 | 3. Install an [OS/arch-appropriate binary of log4shell_sentinel][2] on each target 16 | host. We also provide [pre-built binaries for common OSes here][4]. 17 | 18 | The ingestion script assumes `log4shell_sentinel` is available locally, and is 19 | in your system's `$PATH`. 20 | 21 | You will need to export the following ENV vars for ingestion: 22 | 23 | * `J1_ACCOUNT` 24 | * `J1_ACCESS_TOKEN` 25 | 26 | ## Usage 27 | 28 | `sudo ./scan-for-log4j.sh` - by default, scan the entire filesystem, including container images (recommended by `log4shell_sentinel`) 29 | 30 | `./scan-for-log4j.sh ./some/target/path` - scan only target path, additionally do not use superuser privs 31 | 32 | ### Usage with Docker 33 | 34 | `docker run -v /target/file/path:/scan -e J1_ACCOUNT="$J1_ACCOUNT" -e J1_ACCESS_TOKEN="$J1_ACCESS_TOKEN" -e HOST_IDENTIFIER="$(hostname)" --rm jupiterone/ingest-log4j-vulns` 35 | 36 | Use `-v /:/scan` to scan the entire filesystem (recommended). 37 | 38 | NOTES: 39 | * This does not run as root and does not scan container images. 40 | * The tool expects to have write access to the top-level directory of your volume-mounted scan path. We persist a small file containing a unique token needed for idempotency and to support accurate removal of any Findings ingested for each host, once they are remediated. 41 | * The `HOST_IDENTIFIER` env var is needed since this information is not available inside the running Docker container. 42 | * If desired, you may also specify `-e HOST_IP="some.ip.addr"` to provide the outer hosts' IP address. 43 | 44 | ## Expected Workflow: 45 | 46 | The following suggested workflow can be used to identify and remediate Log4j 47 | vulnerabilities across your entire fleet of hosts. 48 | 49 | Step 1: Deployment 50 | 51 | Deploy this software to your hosts via MDM, Ansible, Chef, etc. Or use Docker, if that is available. 52 | 53 | Step 2: Scanning 54 | 55 | Periodically scan your hosts by creating a CRON job that runs every hour. 56 | 57 | `0 * * * * /path/to/scan-for-log4j.sh` 58 | 59 | or 60 | 61 | `0 * * * * docker run -v /target/file/path:/scan -e J1_ACCOUNT="$J1_ACCOUNT" -e J1_ACCESS_TOKEN="$J1_ACCESS_TOKEN" --rm jupiterone/ingest-log4j-vulns` 62 | 63 | Step 3: Monitoring in JupiterOne 64 | 65 | Issue queries like the following: 66 | 67 | * `Find log4j_vulnerability as v ORDER BY v._createdOn ASC` - vulnerable hosts, oldest findings first 68 | * `Find log4j_vulnerability as v return v.hostname, count(v) as vulns ORDER BY vulns DESC` - show vulnerable hosts, rank ordered by number of vulnerabilities 69 | 70 | Step 4: Remediate Hosts 71 | 72 | As you work to remediate hosts, the above query results will automatically return fewer results over time as these hosts' passing scans report in. 73 | 74 | 75 | 76 | [1]: https://github.com/ossie-git/log4shell_sentinel 77 | [2]: https://github.com/ossie-git/log4shell_sentinel/releases/tag/v1.0.0 78 | [3]: https://github.com/JupiterOne/secops-automation-examples/tree/main/ingest-log4j-vulns#usage-with-docker 79 | [4]: https://github.com/JupiterOne/secops-automation-examples/releases/tag/ingest-log4j-vulns-v0.0.1 80 | -------------------------------------------------------------------------------- /github-codeowners/README.md: -------------------------------------------------------------------------------- 1 | # Automated CODEOWNERS Assignment 2 | 3 | This script is intended to help with the consistent application of [GitHub 4 | CODEOWNERS files][1] for an organization that may not already have these in 5 | place across 100% of its code repositories. 6 | 7 | It will examine each code repository in the org, and create a Pull Request 8 | adding a CODEOWNERS file if one does not already exist, suggesting team 9 | ownership based on commit authors from recent git history. 10 | 11 | ## Prerequisites 12 | 13 | Create one or more GitHub Teams, if you haven't already, and add members to it. 14 | Decide which team should own code repositories when no other teams can be 15 | cleanly identified from git history (the "default" team). 16 | 17 | ## Configuration via Environment Variables 18 | 19 | Copy the `.env.example` file to `.env` and edit it with your token, org, owner, 20 | and default_team values. 21 | 22 | | Environment Variable | Default Value | Description | 23 | | -------------------- | ------------- | ----------- | 24 | | `GITHUB_AUTH_TOKEN` | | Owner-level Personal Access Token (PAT) | 25 | | `ORG` | 'jupiterone' | GitHub Org | 26 | | `OWNER` | 'jupiterone' | GitHub Owner | 27 | | `DEFAULT_TEAM` | 'engineering' | Team to assign by default | 28 | | `RUNMODE` | 'open_pulls' | One of: 'open_pulls' or 'merge_pulls' | 29 | | `ADOPTEDREPOS` | | Optional path to Adopted Repo JSON (see below) | 30 | | `DEBUG` | false | Debug Mode | 31 | | `SILENT` | false | Suppress all logs | 32 | | `ERRLOG` | 'error.log' | Name of error output log | 33 | 34 | ## Installing Dependencies and Running the Script 35 | 36 | ``` 37 | cd codeowners-automation 38 | npm install 39 | node ./index.js 40 | ``` 41 | 42 | ### Run Modes 43 | 44 | #### `open_pulls` Mode 45 | 46 | By default, the `RUNMODE` is set to 'open_pulls', and the script will create pull 47 | requests for all repos without a CODEOWNERS file. 48 | 49 | #### Optional Adopted Repos 50 | 51 | Allow for manual assignment of code ownership (rather than automatic assignment 52 | via repo history inspection) via the `ADOPTEDREPOS` variable. This variable 53 | should be set to a path to a JSON file in the following format: 54 | 55 | ```json 56 | [ 57 | { 58 | "repo": "db-service", 59 | "team": "engineering" 60 | }, 61 | { 62 | "repo": "docs-publishing", 63 | "team": "support" 64 | }, 65 | ... 66 | ] 67 | ``` 68 | 69 | One way to generate this file is to create a shared spreadsheet with the columns 70 | (`repo, team`), and export this as CSV: 71 | 72 | ```csv 73 | repo,team 74 | db-service,engineering 75 | docs-publishing, support 76 | ... 77 | ``` 78 | 79 | Then using an online utility like [convertcsv.com][2] or a CLI utility like 80 | [csvtojson][3], save the converted file to, e.g. `./adoptedrepos.json`. 81 | 82 | Invoke with a command like: 83 | 84 | ``` 85 | ADOPTEDREPOS=./adoptedrepos.json node ./index.js 86 | ``` 87 | 88 | #### `merge_pulls` Mode 89 | 90 | If you'd like to automatically force-merge PRs previously opened with this automation, 91 | you can override the RUNMODE at the command-line, or edit your `.env` file. 92 | 93 | For example: 94 | ``` 95 | RUNMODE=merge_pulls node ./index.js 96 | ``` 97 | 98 | NOTE: This forced merging action is potentially sensitive, and will require 99 | briefly disabling branch protection rules prior to merging, then enabling and 100 | updating the branch-protection rules to include 'Require review from 101 | CODEOWNERS'. You are advised to set `DEBUG=true` when using the 'merge_pulls' 102 | `RUNMODE`, as this will write each repo's original branch protection rules to a 103 | local file prior to modifying them. 104 | 105 | [1]: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 106 | [2]: https://www.convertcsv.com/csv-to-json.htm 107 | [3]: https://www.npmjs.com/package/csvtojson#command-line-usage -------------------------------------------------------------------------------- /playbooks/risk-management.md: -------------------------------------------------------------------------------- 1 | # Risk Management with JupiterOne 2 | 3 | Here's an opinionated guide on Risk Management using the JupiterOne platform: 4 | 5 | ## Step 1: Maintain a version-controlled risk register 6 | 7 | We use GitHub for all version control. Create a private repo for corporate use, say `internal-security-artifacts` or similar. 8 | 9 | In this repo, store your Risks as one or more YAML or JSON files, as [documented in our NodeJS client README][1]. 10 | 11 | For example, you might create a `risks.yml` with the following structure: 12 | 13 | 14 | ``` 15 | --- 16 | - entityId: 17 | entityKey: risk:some-kind-of-identified-risk 18 | entityType: technical_risk 19 | entityClass: Risk 20 | properties: 21 | name: Some Identified Risk 22 | displayName: Some Identified Risk 23 | summary: Corp has identified a risk of type foo 24 | description: 25 | Current controls are insufficient to mitigate technical risk presented by 26 | foo. This situation provides a sufficiently motivated attacker the means 27 | to abuse privileges and/or consume unauthorized resources. 28 | details: 29 | category: technical 30 | threats: privilege escalation, denial-of-service 31 | targets: corporate cloud account 32 | probability: 2-5%, median of 3% 33 | impact: $30,000 34 | score: $600-$1,500 annualized loss exposure 35 | status: open 36 | assessment: corp-risk-assessment-2021 37 | reporter: security.analyst@corp.com 38 | open: true 39 | mitigation: 40 | - monitoring changes to the cloud resource configurations and access 41 | jiraKey: 42 | webLink: 43 | 44 | ``` 45 | 46 | Several of these fields have conventional meaning that will be leveraged by J1 queries: 47 | 48 | | Property | Description | 49 | | ----------- | ----------- | 50 | | category | Type of risk, e.g. physical, logical, human, etc. | 51 | | threats | comma-delimited list of threats posed by the risk | 52 | | targets | asset targets affected by threat: devices, networks, applications, data, users | 53 | | probability | integer model of likelihood: 0-100%, can be a range with a median | 54 | | impact | integer model of business impact if exploited: ideally, dollar values but if that's not easy to obtain, use planning poker card values (0, 1, 2, 3, 5, 8, 13, 20, 40, 100), a fibonacci sequence (0, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...), or orders of magnitude (1x, 10x, 100x, etc.). Pick an impact scoring model and stick with it consistently. Do not use different impact models unless it is to characterize it in monetary terms. Under no circumstances should you use ordinal numbers (e.g., 1st, 2nd, 3rd, 4th, 5th) | 55 | | score | integer product of probability median x impact | 56 | | status | current state of risk, one of: open, mitigated, transferred, accepted | 57 | | assessment | name of associated risk assessment activity, if any | 58 | | reporter | email address of user that identified or reported the risk | 59 | | mitigation | list/array of controls or mitigating activity performed to reduce this risk | 60 | | webLink | url link to additional documentation or issues | 61 | 62 | ## Step 2: Ingest Risks into J1 graph 63 | 64 | Periodically, you can upload `risks.yml` to the graph using the [NodeJS Client][2]: 65 | 66 | Export the ENV vars `J1_ACCOUNT` and `J1_ACCESS_TOKEN`, and issue a command like: 67 | 68 | ``` 69 | j1 -o update --entity -a "$J1_ACCOUNT" -k "$J1_ACCESS_TOKEN" -f ./risks.yml 70 | ``` 71 | 72 | You might also automate this step via GitHub Action or similar CI/CD tooling. 73 | 74 | ## Step 3: Add Insights Dashboard for Risk Registry 75 | 76 | From the [Insights Home][3], click `+` (add), then `Add team board`. Select 'Risk Register' from the list of available templates: 77 | 78 | Dashboard Templates 79 | 80 | ## Step 4: Use Risk Register Dashboard during periodic risk review 81 | 82 | Risk Register Dashboard 83 | 84 | **ProTip**: as an output of this periodic activity, generate additional PRs to your `internal-security-artifacts` repo `risk.yml` file. These PRs can serve as compliance evidence of periodic review activity by the Security team. 85 | 86 | [1]: https://github.com/JupiterOne/jupiterone-client-nodejs#create-or-update-entities-from-a-json-input-file 87 | [2]: https://github.com/JupiterOne/jupiterone-client-nodejs 88 | [3]: https://apps.us.jupiterone.io/insights/home 89 | -------------------------------------------------------------------------------- /security-assessment-report/generate-assessment-report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { JupiterOneClient } = require('@jupiterone/jupiterone-client-nodejs'); 4 | const program = require('commander'); 5 | const fs = require('fs'); 6 | const pdf = require("markdown-pdf"); 7 | 8 | const { 9 | J1_ACCOUNT: account, 10 | J1_API_TOKEN: accessToken, 11 | } = process.env; 12 | 13 | function parseArrayString(text) { 14 | let output = ''; 15 | if (text) { 16 | if (Array.isArray(text)) { 17 | text.forEach(function(item) { 18 | output += `${item}\n\n`; 19 | }); 20 | } else { 21 | output = text; 22 | } 23 | } 24 | return output; 25 | } 26 | 27 | async function main() { 28 | program 29 | .usage('[options]') 30 | .option('--assessment ', 'The name an assessment entity in J1.') 31 | .parse(process.argv); 32 | 33 | const j1Client = new JupiterOneClient({ account, accessToken }); 34 | await j1Client.init(); 35 | 36 | // Query J1 for the assessment with name provided as input 37 | const query = `Find Assessment with name='${program.assessment}'`; 38 | const assessments = await j1Client.queryV1(query); 39 | 40 | for (const a of assessments || []) { 41 | if (a.entity && a.properties) { 42 | // Build Markdown string for report overview 43 | const assessors = Array.isArray(a.properties.assessors) 44 | ? `\n\n- ${a.properties.assessors.join('\n- ')}` 45 | : a.properties.assessors; 46 | const reportOverview = 47 | `# ${a.entity.displayName}\n\n` + 48 | `**Assessor(s)**: ${assessors}\n\n` + 49 | `**Completed On**: ${a.properties.completedOn}\n\n` + 50 | `## Overview\n\n` + 51 | `${a.properties.summary ? '### Summary\n\n' + a.properties.summary + '\n\n' : ''}` + 52 | `${a.properties.description ? '### Description\n\n' + a.properties.description + '\n\n' : ''}` + 53 | `${a.properties.details ? '### Details\n\n' + a.properties.details + '\n\n' : ''}`; 54 | 55 | // Query J1 for all Findings or Risks identified by the Assessment 56 | const findingsQuery = `Find (Risk|Finding) that relates to Assessment with name='${program.assessment}'`; 57 | const findings = await j1Client.queryV1(findingsQuery); 58 | const reportFindingsTOC = []; 59 | const reportFindings = []; 60 | 61 | // Build Markdown string with details of each finding 62 | if (findings && findings.length > 0) { 63 | reportFindingsTOC.push('## List of Findings\n\n'); 64 | reportFindings.push('## Findings Details\n\n'); 65 | 66 | for (const f of findings) { 67 | if (f.entity && f.properties) { 68 | const score = 69 | f.properties.numericSeverity ? `(score: ${f.properties.numericSeverity})` : ''; 70 | const summary = 71 | f.properties.summary ? `${f.properties.summary}\n\n` : ''; 72 | const steps = 73 | f.properties.stepsToReproduce 74 | ? '**Steps to Reproduce:**\n\n' + parseArrayString(f.properties.stepsToReproduce) + '\n\n' 75 | : ''; 76 | const findingOverview = 77 | `### ${f.entity.displayName}\n\n` + 78 | '`' + f.entity._type + '`\n\n' + 79 | `**Severity**: ${f.properties.severity} ${score}\n\n` + 80 | `${summary}` + 81 | `> ${f.properties.description}\n\n` + 82 | `${steps}`; 83 | 84 | // Other finding details 85 | const regex = /severity|numericSeverity|summary|description|stepsToReproduce/; 86 | const findingDetails = []; 87 | Object.keys(f.properties).forEach(function(key) { 88 | const match = regex.exec(key); 89 | if (!match) { 90 | const value = key.endsWith('On') 91 | ? new Date(f.properties[key]).toDateString() 92 | : f.properties[key]; 93 | const line = value && value.length > 24 ? '\n\n ' : ''; 94 | findingDetails.push(`- **${key}**:${line} ${value}\n\n`); 95 | } 96 | }); 97 | reportFindingsTOC.push(`- ${f.entity.displayName}\n\n`); 98 | reportFindings.push(findingOverview); 99 | if (findingDetails.length > 0) { 100 | reportFindings.push('#### Additional Details\n\n' + findingDetails.join('')); 101 | } 102 | } 103 | } 104 | } 105 | 106 | // Generate Markdown and PDF files 107 | const output = 108 | reportOverview + reportFindingsTOC.join('') + reportFindings.join(''); 109 | const reportFilename = `report-${a.id}`; 110 | fs.writeFileSync(`./${reportFilename}.md`, output); 111 | pdf().from(`./${reportFilename}.md`).to(`./${reportFilename}.pdf`, function() { 112 | console.log(`Created Assessment Report: ${reportFilename}`); 113 | }) 114 | } 115 | } 116 | } 117 | 118 | main(); -------------------------------------------------------------------------------- /security-privacy-design/rfc-template.md: -------------------------------------------------------------------------------- 1 | **Instructions (remove before committing)** 2 | 3 | - Fill out all the sections. 4 | - Open a PR to merge your changes into master and invite people you know will 5 | need to review the RFC 6 | - Post a notice to your team chat room (e.g. #team-phc) so other interested 7 | devs can review as needed. If the RFC might impact other teams then post a 8 | notice to #dev also. 9 | 10 | # Template RFC 11 | 12 | - Start Date: (fill me in with today's date, YYYY-MM-DD) 13 | - RFC PR: (link to the PR) 14 | - Issue: (link to the Jira Issue) 15 | 16 | ## Summary 17 | 18 | One paragraph explanation of the feature. 19 | 20 | ## Motivation 21 | 22 | Why are we doing this? What use cases does it support? What is the expected 23 | outcome? 24 | 25 | Please focus on explaining the motivation so that if this RFC is not accepted, 26 | the motivation could be used to develop alternative solutions. In other words, 27 | enumerate the constraints you are trying to solve without coupling them too 28 | closely to the solution you have in mind. 29 | 30 | ## Detailed design 31 | 32 | This is the bulk of the RFC. Explain the design in enough detail for somebody 33 | familiar with the software platform to understand, and for somebody familiar 34 | with the implementation to implement. This should get into specifics and 35 | corner-cases, and include examples of how the feature is used. Any new 36 | terminology should be defined here. 37 | 38 | ## How We Teach This 39 | 40 | What names and terminology work best for these concepts and why? How is this 41 | idea best presented? As a continuation of existing npm patterns, existing 42 | software platform patterns, or as a wholly new one? 43 | 44 | Would the acceptance of this proposal mean our documentation must be 45 | re-organized or altered? Does it change how it is taught to new users at any 46 | level? 47 | 48 | How should this feature be introduced and taught to existing users? 49 | 50 | ## Drawbacks 51 | 52 | Why should we _not_ do this? Please consider the impact on teaching people, on 53 | the integration of this feature with other existing and planned features, on the 54 | impact of churn on existing users. 55 | 56 | There are tradeoffs to choosing any path, please attempt to identify them here. 57 | 58 | ## Security considerations 59 | 60 | ### Data Flow 61 | 62 | Does this feature collect or process additional data? Does it impact the current 63 | data flow of the system/application? 64 | 65 | If so, create new or update the existing data flow diagram and document the 66 | data flow. 67 | 68 | ### Secrets 69 | 70 | Does this feature involve usage of additional secrets (API keys, tokens, etc.), 71 | either external (i.e. storing and using secrets from a provider) or internal 72 | (i.e. generating and using secrets as an internal component)? 73 | 74 | If so, document the secret management process. 75 | 76 | ### Attack Scenarios 77 | 78 | How could an attacker abuse this design? What risks does this approach present 79 | and what mitigations can be pursued? What security requirements need to be 80 | included in the implementation? 81 | 82 | An example of how to document this: 83 | 84 | - **Abuse case name** 85 | - _Risk_ -- a description of the abuse case and the risks identified 86 | - _Mitigation_ -- what is being put in place as mitigation controls 87 | 88 | This is a practice to ensure that some level of security considerations is 89 | always included in the design of a new feature, component or process. 90 | 91 | Note that this is a lightweight, fast-path approach but does not replace a full 92 | threat model, which may be needed as a follow up to the RFC or is completed 93 | separately for the broader product in scope. A full threat model can be a 94 | complex and lengthy process - see 95 | [OWASP document](https://www.owasp.org/index.php/Application_Threat_Modeling). 96 | 97 | ## Privacy Considerations 98 | 99 | Does this feature introduce a new use case of data processing or new types of 100 | user data being processed? 101 | 102 | If so, review and document the following. Keep in mind that privacy management 103 | is more than capturing user consents. 104 | 105 | - Is the use case already covered by existing Privacy Policy and EULA (or Terms 106 | and Conditions)? 107 | 108 | - Do we need to update or capture additional consent? 109 | 110 | - Is the data included in the removal process upon user request or account 111 | deletion? 112 | 113 | - Does the use of user data align with user expectations? 114 | 115 | - How are users made aware of this additional use case / data collection in 116 | order to minimize surprise? 117 | 118 | > Consider just-in-time modal or notification for both awareness/education and 119 | > consent capture, if applicable. 120 | 121 | - What controls are available to users on this data processing use case? 122 | For example: 123 | 124 | - 125 | 126 | - 127 | 128 | - Do we have a Help article documenting this use case and the privacy controls 129 | available to users? 130 | 131 | ## Alternatives 132 | 133 | What other designs have been considered? What is the impact of not doing this? 134 | 135 | ## Unresolved questions 136 | 137 | Optional, but suggested for first drafts. What parts of the design are still 138 | TBD? 139 | -------------------------------------------------------------------------------- /yarn2npm/yarn2npm.js: -------------------------------------------------------------------------------- 1 | const {execSync} = require('child_process') 2 | 3 | const git = require('isomorphic-git'); 4 | const http = require('isomorphic-git/http/node'); 5 | const gitHubAuthFunct = () => { return { username: process.env.GITHUB_AUTH_TOKEN, password: '' }; }; 6 | 7 | const org = 'jupiterone'; 8 | const prbranch = "yarn2npm-patch-1"; 9 | 10 | const fs = require('fs'); 11 | const path = require("path"); 12 | const repoName = process.argv.slice(2); 13 | const dir = `./${repoName}`; 14 | const backupDir = 'yarn2npm'; 15 | 16 | 17 | 18 | const main = async () => { 19 | try { 20 | const origPath = process.cwd(); 21 | 22 | //Create github branch 23 | await createYarn2NpmBranch(repoName, dir, org); 24 | 25 | if (fs.existsSync(`${dir}/package-lock.json`)){ 26 | console.log(`${repoName} is already configured for npm`); 27 | return[]; 28 | } 29 | 30 | //Create backup dir 31 | if (!fs.existsSync(`${dir}/${backupDir}`)){ 32 | fs.mkdirSync(`${dir}/${backupDir}`); 33 | } 34 | 35 | //Backup package.json 36 | let copyFile = 'orig-package.json'; 37 | console.log(`Backing up package.json to ${copyFile}`); 38 | fs.copyFile(`${dir}/package.json`, `${dir}/${backupDir}/${copyFile}`, (err) => { 39 | if (err) throw err; 40 | }); 41 | await git.add({ fs, dir, filepath: `${backupDir}/${copyFile}` }); 42 | 43 | //remove yarn.lock 44 | console.log(`Removing yarn.lock`); 45 | fs.unlink(`${dir}/yarn.lock`, (err) => { 46 | if (err) throw err; 47 | }); 48 | await git.remove({ fs, dir, filepath: 'yarn.lock' }); 49 | 50 | //npm install 51 | console.log(`Running npm install`); 52 | process.chdir(dir); 53 | try{ 54 | execSync('npm install', {stdio: 'inherit'}, function(error) { 55 | if (error) { 56 | console.log(`exit: ${error.code}`); 57 | } 58 | }); 59 | } 60 | catch(e){ 61 | console.log(`Error running npm install. Creating lock file ${backupDir}/failed.lock`); 62 | fs.closeSync(fs.openSync(`${origPath}/${backupDir}/failed.lock`, 'a')); 63 | await git.add({ fs, dir: `${origPath}/${backupDir}`, filepath: 'failed.lock' }); 64 | } 65 | process.chdir(origPath); 66 | await git.add({ fs, dir, filepath: 'package-lock.json' }); 67 | 68 | 69 | //Grab the npm log 70 | const npmlogDir = (`${require('os').homedir()}/.npm/_logs/`); 71 | const newestLog = (fs.readdirSync(npmlogDir) 72 | .filter((file) => fs.lstatSync(path.join(npmlogDir, file)).isFile()) 73 | .map((file) => ({ file, mtime: fs.lstatSync(path.join(npmlogDir, file)).mtime })) 74 | .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()))[0].file; 75 | 76 | console.log(`Backing up npm logs`); 77 | fs.copyFile (`${npmlogDir}/${newestLog}`, `${dir}/${backupDir}/${newestLog}`, (err) => { 78 | if (err) throw err; 79 | }); 80 | await git.add({ fs, dir, filepath: `${backupDir}/${newestLog }` }); 81 | 82 | //Find and replace yarn commands 83 | console.log(`Replacing yarn commands with npm`); 84 | const packageFile = fs.readFileSync(`${dir}/package.json`, { 85 | encoding: 'utf8', 86 | flag: 'r', 87 | }) 88 | .toString().replace(/yarn/g,'npm run'); 89 | 90 | fs.writeFile(`${dir}/package.json`, packageFile, 'utf8', function (err) { 91 | if (err) return console.log(err); 92 | }); 93 | await git.add({ fs, dir, filepath: 'package.json' }); 94 | 95 | await pushChanges(dir); 96 | 97 | return []; 98 | } catch (e) { 99 | console.log(`Error ${e}`); 100 | } 101 | } 102 | 103 | async function pushChanges (dir) { 104 | console.log(`Pushing changes`); 105 | await git.commit({ 106 | fs, 107 | dir, 108 | author: { 109 | name: 'J1 Security', 110 | email: 'security@jupiterone.com' 111 | }, 112 | message: 'Updating yarn to npm' 113 | }); 114 | 115 | await git.push({ 116 | fs, 117 | http, 118 | dir, 119 | remote: 'origin', 120 | force: true, 121 | onAuth: gitHubAuthFunct 122 | }); 123 | } 124 | 125 | async function createYarn2NpmBranch (repo, dir, org, branch = prbranch) { 126 | console.log(`Cloning Repo ${org}/${repo}...`); 127 | await cloneRepo(repo, dir, org); 128 | console.log(`Creating branch ${branch} for ${repo}...`); 129 | await checkoutBranch(dir, branch); 130 | return branch; 131 | } 132 | 133 | async function cloneRepo (repo, dir, org) { 134 | console.log(`Cloning https://github.com/${org}/${repo}`) 135 | try { 136 | await git.clone({ 137 | fs, 138 | http, 139 | dir, 140 | url: `https://github.com/${org}/${repo}`, 141 | onAuth: gitHubAuthFunct, 142 | singleBranch: true, 143 | depth: 2 144 | }); 145 | } catch (e) { 146 | console.log(`error here 2: ${e}}`); 147 | } 148 | } 149 | 150 | async function checkoutBranch (dir, ref) { 151 | await git.branch({ 152 | fs, 153 | dir, 154 | ref, 155 | checkout: true 156 | }); 157 | } 158 | 159 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example automation scripts using JupiterOne platform 2 | 3 | This repo provides several examples on how to maintain security/compliance 4 | as code and to automate SecOps using the JupiterOne platform. 5 | 6 | The examples are in either `bash` or `javascript/typescript`. 7 | 8 | These scripts are provided as-is. For questions, please post in the 9 | [jupiterone-community #dev][1] Slack workspace. 10 | 11 | ## Playbooks 12 | 13 | * [Risk Management][21]: An opinionated guide on Risk Management using the JupiterOne platform. 14 | 15 | ## Folder Contents 16 | 17 | The following is a list of provided examples and their brief summary: 18 | 19 | | Automation | Folder | Description | 20 | | ---------------------------------- | ----------------------------- | ---------------------------------------------------------------- | 21 | | **GitHub CODEOWNERS Creation** | [`/github-codeowners`][14] | Consistent creation of **CODEOWNERS files for your GitHub Org**. | 22 | | **Ingest CycloneDX SBOM File** | [`/ingest-cyclonedx-sbom`][15] | Ingest **`CodeRepo -USES-> CodeModule`** graph data into JupiterOne. | 23 | | **Ingest Log4J Vulns** | [`/ingest-log4j-vulns`][16] | Ingests the output of **`log4shell_sentinel`**. Intended for distribution/deployment to all hosts in your environment that you would like to scan and remediate for **log4j vulnerabilities**. | 24 | | **NPM Inventory** | [`/npm-inventory`][17] | High-fidelity ingestion of **`CodeRepo -USES-> CodeModule`** graph data into JupiterOne, for NPM-specific repos. | 25 | | **Security Assessment Reporting** | [`/security-assessment-report`][3] | Query for any assessment object from JupiterOne and its findings to **generate a PDF document** as output. | 26 | | **Security Assessments and Findings** | [`/security-assessment`][2] | **Document** manual security testing, assessments, and **findings in code** (YAML), and publish to JupiterOne graph for reporting and visualization. | 27 | | **Security Privacy Design RFC Template** | [`/security-privacy-design`][6] | **RFC Template** documenting security considerations at design-time. | 28 | | **Generate SBOM from graph data** | [`/software-bill-of-materials`][18] | Utilize **`CodeRepo -USES-> CodeModule`** graph data to **create a CycloneDX SBOM** file. | 29 | | **Summary Relationships** | [`/summary-relationships`][19] | Create **relationship shortcuts** that summarize complex IAM traversals to simplify queries. | 30 | | **Third Party Vendors** | [`/vendor-management`][4] | Documenting **details about third party vendor in code** (YAML), including security review status, vendor managers, who has access, etc. See also `vendor-stack` below. | 31 | 32 | 33 | ## Other useful integrations and custom automation utilties outside this Repo 34 | 35 | | Utility/Integration | Location | Description | 36 | | ---------------------------------- | ----------------------------- | ---------------------------------------------------------------- | 37 | | **Map Repo Dependencies** | [`map-repo-dependencies`][7] | Ingest data from **NPM package files** (e.g. `package.json`) in your local code repos to create entities and relationships in your JupiterOne graph, so that you can query and **visualize your code repo dependencies**. 38 | | **Detect and Alert on Specific PRs** | [`bitbucket-pr-detector`][8] | **Detect particular kind of pull requests** (for example, a RFC document for a new product feature that includes security and privacy considerations) and **alert the security team** about it. | 39 | | **Enforce Code Review and Security Policies in CI/CD** | [`change-management-client`][9] | A package to **enforce code review and security policies** for pull request approval, author and reviewer validation, and vulnerability checks by collecting and analyzing data from the JupiterOne graph. For an example of its usage, check out the [`change-management-example`][10] repo. | 40 | | **Discover local/on-prem devices using Nmap** | [`graph-nmap`][12] | Use **`Nmap`** to scan local networks to **discover on-prem devices** and create entities to push to JupiterOne graph. | 41 | | **Detect Leaked Secrets in Code** | [`graph-gitleaks-findings`][5] | Use **`gitleaks`** to automate **detection of leaked secrets** in your code repos and publish the findings to your JupiterOne graph for reporting and visualization. | 42 | | **Ingest Vuls.io Findings** | [`graph-vuls-findings`][11] | Ingest **`vuls`** scan reports into JupiterOne graph for reporting and visualization. | 43 | | **Map DNS records to their targets via Shodan data** | [`nslookup-shodan`][13] | Use **shodan** to enrich the domain records mapping in a JupiterOne graph. Identifies domain records that do not already point to a known internal asset, discovers the asset via Shodan, and maps the record to the target host. | 44 | | **Vendor Stack** | [`vendor-stack`][20] | A **library of common technology vendors** used by modern companies, and useful properties for each vendor. | 45 | 46 | 47 | ## Prerequisites and dependencies 48 | 49 | For most of the examples and templates included in this repo, you will need 50 | `jupiterone-client-nodejs`. It has been added as a dependency to this project. 51 | You can also install it globally: 52 | 53 | ```bash 54 | npm install @jupiterone/jupiterone-client-nodejs -g 55 | ``` 56 | 57 | You will need the following environment variables in your local `.env` file 58 | 59 | ```text 60 | J1_ACCOUNT_ID=yourAccountId 61 | J1_API_TOKEN=yourToken 62 | ``` 63 | 64 | [1]: https://jupiterone-community.slack.com/messages/CJMV4SFV5 65 | [2]: ./security-assessment/README.md 66 | [3]: ./security-assessment-report/README.md 67 | [4]: ./vendor-management/README.md 68 | [5]: https://github.com/JupiterOne/graph-gitleaks-findings 69 | [6]: ./security-privacy-design/rfc-template.md 70 | [7]: https://github.com/JupiterOne/map-repo-dependencies 71 | [8]: https://github.com/JupiterOne/bitbucket-pr-detector 72 | [9]: https://github.com/JupiterOne/change-management-client 73 | [10]: https://github.com/JupiterOne/change-management-example 74 | [11]: https://github.com/JupiterOne/graph-vuls-findings 75 | [12]: https://github.com/JupiterOne/graph-nmap 76 | [13]: https://github.com/JupiterOne/nslookup-shodan 77 | [14]: ./github-codeowners/README.md 78 | [15]: ./ingest-cyclonedx-sbom/README.md 79 | [16]: ./ingest-log4j-vulns/README.md 80 | [17]: ./npm-inventory/README.md 81 | [18]: ./software-bill-of-materials/README.md 82 | [19]: ./summary-relationships/README.md 83 | [20]: https://github.com/JupiterOne/vendor-stack 84 | [21]: ./playbooks/risk-management.md 85 | -------------------------------------------------------------------------------- /npm-inventory/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as glob from "glob"; 2 | import { join, dirname } from "path"; 3 | import * as fs from "fs"; 4 | import { retry } from "@lifeomic/attempt"; 5 | 6 | const JupiterOneClient = require("@jupiterone/jupiterone-client-nodejs"); 7 | 8 | if (!process.env.J1_API_TOKEN) { 9 | throw new Error('Missing J1_API_TOKEN ENV Var!'); 10 | } 11 | 12 | if (!process.env.J1_ACCOUNT) { 13 | throw new Error('Missing J1_ACCOUNT ENV Var!'); 14 | } 15 | 16 | export interface Dependency { 17 | name: string; 18 | version: string; 19 | license: string; 20 | direct: boolean; 21 | purl: string; 22 | } 23 | 24 | function seekLicenseFile(moduleDir): string { 25 | // best-effort attempt to retrieve first line that looks like it might contain 26 | // RegExp(/LICENSE/i) from likely files in filesystem 27 | 28 | const pattern = moduleDir + '/LICENSE*'; 29 | const licenseFiles = glob.sync(pattern); 30 | const licenseFile = licenseFiles[0]; 31 | if (!licenseFile) { 32 | return 'unset'; 33 | } 34 | const lines = fs.readFileSync(licenseFile, 'utf8').split('\n'); 35 | let lineMatch; 36 | const matched = lines.some(line => { 37 | lineMatch = line; 38 | return line.match(/\bLICENSE\b/i); 39 | }); 40 | const license = matched ? lineMatch : 'unset'; 41 | return license; 42 | } 43 | 44 | function scrutinizeLicense(pkg, moduleDir): string { 45 | if ((pkg.license || {}).type) { 46 | return pkg.license.type; 47 | } 48 | if ((pkg.licenses || {}).type) { 49 | return pkg.licenses.type; 50 | } 51 | if (pkg.license && !pkg.license.match(/see license/i)) { 52 | return pkg.license; 53 | } 54 | return seekLicenseFile(moduleDir); 55 | } 56 | 57 | async function inventoryPackages(dir: string, directDependencies: string[]): Promise { 58 | const modulePaths = glob.sync(join(dir, "node_modules/**/package.json")); 59 | const dependencyCSVSet: Set = new Set(); 60 | modulePaths.forEach((modulePath) => { 61 | const pkg = require(modulePath); // load package.json from module 62 | if (!pkg.name || !pkg.version) { 63 | return; // skip non top-level package.json files 64 | } 65 | const license = scrutinizeLicense(pkg, join(dir, dirname(modulePath))); 66 | const isDirect = directDependencies.includes(pkg.name); 67 | dependencyCSVSet.add(`${pkg.name},${pkg.version},${license},${isDirect ? 'direct' : 'indirect'}`); 68 | }); 69 | const inventory: Dependency[] = []; 70 | Array.from(dependencyCSVSet).forEach(dependencyCSV => { 71 | const [name, version, license, directStr] = dependencyCSV.split(','); 72 | inventory.push({ 73 | name, 74 | version, 75 | license, 76 | purl: 'pkg:npm/' + name + '@' + version, 77 | direct: directStr === 'direct' 78 | }); 79 | }); 80 | 81 | return inventory; 82 | } 83 | 84 | async function run (baseDir): Promise { 85 | const j1Client = await new JupiterOneClient({ 86 | account: process.env.J1_ACCOUNT, 87 | accessToken: process.env.J1_API_TOKEN, 88 | dev: false 89 | }).init(); 90 | 91 | const repoPkg = require(join(baseDir, 'package.json')); 92 | // NOTE: NPM package name may be different than repo name 93 | const repoName = baseDir.split('/').pop(); 94 | 95 | console.log('Gathering CodeRepo data from JupiterOne...'); 96 | const codeRepoIdMap = await getCodeRepoIdMap(j1Client); 97 | 98 | const codeRepoEntityId = codeRepoIdMap[repoName]; 99 | if (!codeRepoEntityId) { 100 | console.error(`ERROR: ${repoName} doesn't appear to be a CodeRepo known to JupiterOne!`); 101 | process.exit(3); 102 | } 103 | 104 | const npmPackageIdMap = await getNPMPackageIdMap(j1Client); 105 | console.log(`${Object.keys(codeRepoIdMap).length} coderepos found in J1`); 106 | console.log(`${Object.keys(npmPackageIdMap).length} NPM packages found in J1`); 107 | 108 | console.log(`inventory for ${repoPkg.name}@${repoPkg.version}, license: ${repoPkg.license}:\n`); 109 | 110 | // TODO: flag prod/dev deps separately 111 | const directDependencies = Object.keys(repoPkg.dependencies || {}).concat(Object.keys(repoPkg.devDependencies || {})); 112 | 113 | const inventory = await inventoryPackages(baseDir, directDependencies); 114 | console.log(`${inventory.length} unique dependencies`); 115 | const directInventory = inventory.filter(i => i.direct); 116 | 117 | const attemptOptions = { 118 | delay: 20000, 119 | factor: 1.5, 120 | maxAttempts: 0, 121 | maxDelay: 70, 122 | beforeAttempt (context, options): void { 123 | console.log(`attempt number ${context.attemptNum}...`); 124 | } 125 | }; 126 | 127 | for (const dep of directInventory) { 128 | let packageId = npmPackageIdMap[dep.name]; 129 | let res: any; 130 | if (!packageId) { 131 | console.log(`Creating CodeModule entity for ${dep.name} npm_package in J1...`); 132 | res = await retry(() => { 133 | return j1Client.createEntity( 134 | `npm_package:${dep.name}`, //_key 135 | 'npm_package', //_type 136 | 'CodeModule', //_class 137 | { 138 | displayName: dep.name, 139 | fullName: dep.name, 140 | name: dep.name.split('/').pop(), 141 | license: dep.license, 142 | scope: dep.name.split('/')[0].replace(/^@/,''), 143 | version: dep.version 144 | } 145 | ); 146 | }, attemptOptions); 147 | packageId = res.vertex.entity._id; 148 | } 149 | console.log(`Creating ${repoName} -USES-> ${dep.name} relationship in J1...`); 150 | res = await retry(() => { 151 | return j1Client.createRelationship( 152 | `${codeRepoEntityId}:USES:${packageId}`, //_key 153 | 'coderepo_uses_codemodule', //_type 154 | 'USES', //_class 155 | codeRepoEntityId, //fromId 156 | packageId, //toId 157 | { 158 | displayName: `${repoName}:USES:${dep.name}`, 159 | directDependency: dep.direct, 160 | devDependency: Object.keys(repoPkg.devDependencies).includes(dep.name), 161 | version: dep.version, 162 | } 163 | ); 164 | }, attemptOptions); 165 | } 166 | 167 | console.log(`${directInventory.length} direct dependencies found.`) 168 | } 169 | 170 | async function getCodeRepoIdMap (j1Client: typeof JupiterOneClient): Promise { 171 | const cacheFile = `/tmp/codeRepoIdMap-${process.env.J1_ACCOUNT}.json`; 172 | if (fs.existsSync(cacheFile)) { 173 | return require(cacheFile); 174 | } 175 | 176 | const repos = await j1Client.queryV1('Find CodeRepo as c return c.name as name, c._id as id'); 177 | const map = {}; 178 | repos.forEach(repo => { 179 | map[repo.name] = repo.id; 180 | }); 181 | 182 | fs.writeFileSync(cacheFile, JSON.stringify(map), 'utf8'); 183 | return map; 184 | } 185 | 186 | async function getNPMPackageIdMap (j1Client: typeof JupiterOneClient, shouldCache = false): Promise { 187 | const cacheFile = `/tmp/codeModuleIdMap-${process.env.J1_ACCOUNT}.json`; 188 | if (shouldCache && fs.existsSync(cacheFile)) { 189 | return require(cacheFile); 190 | } 191 | 192 | const repos = await j1Client.queryV1('Find npm_package as p return p.id as name, p._id as id'); 193 | const map = {}; 194 | repos.forEach(repo => { 195 | map[repo.name] = repo.id; 196 | }); 197 | 198 | if (shouldCache) { 199 | fs.writeFileSync(cacheFile, JSON.stringify(map), 'utf8'); 200 | } 201 | return map; 202 | } 203 | 204 | run(process.argv[2]).catch(console.error); 205 | -------------------------------------------------------------------------------- /ingest-cyclonedx-sbom/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { retry } from "@lifeomic/attempt"; 3 | import { JupiterOneClient } from '@jupiterone/jupiterone-client-nodejs'; 4 | import { J1Dependency, J1UsesRelationship, SBOM, SBOMComponent } from "./types"; 5 | import minimist from "minimist"; 6 | 7 | /* eslint-disable 8 | @typescript-eslint/no-use-before-define, 9 | @typescript-eslint/no-explicit-any, 10 | */ 11 | 12 | function usage(): void { 13 | console.log('ingest-cyclonedx-sbom --sbom --repo [--devDeps ] [--directDeps ] [--directOnly]'); 14 | console.log(' --sbom path to sbom file to ingest'); 15 | console.log(' --repo name of coderepo ingest '); 16 | console.log(' (optional) --devDeps path to json file array of dev dependencies'); 17 | console.log(' (optional) --directDeps path to json file array of direct dependencies'); 18 | console.log(' (optional) --directOnly only ingest direct dependencies (requires --directDeps)'); 19 | } 20 | 21 | async function run ( 22 | pathToSBOM: string, 23 | repoName: string, 24 | devDependencies: string[], 25 | directDependencies: string[] 26 | ): Promise { 27 | const SBOMComponents = await getSBOMComponents(pathToSBOM); 28 | 29 | const j1Client = await initJ1Client(); 30 | 31 | console.log('Gathering CodeRepo data from JupiterOne...'); 32 | const codeRepoIdMap = await getCodeRepoIdMap(j1Client); 33 | console.log(`${Object.keys(codeRepoIdMap).length} coderepos found in J1`); 34 | 35 | const codeRepoEntityId = codeRepoIdMap[repoName]; 36 | if (!codeRepoEntityId) { 37 | die(`ERROR: ${repoName} doesn't appear to be a CodeRepo known to JupiterOne!`, 3); 38 | } 39 | 40 | console.log('Gathering CodeModule data from JupiterOne...'); 41 | const codeModuleIdMap = await getCodeModuleIdMap(j1Client); 42 | console.log(`${Object.keys(codeModuleIdMap).length} CodeModules found in J1`); 43 | 44 | let ingestedEntitiesCount = 0; 45 | for (const component of SBOMComponents) { 46 | let res: any; 47 | const purl = getPurl(component); 48 | let packageId = codeModuleIdMap[purl]; 49 | 50 | // normalize component name for lookup in optional hint arrays 51 | const normalizedComponentName = component.group ? component.group + '/' + component.name : component.name; 52 | 53 | if (directOnly && !directDependencies.includes(normalizedComponentName)) { 54 | console.log(`Skipping transitive dep: ${normalizedComponentName}`); 55 | continue; // skip transitive deps if --directOnly is specified 56 | } 57 | 58 | if (!packageId) { 59 | 60 | const dependencyProps: J1Dependency = { 61 | displayName: component.name, 62 | fullName: purl, 63 | name: component.name, 64 | purl, 65 | license: getLicense(component), 66 | scope: component.group, 67 | version: component.version, 68 | from: `ingest-cyclonedx-sbom-${repoName}` 69 | }; 70 | 71 | // e.g. pkg:npm/commander@8.3.0 ingests as type 'npm_package' 72 | const j1Type = getType(component); 73 | 74 | process.stdout.write(`Creating CodeModule entity for ${purl} ${j1Type} in J1..`); 75 | res = await retry(() => { 76 | return j1Client.createEntity( 77 | `${j1Type}:${component.name}`, //_key 78 | j1Type, //_type 79 | [ 'CodeModule' ], //_class 80 | dependencyProps 81 | ); 82 | }, attemptOptions); 83 | console.log(' ok'); 84 | packageId = res.vertex.entity._id; 85 | ingestedEntitiesCount++; 86 | } 87 | 88 | process.stdout.write(`Creating ${repoName}:USES:${component.name} relationship in J1..`); 89 | 90 | const relationshipProps: J1UsesRelationship = { 91 | displayName: `${repoName}:USES:${component.name}`, 92 | version: component.version, 93 | }; 94 | 95 | if (devDependencies.length) { 96 | relationshipProps.devDependency = devDependencies.includes(normalizedComponentName); 97 | } 98 | 99 | if (directDependencies.length) { 100 | relationshipProps.directDependency = directDependencies.includes(normalizedComponentName); 101 | } 102 | 103 | res = await retry(() => { 104 | return j1Client.createRelationship( 105 | `${codeRepoEntityId}:USES:${packageId}`, //_key 106 | 'coderepo_uses_codemodule', //_type 107 | 'USES', //_class 108 | codeRepoEntityId, //fromId 109 | packageId, //toId 110 | relationshipProps 111 | ); 112 | }, attemptOptions); 113 | console.log(' ok'); 114 | } 115 | 116 | console.log(`${ingestedEntitiesCount} total dependencies ingested.`) 117 | } 118 | 119 | function getLicense(component: SBOMComponent): string { 120 | let license; 121 | try { 122 | license = component.licenses[0].license.id; 123 | } catch { 124 | license = 'Unknown'; 125 | } 126 | return license; 127 | } 128 | 129 | function getType(component: SBOMComponent): string { 130 | let j1Type; 131 | try { 132 | // e.g. pkg:npm/foo@0.0.1 -> npm_package 133 | j1Type = component.purl.split('/')[0].split(':')[1] + '_package'; 134 | } catch { 135 | j1Type = 'unknown_package'; 136 | 137 | } 138 | return j1Type; 139 | } 140 | 141 | function getPurl(component: SBOMComponent): string { 142 | // url-decode '@' chars 143 | return component.purl.replace(/%40/g, '@'); 144 | } 145 | 146 | async function initJ1Client(): Promise { 147 | return new JupiterOneClient({ 148 | account: process.env.J1_ACCOUNT, 149 | accessToken: process.env.J1_API_TOKEN, 150 | dev: !!(process.env.J1_DEV_ENABLED) 151 | }).init(); 152 | } 153 | 154 | async function getCodeRepoIdMap (j1Client: JupiterOneClient): Promise { 155 | const cacheFile = `/tmp/codeRepoIdMap-${process.env.J1_ACCOUNT}.json`; 156 | if (fs.existsSync(cacheFile) && !process.env.NOCACHE_J1_QUERIES) { 157 | return require(cacheFile); 158 | } 159 | 160 | const repos = await j1Client.queryV1('Find CodeRepo as c return c.name as name, c._id as id'); 161 | const map = {}; 162 | repos.forEach(repo => { 163 | map[repo.name] = repo.id; 164 | }); 165 | 166 | fs.writeFileSync(cacheFile, JSON.stringify(map), 'utf8'); 167 | return map; 168 | } 169 | 170 | async function getCodeModuleIdMap (j1Client: JupiterOneClient, shouldCache = false): Promise { 171 | const cacheFile = `/tmp/codeModuleIdMap-${process.env.J1_ACCOUNT}.json`; 172 | if (shouldCache && fs.existsSync(cacheFile) && !process.env.NOCACHE_J1_QUERIES) { 173 | return require(cacheFile); 174 | } 175 | 176 | const modules = await j1Client.queryV1('Find CodeModule as m return m.id as name, m._id as id'); 177 | const map = {}; 178 | modules.forEach(module => { 179 | map[module.name] = module.id; 180 | }); 181 | 182 | if (shouldCache) { 183 | fs.writeFileSync(cacheFile, JSON.stringify(map), 'utf8'); 184 | } 185 | return map; 186 | } 187 | 188 | function die(msg, rc=1): void { 189 | console.error(msg); 190 | process.exit(rc); 191 | } 192 | 193 | function getSBOMComponents(pathToSBOM): SBOMComponent[] { 194 | let sbomData: SBOM; 195 | try { 196 | sbomData = JSON.parse(fs.readFileSync(pathToSBOM, {encoding: 'utf8'})); 197 | } catch (err) { 198 | console.warn(`Couldn't parse ${pathToSBOM} as JSON: ${err}`); 199 | sbomData = { bomFormat: undefined } as unknown as SBOM; 200 | } 201 | if (! (sbomData.bomFormat === 'CycloneDX')) { 202 | die(`${pathToSBOM} does not appear to be a valid CycloneDX file, aborting!`); 203 | } 204 | const { components } = sbomData; 205 | return Array.isArray(components) ? components : []; 206 | } 207 | const { sbom, repo, devDeps, directDeps, directOnly } = minimist(process.argv.slice(2)); 208 | 209 | if (directOnly && !directDeps) { 210 | usage(); 211 | die('You must provide one or more direct dependencies via JSON if you specify --directOnly!'); 212 | } 213 | 214 | const devDependencies = devDeps ? JSON.parse(fs.readFileSync(devDeps, {encoding: 'utf8'})) : []; 215 | const directDependencies = directDeps ? JSON.parse(fs.readFileSync(directDeps, {encoding: 'utf8'})) : []; 216 | 217 | if (!sbom || !fs.existsSync(sbom)) { 218 | usage(); 219 | die('You must provide a valid path to a CycloneDX bom.json file!', 2); 220 | } 221 | 222 | if (!repo) { 223 | usage(); 224 | die('You must provide a repo name parameter specifying the CodeRepo to ingest the SBOM for!', 2); 225 | } 226 | 227 | if (!process.env.J1_API_TOKEN) { 228 | die('Missing J1_API_TOKEN ENV Var!'); 229 | } 230 | 231 | if (!process.env.J1_ACCOUNT) { 232 | die('Missing J1_ACCOUNT ENV Var!'); 233 | } 234 | 235 | const attemptOptions = { 236 | delay: 20000, 237 | factor: 1.5, 238 | maxAttempts: 0, 239 | maxDelay: 70, 240 | beforeAttempt (): void { 241 | process.stdout.write('.'); 242 | } 243 | }; 244 | 245 | run(sbom, repo, devDependencies, directDependencies).catch(console.error); 246 | -------------------------------------------------------------------------------- /github-codeowners/index.js: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | import { throttling } from '@octokit/plugin-throttling'; 3 | import git from 'isomorphic-git' 4 | import http from 'isomorphic-git/http/node/index.cjs'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import 'dotenv/config'; 8 | 9 | const { version } = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 10 | const USERAGENT = `J1 CODEOWNERS Automation v${version}`; 11 | const PRBRANCH = USERAGENT.toLowerCase().replace(/ /g, '-'); 12 | const PRTITLE = process.env.PRTITLE || 'Add CODEOWNERS'; 13 | const ERRLOG = process.env.ERRLOG || 'error.log'; 14 | const ORG = process.env.ORG || 'jupiterone'; 15 | const OWNER = process.env.OWNER || 'jupiterone'; 16 | const DEFAULT_TEAM = process.env.DEFAULT_TEAM || 'engineering'; 17 | const RUNMODE = process.env.RUNMODE || 'open_pulls'; 18 | const ADOPTEDREPOS = process.env.ADOPTEDREPOS; 19 | const DEBUG = process.env.DEBUG || false; // debug mode 20 | const SILENT = process.env.SILENT || false; // no logs 21 | 22 | async function main() { 23 | log(`RUNMODE is: ${RUNMODE}`); 24 | const adoptedRepoTeamsLookup = await getOptionalAdoptedRepoLookup(); 25 | const filteredGHTeamsLookup = await generateTeamMembershipLookup(); 26 | const repos = await getOrgRepos(); 27 | const errlog = fs.createWriteStream(ERRLOG, {flags: 'a'}); 28 | 29 | for (const repo of repos) { 30 | try { 31 | switch (RUNMODE) { 32 | case 'open_pulls': 33 | await processRepoPROpen(repo, filteredGHTeamsLookup, adoptedRepoTeamsLookup); 34 | break; 35 | case 'merge_pulls': 36 | await processRepoPRMerge(repo); 37 | break; 38 | default: 39 | log(`Unknown RUNMODE: '${RUNMODE}'`, 'error'); 40 | process.exit(2); 41 | } 42 | } catch (err) { 43 | const errMsg = `Error processing ${repo.name}: ${JSON.stringify(err, null, 2)}`; 44 | log(errMsg, 'warn'); 45 | errlog.write(errMsg); 46 | } 47 | } 48 | } 49 | 50 | async function processRepoPROpen(repo, filteredTeamsLookup, adoptedRepoTeamsLookup) { 51 | if (repo.archived) { 52 | log(`SKIPPING repo ${repo.name}, since it is archived.`, 'warn'); 53 | return; 54 | } 55 | 56 | if(await doesCODEOWNERSExist(repo.name)) { 57 | log(`SKIPPING repo ${repo.name} due to existing CODEOWNERS file.`, 'warn'); 58 | return; 59 | } 60 | 61 | if (await getOpenCodeownersPR(repo)) { 62 | log(`SKIPPING repo ${repo.name} due to open CODEOWNERS Pull Request.`, 'warn'); 63 | return; 64 | } 65 | 66 | // discover teams to add to CODEOWNERS 67 | const { ownerTeams, commitMessage } = await generateTeamOwnersForRepo(repo.name, filteredTeamsLookup, adoptedRepoTeamsLookup); 68 | 69 | for (const team of ownerTeams) { 70 | await addTeamToRepo(team, repo.name); // ensure team has push-or-higher access (so CODEOWNERS validates) 71 | } 72 | const branch = await createCODEOWNERSBranch(ownerTeams, repo.name); 73 | await createPullRequest(branch, repo, commitMessage); 74 | } 75 | 76 | async function processRepoPRMerge(repo) { 77 | log(`Checking pr status for repo: ${repo.name}...`); 78 | const pr = await getOpenCodeownersPR(repo); 79 | if (!pr) { 80 | return; // no mergeable PRs found 81 | } 82 | log(`Found mergeable PR: ${pr.html_url}`); 83 | 84 | log(`Retrieving branch protection rules for ${repo.name}:${repo.default_branch}...`); 85 | const protection = await getDefaultBranchProtection(repo); 86 | 87 | if (protection) { 88 | log(`Removing branch protection rules for ${repo.name}:${repo.default_branch}...`); 89 | await deleteBranchProtection(repo); 90 | } 91 | 92 | log(`Merging pr#${pr.number} for ${repo.name}...`); 93 | await mergeRepoPR(repo, pr); 94 | 95 | if (protection) { 96 | log(`Restoring branch protection rules for ${repo.name}:${repo.default_branch}...`); 97 | await restoreBranchProtection(repo, protection); 98 | } 99 | 100 | return; 101 | } 102 | 103 | async function doesCODEOWNERSExist(repo, owner=OWNER) { 104 | log(`Checking if repo ${repo} already has a CODEOWNERS file...`); 105 | // per https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location 106 | if (await contentExists(repo, 'CODEOWNERS')) return true; 107 | if (await contentExists(repo, 'docs/CODEOWNERS')) return true; 108 | if (await contentExists(repo, '.github/CODEOWNERS')) return true; 109 | return false; 110 | } 111 | 112 | async function contentExists(repo, path, owner=OWNER) { 113 | let content; 114 | try { 115 | content = await octokit.repos.getContent({owner, repo, path}); 116 | } catch (e) { 117 | if (e.status) { // throw on 404, thanks for that. 118 | content=e; 119 | } else { 120 | throw new Error(e); 121 | } 122 | } 123 | return content.status === 200; 124 | } 125 | 126 | async function generateTeamOwnersForRepo(repo, teamsLookup, adoptedRepoTeamsLookup) { 127 | log(`Generating team ownership for repo: ${repo}`); 128 | const adoptedRepoTeams = adoptedRepoTeamsLookup[repo]; 129 | const owners = new Set(); 130 | let commitMessage; 131 | 132 | if (adoptedRepoTeams) { 133 | log(`Found manually adopted teams for ${repo}...`); 134 | for (const team of adoptedRepoTeams) { 135 | owners.add(team); 136 | } 137 | commitMessage = `Automatically generated by ${USERAGENT}\n\n` + 138 | 'Using manually adopted teams: ' + adoptedRepoTeams.join(', '); 139 | } else { 140 | log(`Grabbing commits and authors for repo: ${repo}...`); 141 | const commitAuthors = await getTopFrecentAuthorLogins(repo, 4, 60); 142 | log(`Top commit authors for repo ${repo} were: ${commitAuthors.join(', ')}...`, 'debug'); 143 | 144 | for (const author of commitAuthors) { 145 | const authorTeams = teamsLookup[author]; 146 | if (!authorTeams) { continue; } // skip committers not currently part of org 147 | for (const team of authorTeams) { 148 | owners.add(team); 149 | } 150 | } 151 | if (!owners.size) { 152 | log(`WARN: no unique teams found among active commit authors, defaulting to "${DEFAULT_TEAM}"... `, 'warn'); 153 | owners.add(DEFAULT_TEAM); 154 | } 155 | 156 | commitMessage = `Automatically generated by ${USERAGENT}\n\n` + 157 | 'Based on team membership for identified commit authors: ' + 158 | commitAuthors.join(', ') 159 | } 160 | const ownerTeams = Array.from(owners); 161 | 162 | log(`Team OWNERS for ${repo}: ${ownerTeams.join(', ')}...`); 163 | return { ownerTeams, commitMessage }; 164 | } 165 | 166 | async function getTopFrecentAuthorLogins(repo, numAuthors=5, numCommits=100, owner=OWNER) { 167 | try { 168 | const commits = await getRecentCommits(repo, numCommits, owner); 169 | const len = commits.length; 170 | const authorsScores = {}; 171 | for (const i in commits) { 172 | const weight = len - i; // weight bias toward most recent commits 173 | const currentAuthor = commits[i].author; 174 | if (! currentAuthor?.login || currentAuthor?.login.indexOf('[bot]') !== -1) { continue; } // skip commits lacking clear human authorship 175 | if (!authorsScores[currentAuthor.login]) { 176 | authorsScores[currentAuthor.login] = weight; 177 | } else { 178 | authorsScores[currentAuthor.login] += weight; 179 | } 180 | } 181 | const sortedArrayOfWeightedLogins = Object.entries(authorsScores).sort((a,b) => { return b[1] - a[1]; }); // [ ['user1', 100], ['user2', 92],...] 182 | const topAuthorsSliceOfWeightedLogins = sortedArrayOfWeightedLogins.slice(0, numAuthors); 183 | return topAuthorsSliceOfWeightedLogins.map(entry => entry[0]); // [ 'user1', 'user2' ] 184 | } catch (err) { 185 | log({repo, err}, 'warn'); 186 | return []; 187 | } 188 | } 189 | 190 | async function getRecentCommits(repo, max=100, owner=OWNER) { 191 | log(`Retrieving last ${max} commits for repo ${repo}...`); 192 | return octokit.paginate( 193 | octokit.repos.listCommits, 194 | {owner, repo, per_page: max}, 195 | response => response.data); 196 | } 197 | 198 | async function addTeamToRepo(team_slug, repo, permission='push', org=ORG, owner=OWNER) { 199 | log(`Adding team ${team_slug} to repo ${repo}...`); 200 | await waitForSecondaryRateLimitWindow(); 201 | const updatePromise = octokit.teams.addOrUpdateRepoPermissionsInOrg({ 202 | org, 203 | team_slug, 204 | owner, 205 | repo, 206 | permission 207 | }); 208 | addRateLimitingEvent(); 209 | return updatePromise; 210 | } 211 | 212 | async function createCODEOWNERSBranch(owners, repo, org=ORG, branch=PRBRANCH) { 213 | if (fs.existsSync(repo)) { 214 | // ensure we have a clean slate for this work... 215 | log(`Found dir ${repo} from previous run. cleaning up...`); 216 | await cleanupLocalDir(repo); 217 | } 218 | 219 | log(`Creating branch ${branch} for ${repo}...`); 220 | const dir = `./${repo}`; 221 | await cloneRepo(repo, dir); 222 | await checkoutBranch(dir, branch); 223 | 224 | const codeownersStr = '* ' + owners.map(team => `@${org}/${team.trim()}`).join(' '); 225 | 226 | log(`Creating CODEOWNERS file for ${repo}/${branch}...`); 227 | fs.writeFileSync(path.join(dir, 'CODEOWNERS'), codeownersStr, 'ascii'); 228 | await git.add({ fs, dir, filepath: 'CODEOWNERS' }); 229 | await git.commit({ 230 | fs, 231 | dir, 232 | author: { 233 | name: USERAGENT, 234 | email: 'security@jupiterone.com' 235 | }, 236 | message: 'Add CODEOWNERS' 237 | }); 238 | 239 | log(`Pushing branch ${branch} to remote...`); 240 | await waitForSecondaryRateLimitWindow(); 241 | await git.push({ 242 | fs, 243 | http, 244 | dir, 245 | remote: 'origin', 246 | force: true, 247 | onAuth: isometricGitOnAuthGitHubFn 248 | }); 249 | 250 | addRateLimitingEvent(); 251 | 252 | await cleanupLocalDir(repo); 253 | return branch; 254 | } 255 | 256 | async function cloneRepo(repo, dir=`./${repo}`, org=ORG) { 257 | await git.clone({ 258 | fs, 259 | http, 260 | dir, 261 | url: `https://github.com/${org}/${repo}`, 262 | onAuth: isometricGitOnAuthGitHubFn, 263 | singleBranch: true, 264 | depth: 2 265 | }); 266 | } 267 | 268 | async function checkoutBranch(dir, ref) { 269 | await git.branch({ 270 | fs, 271 | dir, 272 | ref, 273 | checkout: true 274 | }); 275 | } 276 | 277 | async function createPullRequest(head, repo, commitMessage, owner=OWNER) { 278 | log(`Creating pull request for ${repo.name} from ${head} -> ${repo.default_branch}...`); 279 | const events = 1; 280 | const perSeconds = 90; 281 | await waitForSecondaryRateLimitWindow(events, perSeconds); 282 | const prPromise = octokit.pulls.create({ 283 | owner, 284 | head, 285 | repo: repo.name, 286 | base: repo.default_branch, 287 | title: PRTITLE, 288 | body: commitMessage 289 | }); 290 | addRateLimitingEvent(); 291 | return prPromise; 292 | } 293 | 294 | async function cleanupLocalDir(repo) { 295 | 296 | if (repo.indexOf('.') !== -1) { 297 | log(`Directory '${repo}' appears to contain '.' chars, cowardly refusing to attempt a recursive cleanup...`, 'warn'); 298 | return; 299 | } 300 | fs.rmdirSync(repo, { recursive: true }); 301 | log(`Directory '${repo}' has been deleted!`); 302 | } 303 | 304 | const isometricGitOnAuthGitHubFn = () => { return { username: process.env.GITHUB_AUTH_TOKEN, password: '' }; }; 305 | 306 | const rateLimitingEventLog = []; 307 | 308 | function addRateLimitingEvent() { 309 | rateLimitingEventLog.push(Date.now()); 310 | } 311 | 312 | // intentionally slow down to avoid triggering undocumented secondary rate limits 313 | // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits 314 | async function waitForSecondaryRateLimitWindow(events=2, seconds=1, sleepMillis=300) { 315 | let windowEvents, isWaiting; 316 | do { 317 | windowEvents = rateLimitingEventLog.filter(e => e >= Date.now() - (seconds * 1000)); 318 | if (windowEvents.length > events) { 319 | if(!isWaiting) { 320 | log(`Secondary rate-limit guard exceeded. (${windowEvents.length} events/${seconds}secs) Waiting for window to clear...`, 'warn'); 321 | isWaiting=true; 322 | } 323 | await new Promise(resolve => { setTimeout(resolve, sleepMillis); }); 324 | } 325 | } while (windowEvents.length > events) 326 | } 327 | 328 | function log(msg, level='log') { 329 | if (level==='debug' && !DEBUG) { 330 | return; 331 | } 332 | if (! SILENT) { 333 | console[level](msg); 334 | } 335 | } 336 | 337 | async function getOrgRepos(org=ORG) { 338 | log('Discovering repos... '); 339 | return octokit.paginate( 340 | octokit.repos.listForOrg, 341 | { org, per_page: 100 }, 342 | response => response.data); 343 | } 344 | 345 | // returns { 346 | // user1: [ 'teamslug1', 'teamslug2' ], 347 | // user2: [ 'teamslug2' ], 348 | // user3: [] 349 | // } 350 | async function generateTeamMembershipLookup(org=ORG) { 351 | log('Discovering teams and memberships... '); 352 | const teams = (await octokit.teams.list({ 353 | org 354 | })).data; 355 | const memberLookup = {}; 356 | for (const team of teams) { 357 | const members = (await octokit.teams.listMembersInOrg({org, team_slug: team.slug, per_page: 100 })).data; 358 | for (const member of members) { 359 | memberLookup[member.login] ? memberLookup[member.login].push(team.slug) : memberLookup[member.login] = [ team.slug ]; 360 | } 361 | } 362 | return filterTeamMemberships(memberLookup); 363 | } 364 | 365 | // exclude members' teams that are not good candidates for ownership 366 | function filterTeamMemberships(memberLookup) { 367 | const filtered = {}; 368 | Object.keys(memberLookup).forEach(member => { 369 | const filteredArray = memberLookup[member].filter(team => { 370 | if (team.indexOf('admin') !== -1) return false; 371 | if (team.indexOf('contract') !== -1) return false; 372 | if (team === 'everyone') return false; 373 | if (team === DEFAULT_TEAM) return false; 374 | return true; 375 | }); 376 | filtered[member] = filteredArray; 377 | }); 378 | 379 | return filtered; 380 | } 381 | 382 | if (! process.env.GITHUB_AUTH_TOKEN) { 383 | log('You must export a personal auth token as GITHUB_AUTH_TOKEN!', 'warn'); 384 | process.exit(2); 385 | } 386 | 387 | const MyOctokit = Octokit.plugin(throttling); 388 | 389 | const octokit = new MyOctokit({ 390 | auth: process.env.GITHUB_AUTH_TOKEN, 391 | userAgent: USERAGENT, 392 | // log: console 393 | throttle: { 394 | onRateLimit: (retryAfter, options, octokit) => { 395 | octokit.log.warn( 396 | `Request quota exhausted for request ${options.method} ${options.url}` 397 | ); 398 | 399 | if (options.request.retryCount === 0) { 400 | // only retries once 401 | octokit.log.info(`Retrying after ${retryAfter} seconds!`); 402 | return true; 403 | } 404 | }, 405 | onSecondaryRateLimit: (retryAfter, options, octokit) => { 406 | // does not retry, only logs a warning 407 | octokit.log.warn( 408 | `SecondaryRateLimit detected for request ${options.method} ${options.url}` 409 | ); 410 | }, 411 | }, 412 | }); 413 | 414 | // Monkey-see, Monkey-patch... 415 | // https://stackoverflow.com/a/18391400 416 | // allows thrown Errors to be JSON.stringified 417 | if (!('toJSON' in Error.prototype)) 418 | Object.defineProperty(Error.prototype, 'toJSON', { 419 | value: function () { 420 | var alt = {}; 421 | Object.getOwnPropertyNames(this).forEach(function (key) { 422 | alt[key] = this[key]; 423 | }, this); 424 | return alt; 425 | }, 426 | configurable: true, 427 | writable: true 428 | }); 429 | 430 | async function getOptionalAdoptedRepoLookup() { 431 | const adoptedRepoLookup = {}; 432 | if (ADOPTEDREPOS) { 433 | if (fs.existsSync(ADOPTEDREPOS)) { 434 | try { 435 | const adoptions = JSON.parse(fs.readFileSync(ADOPTEDREPOS, 'utf8')); 436 | for (const adoption of adoptions) { 437 | if (!('repo' in adoption) || !('team' in adoption)) { 438 | throw new Error('Invalid Format, expected [{repo: "", team: ""}]'); 439 | } 440 | if (!adoption.repo || !adoption.team) { 441 | continue; 442 | } 443 | adoptedRepoLookup[adoption.repo] = adoption.team.split(' '); 444 | } 445 | } catch(err) { 446 | log(`Couldn't parse JSON file: ${ADOPTEDREPOS}: ${err}`, 'error'); 447 | process.exit(2); 448 | } 449 | } else { 450 | log(`Couldn't find JSON file: ${ADOPTEDREPOS}!`, 'error'); 451 | process.exit(2); 452 | } 453 | } 454 | return adoptedRepoLookup; 455 | } 456 | 457 | function getBypassAllowancePayload(protection) { 458 | const bypassAllowancePayload = { 459 | users: (protection.required_pull_request_reviews?.bypass_pull_request_allowances?.users || []).map(u=>u.slug), 460 | teams: (protection.required_pull_request_reviews?.bypass_pull_request_allowances?.teams || []).map(t=>t.slug), 461 | }; 462 | // payload only makes sense when non-empty. 463 | const shouldBypass = Object.keys(bypassAllowancePayload).reduce( 464 | (acc, curKey) => acc || bypassAllowancePayload[curKey].length > 0, false); 465 | return { shouldBypass, bypassAllowancePayload }; 466 | } 467 | 468 | function getRestrictionsPayload(protection) { 469 | const restrictionsPayload = { 470 | users: (protection.restrictions?.users || []).map(u=>u.slug), 471 | teams: (protection.restrictions?.teams || []).map(t=>t.slug), 472 | apps: (protection.restrictions?.apps || []).map(a=>a.slug), 473 | }; 474 | // payload only makes sense when non-empty. 475 | const shouldRestrict = Object.keys(restrictionsPayload).reduce( 476 | (acc, curKey) => acc || restrictionsPayload[curKey].length > 0, false); 477 | return { shouldRestrict, restrictionsPayload }; 478 | } 479 | 480 | function getRequiredStatusChecksPayload(protection) { 481 | if (!protection.required_status_checks?.checks) { 482 | return null; // required field, set to disable 483 | } 484 | return { 485 | strict: !!protection.required_status_checks?.strict, 486 | checks: protection.required_status_checks.checks 487 | }; 488 | } 489 | 490 | async function getOpenCodeownersPR(repo) { 491 | const { data: prs } = await octokit.pulls.list({ 492 | owner: OWNER, 493 | repo: repo.name, 494 | head: `${ORG}:${PRBRANCH}`, 495 | base: repo.default_branch, 496 | state: 'open' 497 | }); 498 | return prs[0]; 499 | } 500 | 501 | async function deleteBranchProtection(repo) { 502 | await waitForSecondaryRateLimitWindow(); 503 | const deleteRes = await octokit.repos.deleteBranchProtection({ 504 | owner: OWNER, 505 | repo: repo.name, 506 | branch: repo.default_branch 507 | }); 508 | addRateLimitingEvent(); 509 | if (DEBUG) log(JSON.stringify(deleteRes, null, 2), 'debug'); 510 | return deleteRes; 511 | } 512 | 513 | async function restoreBranchProtection(repo, protection, forceCodeownersReview=true) { 514 | const { shouldBypass, bypassAllowancePayload } = getBypassAllowancePayload(protection); 515 | const { shouldRestrict, restrictionsPayload } = getRestrictionsPayload(protection); 516 | const codeOwnerReviews = forceCodeownersReview || !!protection.required_pull_request_reviews?.require_code_owner_reviews; 517 | 518 | const updateBranchProtectionPayload = { 519 | owner: OWNER, 520 | repo: repo.name, 521 | branch: repo.default_branch, 522 | required_pull_request_reviews: { 523 | dismiss_stale_reviews: protection.required_pull_request_reviews?.dismiss_stale_reviews, 524 | required_approving_review_count: protection.required_pull_request_reviews?.required_approving_review_count || 1, 525 | require_code_owner_reviews: codeOwnerReviews 526 | }, 527 | enforce_admins: protection.enforce_admins?.enabled, 528 | required_status_checks: getRequiredStatusChecksPayload(protection), 529 | required_signatures: protection.required_signatures?.enabled, 530 | required_linear_history: protection.required_linear_history?.enabled, 531 | allow_force_pushes: protection.allow_force_pushes?.enabled, 532 | allow_deletions: protection.allow_deletions?.enabled, 533 | required_conversation_resolution: protection.required_conversation_resolution?.enabled, 534 | }; 535 | 536 | if (shouldBypass) { 537 | updateBranchProtectionPayload.required_pull_request_reviews.bypass_pull_request_allowances = bypassAllowancePayload; 538 | } 539 | updateBranchProtectionPayload.restrictions = shouldRestrict ? restrictionsPayload : null; // required field 540 | 541 | if (DEBUG) log(JSON.stringify(updateBranchProtectionPayload, null, 2), 'debug'); 542 | 543 | await waitForSecondaryRateLimitWindow(); 544 | const updateRes = await octokit.repos.updateBranchProtection(updateBranchProtectionPayload); 545 | addRateLimitingEvent(); 546 | if (DEBUG) log(JSON.stringify(updateRes, null, 2), 'debug'); 547 | return updateRes; 548 | } 549 | 550 | async function mergeRepoPR(repo, pr) { 551 | await waitForSecondaryRateLimitWindow(1, 90); // try to avoid triggering secondary rate-limiting 552 | const mergeRes = await octokit.pulls.merge({ 553 | owner: OWNER, 554 | repo: repo.name, 555 | pull_number: pr.number, 556 | commit_title: `Merged via ${PRBRANCH}` 557 | }); 558 | addRateLimitingEvent(); 559 | if (DEBUG) log(JSON.stringify(mergeRes, null, 2), 'debug'); 560 | return mergeRes; 561 | } 562 | 563 | async function getDefaultBranchProtection(repo) { 564 | let protection; 565 | try { 566 | const { data } = await octokit.repos.getBranchProtection({ 567 | owner: OWNER, 568 | repo: repo.name, 569 | branch: repo.default_branch, 570 | }); 571 | protection = data; 572 | } catch (err) { 573 | if (err?.status === 404) { 574 | return undefined; 575 | } 576 | throw new Error(err); 577 | } 578 | if (DEBUG) { 579 | const logFile = `${repo.name}-branch-protection-config_${new Date().toISOString().replace(/[-:]+/g,'').split('.')[0]}.json`; 580 | log(`Preserving branch protection configuration to ${logFile}...`, 'debug'); 581 | fs.writeFileSync(logFile, JSON.stringify(protection, null, 2), { encoding: 'utf8'}); 582 | } 583 | return protection; 584 | } 585 | 586 | main().then(console.log).catch(console.error); 587 | -------------------------------------------------------------------------------- /yarn2npm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn2npm", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "yarn2npm", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@actions/github": "^5.1.1", 12 | "isomorphic-git": "^1.21.0" 13 | } 14 | }, 15 | "node_modules/@actions/github": { 16 | "version": "5.1.1", 17 | "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz", 18 | "integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==", 19 | "dependencies": { 20 | "@actions/http-client": "^2.0.1", 21 | "@octokit/core": "^3.6.0", 22 | "@octokit/plugin-paginate-rest": "^2.17.0", 23 | "@octokit/plugin-rest-endpoint-methods": "^5.13.0" 24 | } 25 | }, 26 | "node_modules/@actions/http-client": { 27 | "version": "2.0.1", 28 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", 29 | "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", 30 | "dependencies": { 31 | "tunnel": "^0.0.6" 32 | } 33 | }, 34 | "node_modules/@octokit/auth-token": { 35 | "version": "2.5.0", 36 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", 37 | "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", 38 | "dependencies": { 39 | "@octokit/types": "^6.0.3" 40 | } 41 | }, 42 | "node_modules/@octokit/core": { 43 | "version": "3.6.0", 44 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", 45 | "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", 46 | "dependencies": { 47 | "@octokit/auth-token": "^2.4.4", 48 | "@octokit/graphql": "^4.5.8", 49 | "@octokit/request": "^5.6.3", 50 | "@octokit/request-error": "^2.0.5", 51 | "@octokit/types": "^6.0.3", 52 | "before-after-hook": "^2.2.0", 53 | "universal-user-agent": "^6.0.0" 54 | } 55 | }, 56 | "node_modules/@octokit/endpoint": { 57 | "version": "6.0.12", 58 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 59 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 60 | "dependencies": { 61 | "@octokit/types": "^6.0.3", 62 | "is-plain-object": "^5.0.0", 63 | "universal-user-agent": "^6.0.0" 64 | } 65 | }, 66 | "node_modules/@octokit/graphql": { 67 | "version": "4.8.0", 68 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", 69 | "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", 70 | "dependencies": { 71 | "@octokit/request": "^5.6.0", 72 | "@octokit/types": "^6.0.3", 73 | "universal-user-agent": "^6.0.0" 74 | } 75 | }, 76 | "node_modules/@octokit/openapi-types": { 77 | "version": "12.11.0", 78 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", 79 | "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" 80 | }, 81 | "node_modules/@octokit/plugin-paginate-rest": { 82 | "version": "2.21.3", 83 | "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", 84 | "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", 85 | "dependencies": { 86 | "@octokit/types": "^6.40.0" 87 | }, 88 | "peerDependencies": { 89 | "@octokit/core": ">=2" 90 | } 91 | }, 92 | "node_modules/@octokit/plugin-rest-endpoint-methods": { 93 | "version": "5.16.2", 94 | "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", 95 | "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", 96 | "dependencies": { 97 | "@octokit/types": "^6.39.0", 98 | "deprecation": "^2.3.1" 99 | }, 100 | "peerDependencies": { 101 | "@octokit/core": ">=3" 102 | } 103 | }, 104 | "node_modules/@octokit/request": { 105 | "version": "5.6.3", 106 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", 107 | "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", 108 | "dependencies": { 109 | "@octokit/endpoint": "^6.0.1", 110 | "@octokit/request-error": "^2.1.0", 111 | "@octokit/types": "^6.16.1", 112 | "is-plain-object": "^5.0.0", 113 | "node-fetch": "^2.6.7", 114 | "universal-user-agent": "^6.0.0" 115 | } 116 | }, 117 | "node_modules/@octokit/request-error": { 118 | "version": "2.1.0", 119 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 120 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 121 | "dependencies": { 122 | "@octokit/types": "^6.0.3", 123 | "deprecation": "^2.0.0", 124 | "once": "^1.4.0" 125 | } 126 | }, 127 | "node_modules/@octokit/types": { 128 | "version": "6.41.0", 129 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", 130 | "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", 131 | "dependencies": { 132 | "@octokit/openapi-types": "^12.11.0" 133 | } 134 | }, 135 | "node_modules/async-lock": { 136 | "version": "1.4.0", 137 | "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", 138 | "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" 139 | }, 140 | "node_modules/before-after-hook": { 141 | "version": "2.2.3", 142 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", 143 | "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" 144 | }, 145 | "node_modules/clean-git-ref": { 146 | "version": "2.0.1", 147 | "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", 148 | "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" 149 | }, 150 | "node_modules/crc-32": { 151 | "version": "1.2.2", 152 | "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", 153 | "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", 154 | "bin": { 155 | "crc32": "bin/crc32.njs" 156 | }, 157 | "engines": { 158 | "node": ">=0.8" 159 | } 160 | }, 161 | "node_modules/decompress-response": { 162 | "version": "6.0.0", 163 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 164 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 165 | "dependencies": { 166 | "mimic-response": "^3.1.0" 167 | }, 168 | "engines": { 169 | "node": ">=10" 170 | }, 171 | "funding": { 172 | "url": "https://github.com/sponsors/sindresorhus" 173 | } 174 | }, 175 | "node_modules/deprecation": { 176 | "version": "2.3.1", 177 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 178 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 179 | }, 180 | "node_modules/diff3": { 181 | "version": "0.0.3", 182 | "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", 183 | "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" 184 | }, 185 | "node_modules/ignore": { 186 | "version": "5.2.4", 187 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", 188 | "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", 189 | "engines": { 190 | "node": ">= 4" 191 | } 192 | }, 193 | "node_modules/inherits": { 194 | "version": "2.0.4", 195 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 196 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 197 | }, 198 | "node_modules/is-plain-object": { 199 | "version": "5.0.0", 200 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 201 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", 202 | "engines": { 203 | "node": ">=0.10.0" 204 | } 205 | }, 206 | "node_modules/isomorphic-git": { 207 | "version": "1.21.0", 208 | "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.21.0.tgz", 209 | "integrity": "sha512-ZqCAUM63CYepA3fB8H7NVyPSiOkgzIbQ7T+QPrm9xtYgQypN9JUJ5uLMjB5iTfomdJf3mdm6aSxjZwnT6ubvEA==", 210 | "dependencies": { 211 | "async-lock": "^1.1.0", 212 | "clean-git-ref": "^2.0.1", 213 | "crc-32": "^1.2.0", 214 | "diff3": "0.0.3", 215 | "ignore": "^5.1.4", 216 | "minimisted": "^2.0.0", 217 | "pako": "^1.0.10", 218 | "pify": "^4.0.1", 219 | "readable-stream": "^3.4.0", 220 | "sha.js": "^2.4.9", 221 | "simple-get": "^4.0.1" 222 | }, 223 | "bin": { 224 | "isogit": "cli.cjs" 225 | }, 226 | "engines": { 227 | "node": ">=12" 228 | } 229 | }, 230 | "node_modules/mimic-response": { 231 | "version": "3.1.0", 232 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 233 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 234 | "engines": { 235 | "node": ">=10" 236 | }, 237 | "funding": { 238 | "url": "https://github.com/sponsors/sindresorhus" 239 | } 240 | }, 241 | "node_modules/minimist": { 242 | "version": "1.2.8", 243 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 244 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 245 | "funding": { 246 | "url": "https://github.com/sponsors/ljharb" 247 | } 248 | }, 249 | "node_modules/minimisted": { 250 | "version": "2.0.1", 251 | "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", 252 | "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", 253 | "dependencies": { 254 | "minimist": "^1.2.5" 255 | } 256 | }, 257 | "node_modules/node-fetch": { 258 | "version": "2.6.9", 259 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", 260 | "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", 261 | "dependencies": { 262 | "whatwg-url": "^5.0.0" 263 | }, 264 | "engines": { 265 | "node": "4.x || >=6.0.0" 266 | }, 267 | "peerDependencies": { 268 | "encoding": "^0.1.0" 269 | }, 270 | "peerDependenciesMeta": { 271 | "encoding": { 272 | "optional": true 273 | } 274 | } 275 | }, 276 | "node_modules/once": { 277 | "version": "1.4.0", 278 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 279 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 280 | "dependencies": { 281 | "wrappy": "1" 282 | } 283 | }, 284 | "node_modules/pako": { 285 | "version": "1.0.11", 286 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 287 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 288 | }, 289 | "node_modules/pify": { 290 | "version": "4.0.1", 291 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 292 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", 293 | "engines": { 294 | "node": ">=6" 295 | } 296 | }, 297 | "node_modules/readable-stream": { 298 | "version": "3.6.0", 299 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 300 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 301 | "dependencies": { 302 | "inherits": "^2.0.3", 303 | "string_decoder": "^1.1.1", 304 | "util-deprecate": "^1.0.1" 305 | }, 306 | "engines": { 307 | "node": ">= 6" 308 | } 309 | }, 310 | "node_modules/safe-buffer": { 311 | "version": "5.2.1", 312 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 313 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 314 | "funding": [ 315 | { 316 | "type": "github", 317 | "url": "https://github.com/sponsors/feross" 318 | }, 319 | { 320 | "type": "patreon", 321 | "url": "https://www.patreon.com/feross" 322 | }, 323 | { 324 | "type": "consulting", 325 | "url": "https://feross.org/support" 326 | } 327 | ] 328 | }, 329 | "node_modules/sha.js": { 330 | "version": "2.4.11", 331 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 332 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 333 | "dependencies": { 334 | "inherits": "^2.0.1", 335 | "safe-buffer": "^5.0.1" 336 | }, 337 | "bin": { 338 | "sha.js": "bin.js" 339 | } 340 | }, 341 | "node_modules/simple-concat": { 342 | "version": "1.0.1", 343 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 344 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 345 | "funding": [ 346 | { 347 | "type": "github", 348 | "url": "https://github.com/sponsors/feross" 349 | }, 350 | { 351 | "type": "patreon", 352 | "url": "https://www.patreon.com/feross" 353 | }, 354 | { 355 | "type": "consulting", 356 | "url": "https://feross.org/support" 357 | } 358 | ] 359 | }, 360 | "node_modules/simple-get": { 361 | "version": "4.0.1", 362 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 363 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 364 | "funding": [ 365 | { 366 | "type": "github", 367 | "url": "https://github.com/sponsors/feross" 368 | }, 369 | { 370 | "type": "patreon", 371 | "url": "https://www.patreon.com/feross" 372 | }, 373 | { 374 | "type": "consulting", 375 | "url": "https://feross.org/support" 376 | } 377 | ], 378 | "dependencies": { 379 | "decompress-response": "^6.0.0", 380 | "once": "^1.3.1", 381 | "simple-concat": "^1.0.0" 382 | } 383 | }, 384 | "node_modules/string_decoder": { 385 | "version": "1.3.0", 386 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 387 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 388 | "dependencies": { 389 | "safe-buffer": "~5.2.0" 390 | } 391 | }, 392 | "node_modules/tr46": { 393 | "version": "0.0.3", 394 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 395 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 396 | }, 397 | "node_modules/tunnel": { 398 | "version": "0.0.6", 399 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 400 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 401 | "engines": { 402 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3" 403 | } 404 | }, 405 | "node_modules/universal-user-agent": { 406 | "version": "6.0.0", 407 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 408 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 409 | }, 410 | "node_modules/util-deprecate": { 411 | "version": "1.0.2", 412 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 413 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 414 | }, 415 | "node_modules/webidl-conversions": { 416 | "version": "3.0.1", 417 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 418 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 419 | }, 420 | "node_modules/whatwg-url": { 421 | "version": "5.0.0", 422 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 423 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 424 | "dependencies": { 425 | "tr46": "~0.0.3", 426 | "webidl-conversions": "^3.0.0" 427 | } 428 | }, 429 | "node_modules/wrappy": { 430 | "version": "1.0.2", 431 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 432 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 433 | } 434 | }, 435 | "dependencies": { 436 | "@actions/github": { 437 | "version": "5.1.1", 438 | "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz", 439 | "integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==", 440 | "requires": { 441 | "@actions/http-client": "^2.0.1", 442 | "@octokit/core": "^3.6.0", 443 | "@octokit/plugin-paginate-rest": "^2.17.0", 444 | "@octokit/plugin-rest-endpoint-methods": "^5.13.0" 445 | } 446 | }, 447 | "@actions/http-client": { 448 | "version": "2.0.1", 449 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", 450 | "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", 451 | "requires": { 452 | "tunnel": "^0.0.6" 453 | } 454 | }, 455 | "@octokit/auth-token": { 456 | "version": "2.5.0", 457 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", 458 | "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", 459 | "requires": { 460 | "@octokit/types": "^6.0.3" 461 | } 462 | }, 463 | "@octokit/core": { 464 | "version": "3.6.0", 465 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", 466 | "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", 467 | "requires": { 468 | "@octokit/auth-token": "^2.4.4", 469 | "@octokit/graphql": "^4.5.8", 470 | "@octokit/request": "^5.6.3", 471 | "@octokit/request-error": "^2.0.5", 472 | "@octokit/types": "^6.0.3", 473 | "before-after-hook": "^2.2.0", 474 | "universal-user-agent": "^6.0.0" 475 | } 476 | }, 477 | "@octokit/endpoint": { 478 | "version": "6.0.12", 479 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 480 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 481 | "requires": { 482 | "@octokit/types": "^6.0.3", 483 | "is-plain-object": "^5.0.0", 484 | "universal-user-agent": "^6.0.0" 485 | } 486 | }, 487 | "@octokit/graphql": { 488 | "version": "4.8.0", 489 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", 490 | "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", 491 | "requires": { 492 | "@octokit/request": "^5.6.0", 493 | "@octokit/types": "^6.0.3", 494 | "universal-user-agent": "^6.0.0" 495 | } 496 | }, 497 | "@octokit/openapi-types": { 498 | "version": "12.11.0", 499 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", 500 | "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" 501 | }, 502 | "@octokit/plugin-paginate-rest": { 503 | "version": "2.21.3", 504 | "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", 505 | "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", 506 | "requires": { 507 | "@octokit/types": "^6.40.0" 508 | } 509 | }, 510 | "@octokit/plugin-rest-endpoint-methods": { 511 | "version": "5.16.2", 512 | "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", 513 | "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", 514 | "requires": { 515 | "@octokit/types": "^6.39.0", 516 | "deprecation": "^2.3.1" 517 | } 518 | }, 519 | "@octokit/request": { 520 | "version": "5.6.3", 521 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", 522 | "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", 523 | "requires": { 524 | "@octokit/endpoint": "^6.0.1", 525 | "@octokit/request-error": "^2.1.0", 526 | "@octokit/types": "^6.16.1", 527 | "is-plain-object": "^5.0.0", 528 | "node-fetch": "^2.6.7", 529 | "universal-user-agent": "^6.0.0" 530 | } 531 | }, 532 | "@octokit/request-error": { 533 | "version": "2.1.0", 534 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 535 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 536 | "requires": { 537 | "@octokit/types": "^6.0.3", 538 | "deprecation": "^2.0.0", 539 | "once": "^1.4.0" 540 | } 541 | }, 542 | "@octokit/types": { 543 | "version": "6.41.0", 544 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", 545 | "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", 546 | "requires": { 547 | "@octokit/openapi-types": "^12.11.0" 548 | } 549 | }, 550 | "async-lock": { 551 | "version": "1.4.0", 552 | "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", 553 | "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" 554 | }, 555 | "before-after-hook": { 556 | "version": "2.2.3", 557 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", 558 | "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" 559 | }, 560 | "clean-git-ref": { 561 | "version": "2.0.1", 562 | "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", 563 | "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" 564 | }, 565 | "crc-32": { 566 | "version": "1.2.2", 567 | "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", 568 | "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" 569 | }, 570 | "decompress-response": { 571 | "version": "6.0.0", 572 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 573 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 574 | "requires": { 575 | "mimic-response": "^3.1.0" 576 | } 577 | }, 578 | "deprecation": { 579 | "version": "2.3.1", 580 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 581 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 582 | }, 583 | "diff3": { 584 | "version": "0.0.3", 585 | "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", 586 | "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" 587 | }, 588 | "ignore": { 589 | "version": "5.2.4", 590 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", 591 | "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" 592 | }, 593 | "inherits": { 594 | "version": "2.0.4", 595 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 596 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 597 | }, 598 | "is-plain-object": { 599 | "version": "5.0.0", 600 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 601 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" 602 | }, 603 | "isomorphic-git": { 604 | "version": "1.21.0", 605 | "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.21.0.tgz", 606 | "integrity": "sha512-ZqCAUM63CYepA3fB8H7NVyPSiOkgzIbQ7T+QPrm9xtYgQypN9JUJ5uLMjB5iTfomdJf3mdm6aSxjZwnT6ubvEA==", 607 | "requires": { 608 | "async-lock": "^1.1.0", 609 | "clean-git-ref": "^2.0.1", 610 | "crc-32": "^1.2.0", 611 | "diff3": "0.0.3", 612 | "ignore": "^5.1.4", 613 | "minimisted": "^2.0.0", 614 | "pako": "^1.0.10", 615 | "pify": "^4.0.1", 616 | "readable-stream": "^3.4.0", 617 | "sha.js": "^2.4.9", 618 | "simple-get": "^4.0.1" 619 | } 620 | }, 621 | "mimic-response": { 622 | "version": "3.1.0", 623 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 624 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" 625 | }, 626 | "minimist": { 627 | "version": "1.2.8", 628 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 629 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" 630 | }, 631 | "minimisted": { 632 | "version": "2.0.1", 633 | "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", 634 | "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", 635 | "requires": { 636 | "minimist": "^1.2.5" 637 | } 638 | }, 639 | "node-fetch": { 640 | "version": "2.6.9", 641 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", 642 | "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", 643 | "requires": { 644 | "whatwg-url": "^5.0.0" 645 | } 646 | }, 647 | "once": { 648 | "version": "1.4.0", 649 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 650 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 651 | "requires": { 652 | "wrappy": "1" 653 | } 654 | }, 655 | "pako": { 656 | "version": "1.0.11", 657 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 658 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 659 | }, 660 | "pify": { 661 | "version": "4.0.1", 662 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 663 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" 664 | }, 665 | "readable-stream": { 666 | "version": "3.6.0", 667 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 668 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 669 | "requires": { 670 | "inherits": "^2.0.3", 671 | "string_decoder": "^1.1.1", 672 | "util-deprecate": "^1.0.1" 673 | } 674 | }, 675 | "safe-buffer": { 676 | "version": "5.2.1", 677 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 678 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 679 | }, 680 | "sha.js": { 681 | "version": "2.4.11", 682 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 683 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 684 | "requires": { 685 | "inherits": "^2.0.1", 686 | "safe-buffer": "^5.0.1" 687 | } 688 | }, 689 | "simple-concat": { 690 | "version": "1.0.1", 691 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 692 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 693 | }, 694 | "simple-get": { 695 | "version": "4.0.1", 696 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 697 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 698 | "requires": { 699 | "decompress-response": "^6.0.0", 700 | "once": "^1.3.1", 701 | "simple-concat": "^1.0.0" 702 | } 703 | }, 704 | "string_decoder": { 705 | "version": "1.3.0", 706 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 707 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 708 | "requires": { 709 | "safe-buffer": "~5.2.0" 710 | } 711 | }, 712 | "tr46": { 713 | "version": "0.0.3", 714 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 715 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 716 | }, 717 | "tunnel": { 718 | "version": "0.0.6", 719 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 720 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" 721 | }, 722 | "universal-user-agent": { 723 | "version": "6.0.0", 724 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 725 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 726 | }, 727 | "util-deprecate": { 728 | "version": "1.0.2", 729 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 730 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 731 | }, 732 | "webidl-conversions": { 733 | "version": "3.0.1", 734 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 735 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 736 | }, 737 | "whatwg-url": { 738 | "version": "5.0.0", 739 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 740 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 741 | "requires": { 742 | "tr46": "~0.0.3", 743 | "webidl-conversions": "^3.0.0" 744 | } 745 | }, 746 | "wrappy": { 747 | "version": "1.0.2", 748 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 749 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 750 | } 751 | } 752 | } 753 | -------------------------------------------------------------------------------- /github-codeowners/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeowners-automation", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "codeowners-automation", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@octokit/auth-token": "^2.5.0", 13 | "@octokit/plugin-throttling": "^3.6.1", 14 | "@octokit/rest": "^18.12.0", 15 | "dotenv": "^16.0.0", 16 | "isomorphic-git": "^1.13.1" 17 | } 18 | }, 19 | "node_modules/@octokit/auth-token": { 20 | "version": "2.5.0", 21 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", 22 | "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", 23 | "dependencies": { 24 | "@octokit/types": "^6.0.3" 25 | } 26 | }, 27 | "node_modules/@octokit/core": { 28 | "version": "3.5.1", 29 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", 30 | "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", 31 | "dependencies": { 32 | "@octokit/auth-token": "^2.4.4", 33 | "@octokit/graphql": "^4.5.8", 34 | "@octokit/request": "^5.6.0", 35 | "@octokit/request-error": "^2.0.5", 36 | "@octokit/types": "^6.0.3", 37 | "before-after-hook": "^2.2.0", 38 | "universal-user-agent": "^6.0.0" 39 | } 40 | }, 41 | "node_modules/@octokit/endpoint": { 42 | "version": "6.0.12", 43 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 44 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 45 | "dependencies": { 46 | "@octokit/types": "^6.0.3", 47 | "is-plain-object": "^5.0.0", 48 | "universal-user-agent": "^6.0.0" 49 | } 50 | }, 51 | "node_modules/@octokit/graphql": { 52 | "version": "4.8.0", 53 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", 54 | "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", 55 | "dependencies": { 56 | "@octokit/request": "^5.6.0", 57 | "@octokit/types": "^6.0.3", 58 | "universal-user-agent": "^6.0.0" 59 | } 60 | }, 61 | "node_modules/@octokit/openapi-types": { 62 | "version": "11.2.0", 63 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", 64 | "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" 65 | }, 66 | "node_modules/@octokit/plugin-paginate-rest": { 67 | "version": "2.17.0", 68 | "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", 69 | "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", 70 | "dependencies": { 71 | "@octokit/types": "^6.34.0" 72 | }, 73 | "peerDependencies": { 74 | "@octokit/core": ">=2" 75 | } 76 | }, 77 | "node_modules/@octokit/plugin-request-log": { 78 | "version": "1.0.4", 79 | "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", 80 | "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", 81 | "peerDependencies": { 82 | "@octokit/core": ">=3" 83 | } 84 | }, 85 | "node_modules/@octokit/plugin-rest-endpoint-methods": { 86 | "version": "5.13.0", 87 | "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", 88 | "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", 89 | "dependencies": { 90 | "@octokit/types": "^6.34.0", 91 | "deprecation": "^2.3.1" 92 | }, 93 | "peerDependencies": { 94 | "@octokit/core": ">=3" 95 | } 96 | }, 97 | "node_modules/@octokit/plugin-throttling": { 98 | "version": "3.6.1", 99 | "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-3.6.1.tgz", 100 | "integrity": "sha512-T5DOcDQP0K/Ng5pnOEqCHDFojsftYL5o91MNbbR3nj1yAOACoGj3wDYCx0+5yJkbvRjYUdU0GsUt5/wYBba1cA==", 101 | "dependencies": { 102 | "@octokit/types": "^6.0.1", 103 | "bottleneck": "^2.15.3" 104 | }, 105 | "peerDependencies": { 106 | "@octokit/core": "^3.5.0" 107 | } 108 | }, 109 | "node_modules/@octokit/request": { 110 | "version": "5.6.3", 111 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", 112 | "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", 113 | "dependencies": { 114 | "@octokit/endpoint": "^6.0.1", 115 | "@octokit/request-error": "^2.1.0", 116 | "@octokit/types": "^6.16.1", 117 | "is-plain-object": "^5.0.0", 118 | "node-fetch": "^2.6.7", 119 | "universal-user-agent": "^6.0.0" 120 | } 121 | }, 122 | "node_modules/@octokit/request-error": { 123 | "version": "2.1.0", 124 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 125 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 126 | "dependencies": { 127 | "@octokit/types": "^6.0.3", 128 | "deprecation": "^2.0.0", 129 | "once": "^1.4.0" 130 | } 131 | }, 132 | "node_modules/@octokit/rest": { 133 | "version": "18.12.0", 134 | "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", 135 | "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", 136 | "dependencies": { 137 | "@octokit/core": "^3.5.1", 138 | "@octokit/plugin-paginate-rest": "^2.16.8", 139 | "@octokit/plugin-request-log": "^1.0.4", 140 | "@octokit/plugin-rest-endpoint-methods": "^5.12.0" 141 | } 142 | }, 143 | "node_modules/@octokit/types": { 144 | "version": "6.34.0", 145 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", 146 | "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", 147 | "dependencies": { 148 | "@octokit/openapi-types": "^11.2.0" 149 | } 150 | }, 151 | "node_modules/async-lock": { 152 | "version": "1.3.1", 153 | "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.1.tgz", 154 | "integrity": "sha512-zK7xap9UnttfbE23JmcrNIyueAn6jWshihJqA33U/hEnKprF/lVGBDsBv/bqLm2YMMl1DnpHhUY044eA0t1TUw==" 155 | }, 156 | "node_modules/before-after-hook": { 157 | "version": "2.2.2", 158 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", 159 | "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" 160 | }, 161 | "node_modules/bottleneck": { 162 | "version": "2.19.5", 163 | "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", 164 | "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" 165 | }, 166 | "node_modules/clean-git-ref": { 167 | "version": "2.0.1", 168 | "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", 169 | "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" 170 | }, 171 | "node_modules/crc-32": { 172 | "version": "1.2.1", 173 | "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz", 174 | "integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==", 175 | "dependencies": { 176 | "exit-on-epipe": "~1.0.1", 177 | "printj": "~1.3.1" 178 | }, 179 | "bin": { 180 | "crc32": "bin/crc32.njs" 181 | }, 182 | "engines": { 183 | "node": ">=0.8" 184 | } 185 | }, 186 | "node_modules/decompress-response": { 187 | "version": "6.0.0", 188 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 189 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 190 | "dependencies": { 191 | "mimic-response": "^3.1.0" 192 | }, 193 | "engines": { 194 | "node": ">=10" 195 | }, 196 | "funding": { 197 | "url": "https://github.com/sponsors/sindresorhus" 198 | } 199 | }, 200 | "node_modules/deprecation": { 201 | "version": "2.3.1", 202 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 203 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 204 | }, 205 | "node_modules/diff3": { 206 | "version": "0.0.3", 207 | "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", 208 | "integrity": "sha1-1OXDpM305f4SEatC5pP8tDIVgPw=" 209 | }, 210 | "node_modules/dotenv": { 211 | "version": "16.0.0", 212 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", 213 | "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", 214 | "engines": { 215 | "node": ">=12" 216 | } 217 | }, 218 | "node_modules/exit-on-epipe": { 219 | "version": "1.0.1", 220 | "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", 221 | "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", 222 | "engines": { 223 | "node": ">=0.8" 224 | } 225 | }, 226 | "node_modules/ignore": { 227 | "version": "5.2.0", 228 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", 229 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", 230 | "engines": { 231 | "node": ">= 4" 232 | } 233 | }, 234 | "node_modules/inherits": { 235 | "version": "2.0.4", 236 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 237 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 238 | }, 239 | "node_modules/is-plain-object": { 240 | "version": "5.0.0", 241 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 242 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", 243 | "engines": { 244 | "node": ">=0.10.0" 245 | } 246 | }, 247 | "node_modules/isomorphic-git": { 248 | "version": "1.13.1", 249 | "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.13.1.tgz", 250 | "integrity": "sha512-Hyc/KCCZAqTD5oyn90K5bRbCvRt1vj60OFKKmBZZwGrdzGvXu4FBBaqUrgWnd7N07M8soCOXibkDLq0lGWjNOQ==", 251 | "dependencies": { 252 | "async-lock": "^1.1.0", 253 | "clean-git-ref": "^2.0.1", 254 | "crc-32": "^1.2.0", 255 | "diff3": "0.0.3", 256 | "ignore": "^5.1.4", 257 | "minimisted": "^2.0.0", 258 | "pako": "^1.0.10", 259 | "pify": "^4.0.1", 260 | "readable-stream": "^3.4.0", 261 | "sha.js": "^2.4.9", 262 | "simple-get": "^4.0.1" 263 | }, 264 | "bin": { 265 | "isogit": "cli.cjs" 266 | }, 267 | "engines": { 268 | "node": ">=10" 269 | } 270 | }, 271 | "node_modules/mimic-response": { 272 | "version": "3.1.0", 273 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 274 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 275 | "engines": { 276 | "node": ">=10" 277 | }, 278 | "funding": { 279 | "url": "https://github.com/sponsors/sindresorhus" 280 | } 281 | }, 282 | "node_modules/minimist": { 283 | "version": "1.2.6", 284 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 285 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" 286 | }, 287 | "node_modules/minimisted": { 288 | "version": "2.0.1", 289 | "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", 290 | "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", 291 | "dependencies": { 292 | "minimist": "^1.2.5" 293 | } 294 | }, 295 | "node_modules/node-fetch": { 296 | "version": "2.6.7", 297 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 298 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 299 | "dependencies": { 300 | "whatwg-url": "^5.0.0" 301 | }, 302 | "engines": { 303 | "node": "4.x || >=6.0.0" 304 | }, 305 | "peerDependencies": { 306 | "encoding": "^0.1.0" 307 | }, 308 | "peerDependenciesMeta": { 309 | "encoding": { 310 | "optional": true 311 | } 312 | } 313 | }, 314 | "node_modules/once": { 315 | "version": "1.4.0", 316 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 317 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 318 | "dependencies": { 319 | "wrappy": "1" 320 | } 321 | }, 322 | "node_modules/pako": { 323 | "version": "1.0.11", 324 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 325 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 326 | }, 327 | "node_modules/pify": { 328 | "version": "4.0.1", 329 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 330 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", 331 | "engines": { 332 | "node": ">=6" 333 | } 334 | }, 335 | "node_modules/printj": { 336 | "version": "1.3.1", 337 | "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz", 338 | "integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==", 339 | "bin": { 340 | "printj": "bin/printj.njs" 341 | }, 342 | "engines": { 343 | "node": ">=0.8" 344 | } 345 | }, 346 | "node_modules/readable-stream": { 347 | "version": "3.6.0", 348 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 349 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 350 | "dependencies": { 351 | "inherits": "^2.0.3", 352 | "string_decoder": "^1.1.1", 353 | "util-deprecate": "^1.0.1" 354 | }, 355 | "engines": { 356 | "node": ">= 6" 357 | } 358 | }, 359 | "node_modules/safe-buffer": { 360 | "version": "5.2.1", 361 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 362 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 363 | "funding": [ 364 | { 365 | "type": "github", 366 | "url": "https://github.com/sponsors/feross" 367 | }, 368 | { 369 | "type": "patreon", 370 | "url": "https://www.patreon.com/feross" 371 | }, 372 | { 373 | "type": "consulting", 374 | "url": "https://feross.org/support" 375 | } 376 | ] 377 | }, 378 | "node_modules/sha.js": { 379 | "version": "2.4.11", 380 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 381 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 382 | "dependencies": { 383 | "inherits": "^2.0.1", 384 | "safe-buffer": "^5.0.1" 385 | }, 386 | "bin": { 387 | "sha.js": "bin.js" 388 | } 389 | }, 390 | "node_modules/simple-concat": { 391 | "version": "1.0.1", 392 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 393 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 394 | "funding": [ 395 | { 396 | "type": "github", 397 | "url": "https://github.com/sponsors/feross" 398 | }, 399 | { 400 | "type": "patreon", 401 | "url": "https://www.patreon.com/feross" 402 | }, 403 | { 404 | "type": "consulting", 405 | "url": "https://feross.org/support" 406 | } 407 | ] 408 | }, 409 | "node_modules/simple-get": { 410 | "version": "4.0.1", 411 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 412 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 413 | "funding": [ 414 | { 415 | "type": "github", 416 | "url": "https://github.com/sponsors/feross" 417 | }, 418 | { 419 | "type": "patreon", 420 | "url": "https://www.patreon.com/feross" 421 | }, 422 | { 423 | "type": "consulting", 424 | "url": "https://feross.org/support" 425 | } 426 | ], 427 | "dependencies": { 428 | "decompress-response": "^6.0.0", 429 | "once": "^1.3.1", 430 | "simple-concat": "^1.0.0" 431 | } 432 | }, 433 | "node_modules/string_decoder": { 434 | "version": "1.3.0", 435 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 436 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 437 | "dependencies": { 438 | "safe-buffer": "~5.2.0" 439 | } 440 | }, 441 | "node_modules/tr46": { 442 | "version": "0.0.3", 443 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 444 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 445 | }, 446 | "node_modules/universal-user-agent": { 447 | "version": "6.0.0", 448 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 449 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 450 | }, 451 | "node_modules/util-deprecate": { 452 | "version": "1.0.2", 453 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 454 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 455 | }, 456 | "node_modules/webidl-conversions": { 457 | "version": "3.0.1", 458 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 459 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 460 | }, 461 | "node_modules/whatwg-url": { 462 | "version": "5.0.0", 463 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 464 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 465 | "dependencies": { 466 | "tr46": "~0.0.3", 467 | "webidl-conversions": "^3.0.0" 468 | } 469 | }, 470 | "node_modules/wrappy": { 471 | "version": "1.0.2", 472 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 473 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 474 | } 475 | }, 476 | "dependencies": { 477 | "@octokit/auth-token": { 478 | "version": "2.5.0", 479 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", 480 | "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", 481 | "requires": { 482 | "@octokit/types": "^6.0.3" 483 | } 484 | }, 485 | "@octokit/core": { 486 | "version": "3.5.1", 487 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", 488 | "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", 489 | "requires": { 490 | "@octokit/auth-token": "^2.4.4", 491 | "@octokit/graphql": "^4.5.8", 492 | "@octokit/request": "^5.6.0", 493 | "@octokit/request-error": "^2.0.5", 494 | "@octokit/types": "^6.0.3", 495 | "before-after-hook": "^2.2.0", 496 | "universal-user-agent": "^6.0.0" 497 | } 498 | }, 499 | "@octokit/endpoint": { 500 | "version": "6.0.12", 501 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 502 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 503 | "requires": { 504 | "@octokit/types": "^6.0.3", 505 | "is-plain-object": "^5.0.0", 506 | "universal-user-agent": "^6.0.0" 507 | } 508 | }, 509 | "@octokit/graphql": { 510 | "version": "4.8.0", 511 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", 512 | "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", 513 | "requires": { 514 | "@octokit/request": "^5.6.0", 515 | "@octokit/types": "^6.0.3", 516 | "universal-user-agent": "^6.0.0" 517 | } 518 | }, 519 | "@octokit/openapi-types": { 520 | "version": "11.2.0", 521 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", 522 | "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" 523 | }, 524 | "@octokit/plugin-paginate-rest": { 525 | "version": "2.17.0", 526 | "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", 527 | "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", 528 | "requires": { 529 | "@octokit/types": "^6.34.0" 530 | } 531 | }, 532 | "@octokit/plugin-request-log": { 533 | "version": "1.0.4", 534 | "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", 535 | "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", 536 | "requires": {} 537 | }, 538 | "@octokit/plugin-rest-endpoint-methods": { 539 | "version": "5.13.0", 540 | "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", 541 | "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", 542 | "requires": { 543 | "@octokit/types": "^6.34.0", 544 | "deprecation": "^2.3.1" 545 | } 546 | }, 547 | "@octokit/plugin-throttling": { 548 | "version": "3.6.1", 549 | "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-3.6.1.tgz", 550 | "integrity": "sha512-T5DOcDQP0K/Ng5pnOEqCHDFojsftYL5o91MNbbR3nj1yAOACoGj3wDYCx0+5yJkbvRjYUdU0GsUt5/wYBba1cA==", 551 | "requires": { 552 | "@octokit/types": "^6.0.1", 553 | "bottleneck": "^2.15.3" 554 | } 555 | }, 556 | "@octokit/request": { 557 | "version": "5.6.3", 558 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", 559 | "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", 560 | "requires": { 561 | "@octokit/endpoint": "^6.0.1", 562 | "@octokit/request-error": "^2.1.0", 563 | "@octokit/types": "^6.16.1", 564 | "is-plain-object": "^5.0.0", 565 | "node-fetch": "^2.6.7", 566 | "universal-user-agent": "^6.0.0" 567 | } 568 | }, 569 | "@octokit/request-error": { 570 | "version": "2.1.0", 571 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 572 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 573 | "requires": { 574 | "@octokit/types": "^6.0.3", 575 | "deprecation": "^2.0.0", 576 | "once": "^1.4.0" 577 | } 578 | }, 579 | "@octokit/rest": { 580 | "version": "18.12.0", 581 | "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", 582 | "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", 583 | "requires": { 584 | "@octokit/core": "^3.5.1", 585 | "@octokit/plugin-paginate-rest": "^2.16.8", 586 | "@octokit/plugin-request-log": "^1.0.4", 587 | "@octokit/plugin-rest-endpoint-methods": "^5.12.0" 588 | } 589 | }, 590 | "@octokit/types": { 591 | "version": "6.34.0", 592 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", 593 | "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", 594 | "requires": { 595 | "@octokit/openapi-types": "^11.2.0" 596 | } 597 | }, 598 | "async-lock": { 599 | "version": "1.3.1", 600 | "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.1.tgz", 601 | "integrity": "sha512-zK7xap9UnttfbE23JmcrNIyueAn6jWshihJqA33U/hEnKprF/lVGBDsBv/bqLm2YMMl1DnpHhUY044eA0t1TUw==" 602 | }, 603 | "before-after-hook": { 604 | "version": "2.2.2", 605 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", 606 | "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" 607 | }, 608 | "bottleneck": { 609 | "version": "2.19.5", 610 | "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", 611 | "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" 612 | }, 613 | "clean-git-ref": { 614 | "version": "2.0.1", 615 | "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", 616 | "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" 617 | }, 618 | "crc-32": { 619 | "version": "1.2.1", 620 | "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz", 621 | "integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==", 622 | "requires": { 623 | "exit-on-epipe": "~1.0.1", 624 | "printj": "~1.3.1" 625 | } 626 | }, 627 | "decompress-response": { 628 | "version": "6.0.0", 629 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 630 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 631 | "requires": { 632 | "mimic-response": "^3.1.0" 633 | } 634 | }, 635 | "deprecation": { 636 | "version": "2.3.1", 637 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 638 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 639 | }, 640 | "diff3": { 641 | "version": "0.0.3", 642 | "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", 643 | "integrity": "sha1-1OXDpM305f4SEatC5pP8tDIVgPw=" 644 | }, 645 | "dotenv": { 646 | "version": "16.0.0", 647 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", 648 | "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" 649 | }, 650 | "exit-on-epipe": { 651 | "version": "1.0.1", 652 | "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", 653 | "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" 654 | }, 655 | "ignore": { 656 | "version": "5.2.0", 657 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", 658 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" 659 | }, 660 | "inherits": { 661 | "version": "2.0.4", 662 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 663 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 664 | }, 665 | "is-plain-object": { 666 | "version": "5.0.0", 667 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 668 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" 669 | }, 670 | "isomorphic-git": { 671 | "version": "1.13.1", 672 | "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.13.1.tgz", 673 | "integrity": "sha512-Hyc/KCCZAqTD5oyn90K5bRbCvRt1vj60OFKKmBZZwGrdzGvXu4FBBaqUrgWnd7N07M8soCOXibkDLq0lGWjNOQ==", 674 | "requires": { 675 | "async-lock": "^1.1.0", 676 | "clean-git-ref": "^2.0.1", 677 | "crc-32": "^1.2.0", 678 | "diff3": "0.0.3", 679 | "ignore": "^5.1.4", 680 | "minimisted": "^2.0.0", 681 | "pako": "^1.0.10", 682 | "pify": "^4.0.1", 683 | "readable-stream": "^3.4.0", 684 | "sha.js": "^2.4.9", 685 | "simple-get": "^4.0.1" 686 | } 687 | }, 688 | "mimic-response": { 689 | "version": "3.1.0", 690 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 691 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" 692 | }, 693 | "minimist": { 694 | "version": "1.2.6", 695 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 696 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" 697 | }, 698 | "minimisted": { 699 | "version": "2.0.1", 700 | "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", 701 | "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", 702 | "requires": { 703 | "minimist": "^1.2.5" 704 | } 705 | }, 706 | "node-fetch": { 707 | "version": "2.6.7", 708 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 709 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 710 | "requires": { 711 | "whatwg-url": "^5.0.0" 712 | } 713 | }, 714 | "once": { 715 | "version": "1.4.0", 716 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 717 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 718 | "requires": { 719 | "wrappy": "1" 720 | } 721 | }, 722 | "pako": { 723 | "version": "1.0.11", 724 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 725 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 726 | }, 727 | "pify": { 728 | "version": "4.0.1", 729 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 730 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" 731 | }, 732 | "printj": { 733 | "version": "1.3.1", 734 | "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz", 735 | "integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==" 736 | }, 737 | "readable-stream": { 738 | "version": "3.6.0", 739 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 740 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 741 | "requires": { 742 | "inherits": "^2.0.3", 743 | "string_decoder": "^1.1.1", 744 | "util-deprecate": "^1.0.1" 745 | } 746 | }, 747 | "safe-buffer": { 748 | "version": "5.2.1", 749 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 750 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 751 | }, 752 | "sha.js": { 753 | "version": "2.4.11", 754 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 755 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 756 | "requires": { 757 | "inherits": "^2.0.1", 758 | "safe-buffer": "^5.0.1" 759 | } 760 | }, 761 | "simple-concat": { 762 | "version": "1.0.1", 763 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 764 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 765 | }, 766 | "simple-get": { 767 | "version": "4.0.1", 768 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 769 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 770 | "requires": { 771 | "decompress-response": "^6.0.0", 772 | "once": "^1.3.1", 773 | "simple-concat": "^1.0.0" 774 | } 775 | }, 776 | "string_decoder": { 777 | "version": "1.3.0", 778 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 779 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 780 | "requires": { 781 | "safe-buffer": "~5.2.0" 782 | } 783 | }, 784 | "tr46": { 785 | "version": "0.0.3", 786 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 787 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 788 | }, 789 | "universal-user-agent": { 790 | "version": "6.0.0", 791 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 792 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 793 | }, 794 | "util-deprecate": { 795 | "version": "1.0.2", 796 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 797 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 798 | }, 799 | "webidl-conversions": { 800 | "version": "3.0.1", 801 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 802 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 803 | }, 804 | "whatwg-url": { 805 | "version": "5.0.0", 806 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 807 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 808 | "requires": { 809 | "tr46": "~0.0.3", 810 | "webidl-conversions": "^3.0.0" 811 | } 812 | }, 813 | "wrappy": { 814 | "version": "1.0.2", 815 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 816 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 817 | } 818 | } 819 | } 820 | --------------------------------------------------------------------------------