├── cli ├── .gitignore ├── commands │ ├── commandsexports.js │ ├── initalize.js │ ├── configure.js │ └── start.js ├── index.js ├── questions │ └── questions.js ├── package.json └── util │ └── QueryInspector.js ├── .gitignore ├── chaos-qoala-agent ├── .gitignore ├── tests │ ├── jest.config.js │ └── index.test.js ├── .eslintrc.js ├── package.json └── index.js ├── chaos-qoala-integration-tests ├── .gitignore ├── tests │ ├── jest.config.js │ ├── gql-schema │ │ └── schema.gql │ ├── globalSetup.js │ ├── globalTeardown.js │ ├── query-inspector.test.js │ ├── platforms │ │ ├── express-standalone.js │ │ ├── express-graphql-app.js │ │ └── apollo-server-express-app.js │ ├── express.test.js │ └── apollo.test.js ├── chaos-qoala-config.js ├── .eslintrc.js ├── steady-state-server-stub │ ├── readme.txt │ ├── server.js │ └── results.js └── package.json ├── client ├── scss │ ├── _graphs.scss │ ├── application.scss │ ├── _fileupload.scss │ ├── _navbar.scss │ ├── _thisisus.scss │ ├── _instructions.scss │ └── _main.scss ├── src │ ├── components │ │ ├── Upload.jsx │ │ ├── DownloadButton.jsx │ │ ├── NavBar.jsx │ │ ├── Home.jsx │ │ ├── App.jsx │ │ ├── SteadyStateChart.jsx │ │ ├── ChaosFlagChart.jsx │ │ ├── Main.jsx │ │ ├── Graphs.jsx │ │ ├── Instructions.jsx │ │ ├── FileUpload.jsx │ │ └── ThisIsUs.jsx │ ├── App.test.js │ ├── index.js │ ├── index.css │ └── App.css ├── index.html ├── .gitignore ├── public │ ├── manifest.json │ └── index.html ├── webpack.config.js ├── package.json ├── server │ └── server.js └── README.md ├── aws ├── common-aws.js ├── package.json ├── delete-aws.js ├── .env_EXAMPLE ├── lambda.js ├── deploy-aws.js ├── cloud-formation-s3.yml ├── chaos-website-deployer.js ├── cloud-formation-lambda-api.yml └── package-lock.json ├── LICENSE └── README.md /cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /chaos-qoala-agent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /chaos-qoala-agent/tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | -------------------------------------------------------------------------------- /client/scss/_graphs.scss: -------------------------------------------------------------------------------- 1 | h2 { 2 | text-align: center; 3 | color: rgb(132, 0, 255); 4 | } -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globalSetup: './globalSetup.js', 3 | globalTeardown: './globalTeardown.js', 4 | }; 5 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/gql-schema/schema.gql: -------------------------------------------------------------------------------- 1 | type Query{ 2 | dontKnockMeOut: String 3 | knockMeOut: String 4 | } 5 | 6 | schema { 7 | query: Query 8 | } -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/globalSetup.js: -------------------------------------------------------------------------------- 1 | const globalSetupFunc = () => { 2 | // add any global startup tasks 3 | }; 4 | 5 | module.exports = globalSetupFunc; 6 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/globalTeardown.js: -------------------------------------------------------------------------------- 1 | const globalTeardownFunc = () => { 2 | // add any global cleanup tasks 3 | }; 4 | 5 | module.exports = globalTeardownFunc; 6 | -------------------------------------------------------------------------------- /cli/commands/commandsexports.js: -------------------------------------------------------------------------------- 1 | // This exports the command files in one file to make things a touch easier 2 | 3 | module.exports = { 4 | ...require('./initalize'), 5 | ...require('./configure'), 6 | ...require('./start'), 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/components/Upload.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileUpload from './FileUpload.jsx'; 3 | 4 | function Upload() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | 13 | export default Upload; 14 | -------------------------------------------------------------------------------- /client/src/components/DownloadButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function DownloadButton() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default DownloadButton 12 | -------------------------------------------------------------------------------- /client/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import '_navbar.scss'; 2 | @import '_main.scss'; 3 | @import '_instructions.scss'; 4 | @import '_thisisus.scss'; 5 | @import '_fileupload.scss'; 6 | @import '_graphs.scss'; 7 | @import url('https://fonts.googleapis.com/css?family=Manjari&display=swap'); 8 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './components/App.jsx'; 4 | 5 | // // uncomment so that webpack can bundle styles 6 | import styles from '../scss/application.scss'; 7 | 8 | render( 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ChasQoaLa: Chaos Engineering Meets GraphQL 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/chaos-qoala-config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | socketPort: 'http://localhost:1025', 3 | graphQLPort: 'http://localhost:3000', 4 | blastRadius: 1.0, 5 | delay: 3000, 6 | missingData: true, 7 | affectedQueries: { knockMeOut: true, dontKnockMeOut: false }, 8 | runTime: '60', 9 | ensueChaos: true, 10 | }; 11 | 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /chaos-qoala-agent/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | 'airbnb-base', 10 | ], 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly', 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2018, 17 | }, 18 | rules: { 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | 'airbnb-base', 10 | ], 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly', 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2018, 17 | }, 18 | rules: { 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # aws 4 | .creds.js 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /client/scss/_fileupload.scss: -------------------------------------------------------------------------------- 1 | .cq-file-upload { 2 | margin: 100px; 3 | border-style: solid; 4 | border-color: rgb(187, 20, 151); 5 | border-width: 10px; 6 | text-align: center; 7 | text-transform: capitalize; 8 | color: rgb(187, 20, 151); 9 | font-family: Verdana, Geneva, Tahoma, sans-serif ; 10 | font-size: 15px; 11 | font-weight: bold; 12 | background-color: rgb(0, 0, 0); 13 | } 14 | 15 | .cq-file-upload:hover { 16 | border-color: black; 17 | background-color: rgb(187, 20, 151); 18 | color: black; 19 | } -------------------------------------------------------------------------------- /aws/common-aws.js: -------------------------------------------------------------------------------- 1 | const DOMAIN_NAME = 'lovechaos.net'; 2 | module.exports = { 3 | DOMAIN_NAME : DOMAIN_NAME, 4 | S3_STACK_NAME : DOMAIN_NAME.replace('.', '') + '-www', 5 | API_STACK_NAME : DOMAIN_NAME.replace('.', '') + '-api', 6 | LAMBDA_S3_BUCKET : DOMAIN_NAME.replace('.', '') + '-lambda', 7 | API_END_POINT : 'api.' + DOMAIN_NAME, 8 | REGION_HOSTED_ZONE_ID : process.env.REGION_HOSTED_ZONE_ID, 9 | CUSTOM_DOMAIN_HOSTED_ZONE_ID : process.env.CUSTOM_DOMAIN_HOSTED_ZONE_ID, 10 | REGIONAL_CERTIFICATE_ARN : process.env.REGIONAL_CERTIFICATE_ARN 11 | }; 12 | -------------------------------------------------------------------------------- /chaos-qoala-agent/tests/index.test.js: -------------------------------------------------------------------------------- 1 | beforeAll(() => { 2 | // eslint-disable-next-line no-console 3 | console.log('add global setup code here'); 4 | }); 5 | 6 | 7 | // test altering basic query response 8 | describe('Chaos 🐨 Agent Unit Tests', () => { 9 | // note the async so we can await fetch 10 | test('add unit tests here', () => { 11 | expect('the more tests the merrier').toBe('the more tests the merrier'); 12 | }); 13 | }); 14 | 15 | 16 | afterAll(() => { 17 | // eslint-disable-next-line no-console 18 | console.log('add global clean up code here'); 19 | }); 20 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | 5 | const { configure, start } = require('./commands/commandsexports'); 6 | 7 | program 8 | // defines a new command and how the command will be executed as well as what text the user will see 9 | .command('configure') 10 | .description('Initiate a config file') 11 | .action(configure); 12 | 13 | program 14 | .command('start') 15 | .description('Send the information to the ChaosQoala agent') 16 | .action(start); 17 | 18 | program 19 | .parse(process.argv); 20 | 21 | 22 | if (!process.argv.slice(2).length) { 23 | program.outputHelp(); 24 | } 25 | -------------------------------------------------------------------------------- /aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "deploy-aws": "node deploy-aws.js", 9 | "delete-aws": "node delete-aws.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "adm-zip": "^0.4.13", 16 | "aws-sdk": "^2.524.0", 17 | "dotenv": "^8.1.0" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^6.3.0", 21 | "eslint-config-airbnb-base": "^14.0.0", 22 | "eslint-plugin-import": "^2.18.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | React App 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /aws/delete-aws.js: -------------------------------------------------------------------------------- 1 | const chaosDeployer = require('./chaos-website-deployer'); 2 | const awsVars = require('./common-aws'); 3 | 4 | (async () => { 5 | try { 6 | await chaosDeployer.init(); 7 | 8 | const s3Exists = await chaosDeployer.stackExists(awsVars.S3_STACK_NAME); 9 | const apiExists = await chaosDeployer.stackExists(awsVars.API_STACK_NAME); 10 | 11 | if (s3Exists) { 12 | await chaosDeployer.deleteS3Objects(awsVars.DOMAIN_NAME); 13 | await chaosDeployer.deleteS3Objects(awsVars.LAMBDA_S3_BUCKET); 14 | await chaosDeployer.deleteStack(awsVars.S3_STACK_NAME); 15 | } 16 | 17 | if (apiExists) await chaosDeployer.deleteStack(awsVars.API_STACK_NAME); 18 | } catch (err) { 19 | console.log(err); 20 | } 21 | })(); 22 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/query-inspector.test.js: -------------------------------------------------------------------------------- 1 | const express = require('./platforms/express-graphql-app'); 2 | 3 | const platform = express; 4 | const QueryInspector = require('../../cli/util/QueryInspector'); 5 | 6 | beforeAll(async () => { 7 | await platform.start(); 8 | }); 9 | 10 | describe('Schema Inspector', () => { 11 | test('basic test', async () => { 12 | const url = `http://localhost:${platform.port}/graphql`; 13 | const inspector = new QueryInspector(url); 14 | await inspector.hydrateQueryMap(); 15 | const queryList = inspector.getQueryMap(); 16 | expect(Object.keys(queryList).join()).toBe('dontKnockMeOut,knockMeOut'); 17 | }); 18 | }); 19 | 20 | // stop platform servers at end of all tests 21 | afterAll(async (done) => { 22 | await platform.stop(); 23 | done(); 24 | }); 25 | -------------------------------------------------------------------------------- /aws/.env_EXAMPLE: -------------------------------------------------------------------------------- 1 | # your .aws/certificates file for the node sdk must include this entry with the square brackets this [chaos] instead of [default]. 2 | # also make sure you have created a user with the AdministratorAccess managed policy role and use the keys for that user in the 3 | # .aws/certificates file 4 | AWS_PROFILE=chaos 5 | 6 | # AWW region you are deploying to 7 | AWS_REGION=us-west-1 8 | 9 | # lookup the hosted zone id for the region you are deploying in from the following site 10 | # https://gist.github.com/nicolasdao/558050d892f74d63b515ebaf0485457c 11 | REGION_HOSTED_ZONE_ID=Z2MUQ32089INYE 12 | 13 | # look in route 53 for your personal domain details (chaosqoala.com) 14 | CUSTOM_DOMAIN_HOSTED_ZONE_ID=Z3.......... 15 | 16 | # the certificate ARN you generact from ACM for your personal domain 17 | REGIONAL_CERTIFICATE_ARN=arn:aws:acm:us-west-1:4.......... -------------------------------------------------------------------------------- /client/src/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import Upload from "./Upload.jsx"; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | // import DownloadButton from './DownloadButton.jsx'; 6 | 7 | function NavBar() { 8 | return ( 9 | 28 | ); 29 | } 30 | 31 | export default NavBar; 32 | -------------------------------------------------------------------------------- /chaos-qoala-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chaos-qoala-agent", 3 | "version": "0.0.1", 4 | "description": "Chaos QoaLa Agent - for use on a GraphQL server in conjunction with Chaos QoaLa Controller client app", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --config ./tests/jest.config.js" 8 | }, 9 | "author": "tasselwobegong.com", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "socket.io": "^2.2.0", 14 | "zlib": "^1.0.5" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^6.2.1", 18 | "eslint-config-airbnb": "^18.0.1", 19 | "eslint-config-airbnb-base": "^14.0.0", 20 | "eslint-plugin-import": "^2.18.2", 21 | "eslint-plugin-jest": "^22.15.2", 22 | "eslint-plugin-jsx-a11y": "^6.2.3", 23 | "eslint-plugin-react": "^7.14.3", 24 | "eslint-plugin-react-hooks": "^1.7.0", 25 | "jest": "^24.9.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import NavBar from './NavBar.jsx'; 4 | import Upload from './Upload.jsx'; 5 | import Main from './Main.jsx'; 6 | import Instructions from './Instructions.jsx'; 7 | import ThisIsUs from './ThisIsUs.jsx'; 8 | 9 | class Home extends Component { 10 | constructor() { 11 | super(); 12 | this.state = {}; 13 | } 14 | 15 | render() { 16 | return ( 17 | // 18 |
19 | 20 |
21 | 22 | 23 | {/* 24 | 25 |
26 | 27 | */} 28 |
29 | //
30 | ); 31 | } 32 | } 33 | 34 | export default Home; 35 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/steady-state-server-stub/readme.txt: -------------------------------------------------------------------------------- 1 | #starting the fake steady state data service 2 | cd chaos-qoala-integration-tests 3 | npm run steadystate 4 | 5 | #default port is 7766 6 | 7 | #start the test run (example below assumes your machine has an IP of 192.168.0.85): 8 | 9 | PUT http://192.168.0.85:7766/steadystate/start 10 | 11 | #get the results (if there was no prior call to /steadystate/start then error code of 400 will be returned) 12 | 13 | base url is /steadystate/start which then takes 4 url args 14 | 1. baseline - baseline (integer) figure for steady state 15 | 2. lower variance - % as integer 16 | 3. upper variance - % as an integer 17 | 4. results per second - as an integer 18 | 19 | so to get results based on a baseline of 100 with a maximum 50% variance in the lower range 20 | and a 10% variance in the upper range, and 10 results per test run second you would use the following: 21 | 22 | GET http://192.168.0.85:7766/steadystate/stop/100/50/10/10/ -------------------------------------------------------------------------------- /client/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import Home from './Home.jsx'; 4 | // import NavBar from './NavBar.jsx'; 5 | import Upload from './Upload.jsx'; 6 | import NavBar from './NavBar.jsx'; 7 | // import Main from './Main.jsx'; 8 | // import Instructions from './Instructions.jsx'; 9 | // import ThisIsUs from './ThisIsUs.jsx'; 10 | class App extends Component { 11 | constructor() { 12 | super(); 13 | this.state = {}; 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 22 | {/* */} 23 | 24 | {/*
25 | 26 | */} 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default App; -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | 4 | module.exports = { 5 | entry: { 6 | src: './src/index.js' 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'build'), 10 | filename: 'bundle.js' 11 | }, 12 | mode: process.env.NODE_ENV, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | query: { 20 | presets: ['@babel/env', '@babel/react'] 21 | } 22 | }, 23 | { 24 | test: /\.s?css/, 25 | use: [ 26 | 'style-loader', 'css-loader', 'sass-loader' 27 | ] 28 | } 29 | ] 30 | }, 31 | devServer: { 32 | publicPath: '/build', 33 | proxy: { 34 | '/api': 'http://localhost:3000' 35 | } 36 | }, 37 | 38 | } 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /client/src/components/SteadyStateChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | 4 | class SteadyStateChart extends Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | // static defaultProps = { 10 | // displayTitle: true, 11 | // displayLegend: true, 12 | // legendPosition: 'right', 13 | // location: 'City' 14 | // } 15 | 16 | render() { 17 | return ( 18 |
19 | 33 |
34 | ); 35 | } 36 | 37 | } 38 | 39 | export default SteadyStateChart; -------------------------------------------------------------------------------- /cli/commands/initalize.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | // reads the path to the package.json file 5 | const packageJSON = join(__dirname, '..', 'package.json'); 6 | const configFile = require(packageJSON); 7 | 8 | /* Saves the users' questions in the package.json file */ 9 | function initalize(state) { 10 | configFile.chaosQoala = { 11 | ...state, 12 | }; 13 | 14 | writeFileSync( 15 | packageJSON, 16 | JSON.stringify(configFile, null, 2), 17 | ); 18 | } 19 | 20 | /* This function reads the configuration file */ 21 | 22 | function read() { 23 | const { configQuestions = {} } = configFile; 24 | const { questions = [], state = {} } = configQuestions; 25 | return { 26 | questions, 27 | state, 28 | }; 29 | } 30 | 31 | /* This function saves the updated config file */ 32 | function save(state) { 33 | const { configQuestions = {} } = configFile; 34 | initalize({ ...configQuestions, state: { ...configQuestions.state, ...state } }); 35 | } 36 | 37 | module.exports = { 38 | initalize, 39 | read, 40 | save, 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/components/ChaosFlagChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | 4 | class ChaosFlagChart extends Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | // static defaultProps = { 10 | // displayTitle: true, 11 | // displayLegend: true, 12 | // legendPosition: 'right', 13 | // location: 'City' 14 | // } 15 | 16 | render() { 17 | return ( 18 |
19 | 38 |
39 | ); 40 | } 41 | 42 | } 43 | 44 | export default ChaosFlagChart; -------------------------------------------------------------------------------- /client/src/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | function Main() { 5 | return ( 6 |
7 |

8 | 12 | ChaosQoaLa 13 |

14 |

Bringing Chaos Engineering to GraphQL

15 | 16 |

17 | Star Us on GitHub!{" "} 18 | 19 | 26 | 27 |

28 | 33 |
34 | ); 35 | } 36 | 37 | export default Main; 38 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm link chaos-qoala-agent && jest --config ./tests/jest.config.js --runInBand", 8 | "steadystate": "nodemon ./steady-state-server-stub/server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "eslint": "^6.2.1", 15 | "eslint-config-airbnb": "^18.0.1", 16 | "eslint-config-airbnb-base": "^14.0.0", 17 | "eslint-plugin-import": "^2.18.2", 18 | "eslint-plugin-jest": "^22.15.2", 19 | "eslint-plugin-jsx-a11y": "^6.2.3", 20 | "eslint-plugin-react": "^7.14.3", 21 | "eslint-plugin-react-hooks": "^1.7.0", 22 | "jest": "^24.9.0", 23 | "nodemon": "^1.19.1" 24 | }, 25 | "dependencies": { 26 | "apollo-server-express": "^2.8.2", 27 | "chaosqoala-agent": "^1.0.0", 28 | "express": "^4.17.1", 29 | "express-graphql": "^0.9.0", 30 | "graphql": "^14.4.2", 31 | "node-fetch": "^2.6.0", 32 | "path": "^0.12.7", 33 | "random-js": "^2.1.0", 34 | "socket.io-client": "^2.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /aws/lambda.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(event, context) { 2 | const body = JSON.parse(event.body); 3 | const data = JSON.parse(body); 4 | 5 | const { chaosResults } = data; 6 | const { agentData } = data; 7 | 8 | const chaosTimes = chaosResults.map(function(element) { 9 | return element["timeOfResult"]; 10 | }); 11 | 12 | const chaosData = chaosResults.map(function(element) { 13 | return element["result"]; 14 | }); 15 | 16 | const agentTimes = agentData.map(function(element) { 17 | return element["timeOfResponse"]; 18 | }); 19 | 20 | const agentFlags = agentData.map(function(element) { 21 | return element["chaosResponse"]; 22 | }); 23 | 24 | const responseCode = 200; 25 | const response = { 26 | statusCode: responseCode, 27 | headers: { 28 | "Access-Control-Allow-Origin": "*", 29 | "Content-Type" : "application/json", 30 | "Access-Control-Allow-Origin" : "*", 31 | "Allow" : "OPTIONS, POST", 32 | "Access-Control-Allow-Methods" : "OPTIONS, POST", 33 | "Access-Control-Allow-Headers" : "*" 34 | }, 35 | body: JSON.stringify({ chaosTimes, chaosData, agentTimes, agentFlags }) 36 | }; 37 | context.succeed(response); 38 | }; -------------------------------------------------------------------------------- /cli/questions/questions.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | type: 'input', 3 | name: 'socketPort', 4 | message: 'Please enter the URI of the Socket.io port over which Chaos will be sent and received', 5 | }, { 6 | type: 'input', 7 | name: 'graphQLPort', 8 | message: 'Please enter the URI of GraphQL service', 9 | }, 10 | { 11 | type: 'number', 12 | name: 'blastRadius', 13 | message: 'Please enter your desired blast radius', 14 | default: Math.random(), 15 | }, { 16 | type: 'number', 17 | name: 'delay', 18 | message: 'Please enter the amount of time you would like your data to be dealyed (in milliseconds)', 19 | }, { 20 | type: 'number', 21 | name: 'runTime', 22 | message: 'Please enter how long you would like the ChaosQoala to run (in minutes). **NOTE: The default runtime is 1 hour', 23 | default: 60, 24 | }, { 25 | type: 'input', 26 | name: 'steadyStateStartURL', 27 | message: 'Please enter the steady state start URL', 28 | }, { 29 | type: 'input', 30 | name: 'steadyStateStartHTTPVerb', 31 | message: 'Please enter the steady state start HTTP verb', 32 | }, { 33 | type: 'input', 34 | name: 'steadyStateStopURL', 35 | message: 'Please enter the steady state stop URL', 36 | }, { 37 | type: 'input', 38 | name: 'steadyStateStopHTTPVerb', 39 | message: 'Please enter the steady state stop HTTP verb', 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/platforms/express-standalone.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const expressGraphQL = require('express-graphql'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const { buildSchema } = require('graphql'); 7 | 8 | // Create an express server and a GraphQL endpoint 9 | const app = express(); 10 | 11 | // 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 12 | // eslint-disable-next-line import/no-unresolved 13 | // eslint-disable-next-line import/no-extraneous-dependencies 14 | const { chaos, chaosSocketServer } = require('chaos-qoala-agent'); 15 | 16 | app.use(chaos); 17 | this.chaosSocketServer = chaosSocketServer; 18 | // 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 19 | 20 | // get gql test schema definition 21 | const pathToSchema = path.resolve(__dirname, '../gql-schema/schema.gql'); 22 | const schema = buildSchema(fs.readFileSync(pathToSchema, 'utf-8')); 23 | 24 | // Root resolver 25 | const root = { 26 | knockMeOut: () => 'I should be knocked out by Chaos QoaLa', 27 | dontKnockMeOut: () => 'I should NOT be knocked out by Chaos QoaLa', 28 | }; 29 | 30 | app.use('/graphql', expressGraphQL({ 31 | schema, 32 | rootValue: root, 33 | graphiql: true, 34 | })); 35 | 36 | app.listen(3000, () => { 37 | // eslint-disable-next-line no-console 38 | console.log('🚆 Express GraphQL Server ready at localhost:3000/graphql'); 39 | }); 40 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chaosqoala", 3 | "version": "1.0.0", 4 | "description": "Chaos Engineering meets GraphQL", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "bin": { 12 | "chaosqoala": "./index.js" 13 | }, 14 | "dependencies": { 15 | "chalk": "^2.4.2", 16 | "commander": "^3.0.0", 17 | "express": "^4.17.1", 18 | "figlet": "^1.2.3", 19 | "fs": "0.0.1-security", 20 | "http": "0.0.0", 21 | "inquirer": "^7.0.0", 22 | "node-fetch": "^2.6.0", 23 | "node-sass": "^4.12.0", 24 | "prompt": "^1.0.0", 25 | "save": "^2.4.0", 26 | "socket.io": "^2.2.0", 27 | "socket.io-client": "^2.2.0" 28 | }, 29 | "chaosQoala": { 30 | "state": { 31 | "socketPort": "http://localhost:1025", 32 | "graphQLPort": "http://localhost:3000/graphql", 33 | "blastRadius": "0.5", 34 | "delay": "3000", 35 | "runTime": "60", 36 | "steadyStateStartURL": "http://localhost:7766/steadystate/start", 37 | "steadyStateStartHTTPVerb": "PUT", 38 | "steadyStateStopURL": "http://localhost:7766/steadystate/stop/100/50/20/10", 39 | "steadyStateStopHTTPVerb": "GET", 40 | "affectedQueries": { 41 | "dontKnockMeOut": false, 42 | "knockMeOut": true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cli/util/QueryInspector.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | class QueryInspector { 4 | constructor(uri) { 5 | this.uri = uri; 6 | this.queryMap = {}; 7 | } 8 | 9 | // returns the count of querys found in the schema 10 | async hydrateQueryMap() { 11 | const queryToGetAllQueryNames = '{__schema{queryType{fields{name}}}}'; 12 | const query = { query: queryToGetAllQueryNames, variables: null }; 13 | 14 | const response = await fetch(this.uri, { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | }, 19 | body: JSON.stringify(query), 20 | }); 21 | const responseJson = await response.json(); 22 | 23 | // eslint-disable-next-line no-underscore-dangle 24 | responseJson.data.__schema.queryType.fields.forEach((element) => { 25 | // add query as key in an object - the boolean represents 26 | // if the data returned from the query will potentially be 27 | // affected due to user preferences in the chaos config 28 | // for the moment we don't have access to those preferences 29 | // so just initialize to false 30 | this.queryMap[element.name] = false; 31 | }); 32 | 33 | return Object.keys(this.queryMap).length; 34 | } 35 | 36 | // returns the list of available queries once the hydrate function has been called 37 | getQueryMap() { 38 | return this.queryMap; 39 | } 40 | } 41 | 42 | module.exports = QueryInspector; 43 | -------------------------------------------------------------------------------- /client/src/components/Graphs.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SteadyStateChart from './SteadyStateChart.jsx'; 3 | import ChaosFlagChart from './ChaosFlagChart.jsx'; 4 | 5 | class Graphs extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | steadyStateChartData: {}, 10 | chaosFlagChartData: {} 11 | }; 12 | } 13 | 14 | 15 | render() { 16 | const steadyStateChartConfig = { 17 | labels: this.props.data.chaosTimes, 18 | datasets: [ 19 | { 20 | label: "Latency", 21 | data: this.props.data.chaosData, 22 | fill: false, 23 | borderColor: "red", 24 | backgroundColor: "blue" 25 | } 26 | ] 27 | }; 28 | const chaosFlagChartConfig = { 29 | labels: this.props.data.agentTimes, 30 | datasets: [ 31 | { 32 | label: "Chaos Instances", 33 | data: this.props.data.agentFlags, 34 | backgroundColor: "red", 35 | fill: false 36 | } 37 | ] 38 | }; 39 | return ( 40 |
41 |

ChaosQoaLa Experiment Results

42 | 46 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | export default Graphs; -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/steady-state-server-stub/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const getResults = require('./results'); 3 | 4 | const PORT = 7766; 5 | const app = express(); 6 | 7 | let startTimeGlobal; 8 | 9 | app.put('/steadystate/start/', (req, res) => { 10 | startTimeGlobal = new Date(); 11 | res.sendStatus(200); 12 | }); 13 | 14 | app.get('/steadystate/stop/:baseline/:lowerVariance/:upperVariance/:resultsPerSecond/', (req, res) => { 15 | const stopTime = new Date(Date.now()); 16 | // if no start time then return client error 400 17 | if (!startTimeGlobal) return res.sendStatus(400); 18 | // send back results array based on user query params 19 | const baseline = parseInt(req.params.baseline, 10); 20 | const lowerVariance = parseInt(req.params.lowerVariance, 10); 21 | const upperVariance = parseInt(req.params.upperVariance, 10); 22 | const resultsPerSecond = parseInt(req.params.resultsPerSecond, 10); 23 | res.status(200); 24 | res.json(getResults( 25 | baseline, 26 | lowerVariance, 27 | upperVariance, 28 | startTimeGlobal, 29 | stopTime, 30 | resultsPerSecond, 31 | )); 32 | // reset start time so that we can do a clean start/stop cycle 33 | // on the next iteration of testing 34 | startTimeGlobal = undefined; 35 | return undefined; 36 | }); 37 | 38 | app.listen(PORT, () => { 39 | // eslint-disable-next-line no-console 40 | console.log(`📉 steady state service ready at localhost:${PORT}/steadystate/`); 41 | }); 42 | 43 | module.exports = app; 44 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/steady-state-server-stub/results.js: -------------------------------------------------------------------------------- 1 | const { MersenneTwister19937, integer } = require('random-js'); 2 | 3 | const getResults = ( 4 | baseline, 5 | lowerVariance, 6 | upperVariance, 7 | startDate, 8 | endDate, 9 | resultsPerSecond, 10 | ) => { 11 | // results container 12 | const results = []; 13 | 14 | // calculate min/max range via varience against baseline 15 | const lowerDelta = Math.floor(baseline * (lowerVariance / 100)); 16 | const upperDelta = Math.floor(baseline * (upperVariance / 100)); 17 | const min = baseline - lowerDelta; 18 | const max = baseline + upperDelta; 19 | 20 | // create a Mersenne Twister-19937 that is auto-seeded based on time and other random values 21 | const engine = MersenneTwister19937.autoSeed(); 22 | // create a distribution that will consistently produce integers within min/max 23 | const distribution = integer(min, max); 24 | // ms interval bewteen results 25 | const msBetweenResults = Math.floor(1000 / resultsPerSecond); 26 | 27 | // fake time that will increment during results production 28 | let resultMs = startDate.getTime(); 29 | const endMs = endDate.getTime(); 30 | // while the end time hasnt been reached add a new fake result 31 | while ((resultMs + msBetweenResults) <= endMs) { 32 | // new result time 33 | resultMs += msBetweenResults; 34 | results.push({ timeOfResult: (new Date(resultMs)).toJSON(), result: distribution(engine) }); 35 | } 36 | return results; 37 | }; 38 | 39 | module.exports = getResults; 40 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/platforms/express-graphql-app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const expressGraphQL = require('express-graphql'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const { buildSchema } = require('graphql'); 7 | 8 | module.exports = { 9 | gqlserver: undefined, 10 | 11 | port: 3000, 12 | 13 | chaosSocketServer: undefined, 14 | 15 | start() { 16 | // Create an express server and a GraphQL endpoint 17 | const app = express(); 18 | 19 | // 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 20 | const { chaos, chaosSocketServer } = require('chaosqoala-agent'); 21 | app.use(chaos); 22 | this.chaosSocketServer = chaosSocketServer; 23 | // 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 24 | 25 | // get gql test schema definition 26 | const pathToSchema = path.resolve(__dirname, '../gql-schema/schema.gql'); 27 | const schema = buildSchema(fs.readFileSync(pathToSchema, 'utf-8')); 28 | 29 | // Root resolver 30 | const root = { 31 | knockMeOut: () => 'I should be knocked out by Chaos QoaLa', 32 | dontKnockMeOut: () => 'I should NOT be knocked out by Chaos QoaLa', 33 | }; 34 | 35 | app.use('/graphql', expressGraphQL({ 36 | schema, 37 | rootValue: root, 38 | graphiql: true, 39 | })); 40 | 41 | this.gqlserver = app.listen(this.port, () => { 42 | // eslint-disable-next-line no-console 43 | console.log(`🚆 Express GraphQL Server ready at localhost:${this.port}/graphql`); 44 | }); 45 | }, 46 | 47 | stop() { 48 | // we need to close the chaos socket server for a clean exit 49 | this.chaosSocketServer.close(); 50 | // then close the gql server 51 | this.gqlserver.close(); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/platforms/apollo-server-express-app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | module.exports = { 8 | gqlserver: undefined, 9 | 10 | port: 4000, 11 | 12 | chaosSocketServer: undefined, 13 | 14 | start() { 15 | // get gql test schema definition 16 | const pathToSchema = path.resolve(__dirname, '../gql-schema/schema.gql'); 17 | const typeDefs = fs.readFileSync(pathToSchema, 'utf-8'); 18 | 19 | // Provide resolver functions for your schema fields 20 | const resolvers = { 21 | Query: { 22 | knockMeOut: () => 'I should be knocked out by Chaos QoaLa', 23 | dontKnockMeOut: () => 'I should NOT be knocked out by Chaos QoaLa', 24 | }, 25 | }; 26 | 27 | const apollo = new ApolloServer({ typeDefs, resolvers }); 28 | const app = express(); 29 | 30 | // 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 31 | const { chaos, chaosSocketServer } = require('chaosqoala-agent'); 32 | app.use(chaos); 33 | this.chaosSocketServer = chaosSocketServer; 34 | // 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 🐨 35 | 36 | apollo.applyMiddleware({ app }); // app is from an existing express app 37 | 38 | this.gqlserver = app.listen({ port: this.port }, () => { 39 | // eslint-disable-next-line no-console 40 | console.log(`🚀 Apollo Server ready at http://localhost:${this.port}${apollo.graphqlPath}`); 41 | }); 42 | }, 43 | 44 | stop() { 45 | // we need to close the chaos socket server for a clean exit 46 | this.chaosSocketServer.close(); 47 | // then close the gql server 48 | this.gqlserver.close(); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /cli/commands/configure.js: -------------------------------------------------------------------------------- 1 | const { prompt } = require('inquirer'); 2 | const { magentaBright } = require('chalk'); 3 | const { textSync } = require('figlet'); 4 | const QueryInspector = require('../util/QueryInspector'); 5 | 6 | // Requiring in the configure file from the same file 7 | const initalize = require('./initalize'); 8 | // importing the read and save functions from the initalize.js file 9 | const { read, save } = initalize; 10 | 11 | // declaring the questions and state objects 12 | let { questions } = read(); 13 | 14 | /* Declaring a variable and initialziing it to null. 15 | This will variable will be set to the results of 16 | the user answering the questions and it will be 17 | passed to the send function to connect to the socket */ 18 | 19 | questions = require('../questions/questions') 20 | .map(({ message, name }) => ({ 21 | message, 22 | // This will be the returned property on the package.JSON object 23 | name, 24 | })); 25 | 26 | function configure() { 27 | /* The below uses figlet and chalk to print ChaosQoala in 28 | the terminal when the user starts ChaosQoala */ 29 | console.log( 30 | magentaBright( 31 | textSync('ChaosQoaLa', { horizontalLayout: 'full' }), 32 | ), 33 | ); 34 | prompt(questions) 35 | .then(async (result) => { 36 | const { graphQLPort } = result; 37 | const inspector = new QueryInspector(graphQLPort); 38 | await inspector.hydrateQueryMap(); 39 | const affectedQueries = inspector.getQueryMap(); 40 | save({ ...result, affectedQueries }); 41 | console.log('Your config has been saved to package.json - to enable data knockout on a schema query set it\'s flag to true in the affectedQueries section of package.json'); 42 | }); 43 | } 44 | 45 | module.exports = { 46 | configure, 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/components/Instructions.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | function Instructions() { 5 | return ( 6 |
7 |
8 |
9 |

Planning Phase

10 |

Add steady state data feed and make baseline data available

11 |

Install and configure the agent on your GraphQL servers

12 |

Install the CLI on test users’ machines

13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |

Execution Phase

21 |

Run CLI to create a test setup

22 |

Start & stop the experiment

23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |

Upload & Analyze Results

31 |

Compare steady state data during "chaos" against baseline

32 |
33 |
34 | 38 |
39 |
40 | 41 | 42 | 43 | 46 |
47 | ); 48 | } 49 | 50 | export default Instructions; 51 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactcharts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "aws-sdk": "^2.522.0", 7 | "axios": "^0.19.0", 8 | "babel": "^6.23.0", 9 | "bluebird": "^3.5.5", 10 | "chart.js": "^2.8.0", 11 | "express": "^4.17.1", 12 | "file-type": "^12.3.0", 13 | "multiparty": "^4.2.1", 14 | "node-sass": "^4.12.0", 15 | "nodemon": "^1.19.2", 16 | "path": "^0.12.7", 17 | "react": "^16.9.0", 18 | "react-chartjs-2": "^2.7.6", 19 | "react-dom": "^16.9.0", 20 | "react-dropzone": "^10.1.8", 21 | "react-router": "^5.0.1", 22 | "react-router-dom": "^5.0.1", 23 | "react-scripts": "3.1.1", 24 | "webpack-dev-server": "^3.8.0" 25 | }, 26 | "scripts": { 27 | "start": "nodemon server/server.js", 28 | "build": "NODE_ENV=production webpack", 29 | "dev": "NODE_ENV=development nodemon server/server.js & webpack-dev-server --open", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.6.0", 50 | "@babel/preset-env": "^7.6.0", 51 | "babel-loader": "^8.0.6", 52 | "eslint": "^6.3.0", 53 | "eslint-config-airbnb": "^18.0.1", 54 | "eslint-plugin-import": "^2.18.2", 55 | "eslint-plugin-jsx-a11y": "^6.2.3", 56 | "eslint-plugin-react": "^7.14.3", 57 | "eslint-plugin-react-hooks": "^1.7.0", 58 | "style-loader": "^1.0.0", 59 | "webpack": "^4.39.3", 60 | "webpack-cli": "^3.3.8" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /aws/deploy-aws.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const chaosDeployer = require('./chaos-website-deployer'); 3 | const AdmZip = require('adm-zip'); 4 | 5 | const awsVars = require('./common-aws'); 6 | 7 | (async () => { 8 | try { 9 | await chaosDeployer.init(); 10 | 11 | const s3CreationParameters = [ 12 | { 13 | ParameterKey: 'chaosDomain', 14 | ParameterValue: awsVars.DOMAIN_NAME, 15 | }, 16 | { 17 | ParameterKey: 'chaosLambdaS3Bucket', 18 | ParameterValue: awsVars.LAMBDA_S3_BUCKET, 19 | }]; 20 | 21 | await chaosDeployer.createStack(awsVars.S3_STACK_NAME, './cloud-formation-s3.yml', s3CreationParameters); 22 | 23 | const indexContents = fs.readFileSync('../client/index.html', 'utf8'); 24 | const bundleContents = fs.readFileSync('../client/build/bundle.js', 'utf8'); 25 | await chaosDeployer.createObject(awsVars.DOMAIN_NAME, 'index.html', indexContents, {ContentType: "text/html", ACL: "public-read"}); 26 | await chaosDeployer.createObject(awsVars.DOMAIN_NAME, 'build/bundle.js', bundleContents, {ContentType: "text/javascript", ACL: "public-read"}); 27 | 28 | const zip = new AdmZip(); 29 | zip.addLocalFile('./lambda.js'); 30 | const lambdaZip = zip.toBuffer(); 31 | await chaosDeployer.createObject(awsVars.LAMBDA_S3_BUCKET, 'lambda.zip', lambdaZip); 32 | 33 | const apiCreationParameters = [ 34 | { ParameterKey: 'chaosLambdaS3Bucket', ParameterValue: awsVars.LAMBDA_S3_BUCKET,}, 35 | { ParameterKey: 'regionalCertificateArn', ParameterValue: awsVars.REGIONAL_CERTIFICATE_ARN,}, 36 | { ParameterKey: 'customHostedZoneId', ParameterValue: awsVars.CUSTOM_DOMAIN_HOSTED_ZONE_ID,}, 37 | { ParameterKey: 'regionHostedZoneId', ParameterValue: awsVars.REGION_HOSTED_ZONE_ID,}, 38 | { ParameterKey: 'chaosDomainApi', ParameterValue: awsVars.API_END_POINT,}, 39 | ]; 40 | await chaosDeployer.createStack(awsVars.API_STACK_NAME, './cloud-formation-lambda-api.yml', apiCreationParameters); 41 | 42 | } catch (err) { 43 | console.log(err); 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /client/src/components/FileUpload.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { useDropzone } from 'react-dropzone'; 3 | import Graphs from './Graphs.jsx'; 4 | 5 | function FileUpload() { 6 | const [data, setData] = useState(); 7 | const onDrop = useCallback((acceptedFiles) => { 8 | const reader = new FileReader(); 9 | 10 | reader.onabort = () => console.log('file reading was aborted'); 11 | reader.onerror = () => console.log('file reading has failed'); 12 | reader.onload = () => { 13 | // Do whatever you want with the file contents 14 | const binaryStr = reader.result; 15 | // aws lambda end point 16 | fetch('https://api.chaosqoala.io/transform/logs/', { 17 | method: 'POST', 18 | mode: 'cors', 19 | cache: 'no-cache', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: JSON.stringify(binaryStr), 24 | }) 25 | .then((response) => response.json()) 26 | .then((json) => setData(json)); 27 | }; 28 | 29 | acceptedFiles.forEach((file) => reader.readAsBinaryString(file)); 30 | }, []); 31 | 32 | // call back that is invoked when file is rejected 33 | const onDropRejected = useCallback((rejectedFile) => { 34 | console.log('Error uploading file - probably because it is not JSON'); 35 | }); 36 | 37 | // accept a single json file 38 | const accept = 'application/json'; 39 | const multiple = false; 40 | 41 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 42 | onDrop, onDropRejected, accept, multiple, 43 | }); 44 | return ( 45 |
46 |
47 | 48 | { 49 | isDragActive 50 | ?

Drop the files here ...

51 | :

Drag and drop some files here, or click to select files

52 | } 53 |
54 |
55 | {!data ? '' : } 56 |
57 |
58 | ); 59 | } 60 | 61 | export default FileUpload; 62 | -------------------------------------------------------------------------------- /client/server/server.js: -------------------------------------------------------------------------------- 1 | // const express = require("express"); 2 | // const app = express(); 3 | // const AWS = require("aws-sdk"); 4 | // const fs = require("fs"); 5 | // const fileType = require("file-type"); 6 | // const bluebird = require("bluebird"); 7 | // const multiparty = require("multiparty"); 8 | 9 | // // const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = require("../creds"); 10 | 11 | // // configure the keys for accessing AWS 12 | // AWS.config.update({ 13 | // accessKeyId: AWS_ACCESS_KEY_ID, 14 | // secretAccessKey: AWS_SECRET_ACCESS_KEY 15 | // }); 16 | 17 | // // configure AWS to work with promises 18 | // AWS.config.setPromisesDependency(bluebird); 19 | 20 | // // create S3 instance 21 | // const s3 = new AWS.S3(); 22 | 23 | // // abstracts function to upload a file returning a promise 24 | // const uploadFile = (buffer, name, type) => { 25 | // const params = { 26 | // ACL: "public-read", 27 | // Body: buffer, 28 | // Bucket: process.env.S3_BUCKET, 29 | // ContentType: type.mime, 30 | // Key: `${name}.${type.ext}` 31 | // }; 32 | // return s3.upload(params).promise(); 33 | // }; 34 | 35 | // // Define POST route 36 | // app.post("/test-upload", (request, response) => { 37 | // const form = new multiparty.Form(); 38 | // form.parse(request, async (error, fields, files) => { 39 | // if (error) throw new Error(error); 40 | // try { 41 | // const path = files.file[0].path; 42 | // const buffer = fs.readFileSync(path); 43 | // const type = fileType(buffer); 44 | // const timestamp = Date.now().toString(); 45 | // const fileName = `bucketFolder/${timestamp}-lg`; 46 | // const data = await uploadFile(buffer, fileName, type); 47 | // return response.status(200).send(data); 48 | // } catch (error) { 49 | // return response.status(400).send(error); 50 | // } 51 | // }); 52 | // }); 53 | 54 | // app.listen(PORT || 9000); 55 | // console.log("Server up and running..."); 56 | 57 | const express = require('express'); 58 | const app = express(); 59 | const path = require('path'); 60 | const fs = require('fs'); 61 | 62 | // serve index.html on the route '/' 63 | app.get('/', (req, res) => { 64 | res.sendFile(path.join(__dirname, '../index.html')); 65 | }); 66 | 67 | 68 | app.listen(3000); //listens on port 3000 -> http://localhost:3000/ 69 | 70 | 71 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/express.test.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const io = require('socket.io-client'); 3 | // express with chaos agent embedded 4 | const express = require('./platforms/express-graphql-app'); 5 | // import chaos config file - TODO: remove once controler has command line mode 6 | const config = require('../chaos-qoala-config'); 7 | // get express config 8 | const platform = express; 9 | 10 | let agentDataReceived = false; 11 | 12 | // increase default jest timeout as our tests intentionally add latency 13 | jest.setTimeout(30000); 14 | 15 | // start platform server before testing 16 | beforeAll((done) => { 17 | platform.start(); 18 | const socket = io.connect('http://localhost:1025'); 19 | // define event handler for sucessfull connection 20 | socket.on('connect', async () => { 21 | socket.emit('eucalyptus', config, () => { 22 | done(); 23 | }); 24 | }); 25 | // listen for incoming agent data sent during a chaos experiment 26 | socket.on('eucalyptus', (agentData) => { 27 | agentDataReceived = true; 28 | }); 29 | }); 30 | 31 | // test altering basic query response 32 | describe('Chaos 🐨 Proof of Concept', () => { 33 | // note the async so we can await fetch 34 | test('change response data', async () => { 35 | // construct url 36 | const url = `http://localhost:${platform.port}/graphql`; 37 | 38 | // gql query 39 | const query = { 40 | query: '{ dontKnockMeOut, knockMeOut }', 41 | variables: null, 42 | }; 43 | 44 | // start time 45 | const startTimeInMS = performance.now(); 46 | 47 | // note we await the fetch to stop jest moving on 48 | const response = await fetch(url, { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify(query), 54 | }); 55 | 56 | // expect the correct injection via chaos middleware 57 | const json = await response.json(); 58 | expect(json.data.dontKnockMeOut).toBe('I should NOT be knocked out by Chaos QoaLa'); 59 | expect(json.data.knockMeOut).toBe(undefined); 60 | 61 | // check time is within 3 seconds of the configured delay 62 | const THREE_SECONDS_IN_MS = 3000; 63 | const timeTakenInMS = performance.now() - startTimeInMS; 64 | expect(timeTakenInMS).toBeGreaterThan(config.delay); 65 | expect(timeTakenInMS).toBeLessThan(config.delay + THREE_SECONDS_IN_MS); 66 | expect(agentDataReceived).toBe(true); 67 | }); 68 | }); 69 | 70 | // stop platform servers at end of all tests 71 | afterAll(() => { 72 | platform.stop(); 73 | }); 74 | -------------------------------------------------------------------------------- /chaos-qoala-integration-tests/tests/apollo.test.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const io = require('socket.io-client'); 3 | // apollp with chaos agent embedded 4 | const apollo = require('./platforms/apollo-server-express-app'); 5 | // import chaos config file - TODO: remove once controler has command line mode 6 | const config = require('../chaos-qoala-config'); 7 | // get apollo config 8 | const platform = apollo; 9 | 10 | let agentDataReceived = false; 11 | 12 | // increase default jest timeout as our tests intentionally add latency 13 | jest.setTimeout(30000); 14 | 15 | // start platform server before testing 16 | beforeAll((done) => { 17 | platform.start(); 18 | const socket = io.connect('http://localhost:1025'); 19 | // define event handler for sucessfull connection 20 | socket.on('connect', () => { 21 | socket.emit('eucalyptus', config, () => { 22 | done(); 23 | }); 24 | }); 25 | // testing some code here (to see if agent data is received from index.js--->tests passing sucessfully) 26 | socket.on('eucalyptus', (agentData) => { 27 | agentDataReceived = true; 28 | }); 29 | }); 30 | 31 | // test altering basic query response 32 | describe('Chaos 🐨 Proof of Concept', () => { 33 | // note the async so we can await fetch 34 | test('change response data', async () => { 35 | // construct url 36 | const url = `http://localhost:${platform.port}/graphql`; 37 | 38 | // gql query 39 | const query = { 40 | query: '{ dontKnockMeOut, knockMeOut }', 41 | variables: null, 42 | }; 43 | 44 | // start time 45 | const startTimeInMS = performance.now(); 46 | 47 | // note we await the fetch to stop jest moving on 48 | const response = await fetch(url, { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify(query), 54 | }); 55 | 56 | // expect the correct injection via chaos middleware 57 | const json = await response.json(); 58 | expect(json.data.dontKnockMeOut).toBe('I should NOT be knocked out by Chaos QoaLa'); 59 | expect(json.data.knockMeOut).toBe(undefined); 60 | 61 | // check time is within 3 seconds of the configured delay 62 | const THREE_SECONDS_IN_MS = 3000; 63 | const timeTakenInMS = performance.now() - startTimeInMS; 64 | expect(timeTakenInMS).toBeGreaterThan(config.delay); 65 | expect(timeTakenInMS).toBeLessThan(config.delay + THREE_SECONDS_IN_MS); 66 | expect(agentDataReceived).toBe(true); 67 | }); 68 | }); 69 | 70 | // stop platform server at end of all tests 71 | afterAll(() => { 72 | platform.stop(); 73 | }); 74 | -------------------------------------------------------------------------------- /cli/commands/start.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, appendFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | const io = require('socket.io-client'); 4 | const fetch = require('node-fetch'); 5 | 6 | const ALL_AGENT_DATA = []; 7 | 8 | const packageJSON = join(__dirname, '..', 'package.json'); 9 | 10 | /* The below function reads the package.json file and grabs the state 11 | and opens a socket.io connection to send the configure information to the agent */ 12 | async function start() { 13 | // console.log(state); 14 | const myPackageJSON = readFileSync(packageJSON, 'utf8'); 15 | const parsedMyPackageJSON = JSON.parse(myPackageJSON); 16 | const { state } = parsedMyPackageJSON.chaosQoala; 17 | // Establishes a ensueChaos property on the state that will be set to 18 | state.ensueChaos = true; 19 | // creates result file with current time stamp. Appends file with chaos config from state 20 | const fileTimestamp = new Date().toJSON(); 21 | 22 | // Retrieves the user's socket information from the package.json 23 | const { socketPort } = state; 24 | const socket = io.connect(socketPort); 25 | socket.on('connect', () => { 26 | socket.emit('eucalyptus', state, () => { 27 | console.log('GraphQL server received config'); 28 | }); 29 | }); 30 | 31 | socket.on('eucalyptus', (agentData) => { 32 | ALL_AGENT_DATA.push(agentData); 33 | }); 34 | 35 | const { 36 | steadyStateStartURL, steadyStateStartHTTPVerb, steadyStateStopURL, steadyStateStopHTTPVerb, 37 | } = state; 38 | 39 | await fetch(steadyStateStartURL, { 40 | method: steadyStateStartHTTPVerb, 41 | }) 42 | .then(() => { console.log('steady state monitoring service started successfully'); }) 43 | .catch((err) => { console.log(`steady steady monitoring service failed to start: ${err}`); }); 44 | 45 | const keypress = async () => { 46 | process.stdin.setRawMode(true); 47 | return new Promise(resolve => process.stdin.once('data', () => { 48 | process.stdin.setRawMode(false); 49 | resolve(); 50 | })); 51 | }; 52 | 53 | (async () => { 54 | console.log('Press any key to stop chaos'); 55 | await keypress(); 56 | await fetch(steadyStateStopURL, { method: steadyStateStopHTTPVerb }) 57 | .then(response => response.json()) 58 | .then((chaosResults) => { 59 | appendFileSync( 60 | `${fileTimestamp}_results.json`, 61 | JSON.stringify({ chaosConfig: state, chaosResults, agentData: ALL_AGENT_DATA }, null, 2), 62 | ); 63 | }); 64 | // ensueChaos flag changed to false when any key pressed 65 | state.ensueChaos = false; 66 | socket.emit('eucalyptus', state, () => { 67 | console.log('chaos ended'); 68 | }); 69 | })().then(process.exit); 70 | } 71 | 72 | 73 | module.exports = { 74 | start, 75 | }; 76 | -------------------------------------------------------------------------------- /client/src/components/ThisIsUs.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | 3 | function ThisIsUs() { 4 | return ( 5 |
6 |
7 |

Meet The Team

8 |
9 |
10 |
11 |
Jacob Ory
12 |
13 | 21 |
22 |
23 |
Nicolas Venegas Parker
24 |
25 | 33 |
34 |
35 |
Samantha Wessel
36 |
37 | 45 |
46 |
47 |
Simon Maharai
48 |
49 | 57 |
58 |
59 |
60 |

Want to contribute?

61 |
ChaosQoala is an open source project
62 |
63 | 66 |
© 2019 ChaosQoala/OSLabs. All rights reserved
67 |
68 | ); 69 | } 70 | 71 | export default ThisIsUs; 72 | -------------------------------------------------------------------------------- /client/scss/_navbar.scss: -------------------------------------------------------------------------------- 1 | @mixin for-phone-only { 2 | @media (max-width: 599px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin for-tablet-portrait-up { 8 | @media (min-width: 600px) and (max-width: 924px) { 9 | @content; 10 | } 11 | } 12 | @mixin for-tablet-landscape-up { 13 | @media (min-width: 925px) { 14 | @content; 15 | } 16 | } 17 | @mixin for-desktop-up { 18 | @media (min-width: 1200px) { 19 | @content; 20 | } 21 | } 22 | @mixin for-big-desktop-up { 23 | @media (min-width: 1800px) { 24 | @content; 25 | } 26 | } 27 | 28 | #navbar { 29 | background-color: #171c1fe8; 30 | position: fixed; 31 | top: 0; 32 | left: 0; 33 | overflow-x: hidden; 34 | min-width: 100vw; 35 | display: flex; 36 | margin-left: auto; 37 | margin-right: auto; 38 | grid-template-columns: 1fr 15rem 15rem; 39 | justify-content: space-between; 40 | height: 3rem; 41 | align-items: center; 42 | padding: 0 10px; 43 | color: rgba(117, 117, 117, 1); 44 | box-shadow: 5px 10px 20px -20px white; 45 | font-family: "Manjari"; 46 | } 47 | 48 | #navBarQL { 49 | height: 2.5rem; 50 | margin-left: 1.3rem; 51 | @include for-phone-only { 52 | height: 2rem; 53 | } 54 | @include for-tablet-portrait-up { 55 | height: 2.3rem; 56 | } 57 | } 58 | 59 | #navbarupload { 60 | box-shadow: inset 0px 1px 0px 0px #ffffff; 61 | background: white; 62 | border: 1px solid #dcdcdc; 63 | border-radius: 5px; 64 | display: inline-block; 65 | color: #666666; 66 | font-weight: bold; 67 | padding: 0.2rem 0.8rem; 68 | text-decoration: none; 69 | @include for-phone-only { 70 | height: 1rem; 71 | border-radius: 3px; 72 | margin-right: 0.5em; 73 | font-size: 0.9em; 74 | padding: 0.1em; 75 | } 76 | 77 | @include for-tablet-portrait-up { 78 | height: 1rem; 79 | border-radius: 3px; 80 | margin-right: 0.5em; 81 | font-size: 0.9em; 82 | padding: 0.1em; 83 | } 84 | 85 | @include for-tablet-landscape-up { 86 | margin-right: 0.5rem; 87 | } 88 | } 89 | #navbarupload:hover { 90 | color: #e31e98; 91 | } 92 | 93 | #navbardownload { 94 | box-shadow: inset 0px 1px 0px 0px #ffffff; 95 | background: white; 96 | border: 1px solid #dcdcdc; 97 | border-radius: 5px; 98 | display: inline-block; 99 | color: #666666; 100 | font-weight: bold; 101 | padding: 0.2rem 0.8rem; 102 | text-decoration: none; 103 | @include for-phone-only { 104 | height: 1rem; 105 | border-radius: 3px; 106 | margin-right: 0.5em; 107 | font-size: 0.9em; 108 | padding: 0.1em; 109 | } 110 | @include for-tablet-portrait-up { 111 | height: 1rem; 112 | border-radius: 3px; 113 | margin-right: 0.5em; 114 | font-size: 0.9em; 115 | padding: 0.1em; 116 | } 117 | } 118 | #navbardownload:hover { 119 | color: #e31e98; 120 | } 121 | 122 | #rightnav { 123 | margin-right: 1.3rem; 124 | } 125 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /aws/cloud-formation-s3.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | chaosLambdaS3Bucket: 4 | Description: Name of the S3 lambda bucket 5 | Type: String 6 | chaosDomain: 7 | Description: Name of the domain to host on i.e example.com 8 | Type: String 9 | Mappings: 10 | RegionMap: 11 | us-east-1: 12 | S3hostedzoneID: Z3AQBSTGFYJSTF 13 | websiteendpoint: s3-website-us-east-1.amazonaws.com 14 | us-west-1: 15 | S3hostedzoneID: Z2F56UZL2M1ACD 16 | websiteendpoint: s3-website-us-west-1.amazonaws.com 17 | us-west-2: 18 | S3hostedzoneID: Z3BJ6K6RIION7M 19 | websiteendpoint: s3-website-us-west-2.amazonaws.com 20 | eu-west-1: 21 | S3hostedzoneID: Z1BKCTXD74EZPE 22 | websiteendpoint: s3-website-eu-west-1.amazonaws.com 23 | ap-southeast-1: 24 | S3hostedzoneID: Z3O0J2DXBE1FTB 25 | websiteendpoint: s3-website-ap-southeast-1.amazonaws.com 26 | ap-southeast-2: 27 | S3hostedzoneID: Z1WCIGYICN2BYD 28 | websiteendpoint: s3-website-ap-southeast-2.amazonaws.com 29 | ap-northeast-1: 30 | S3hostedzoneID: Z2M4EHUR26P7ZW 31 | websiteendpoint: s3-website-ap-northeast-1.amazonaws.com 32 | sa-east-1: 33 | S3hostedzoneID: Z31GFT0UA1I2HV 34 | websiteendpoint: s3-website-sa-east-1.amazonaws.com 35 | us-east-2: 36 | S3hostedzoneID: Z2O1EMRO9K5GLX 37 | websiteendpoint: s3-website.us-east-2.amazonaws.com 38 | Resources: 39 | S3LambdaBucket: 40 | Type: AWS::S3::Bucket 41 | Properties: 42 | BucketName: 43 | # refer to chaos-website-deployer code to work out how this param is passed 44 | Ref: chaosLambdaS3Bucket 45 | PublicAccessBlockConfiguration: 46 | BlockPublicAcls: true 47 | BlockPublicPolicy: true 48 | IgnorePublicAcls: true 49 | RestrictPublicBuckets: true 50 | RootBucket: 51 | Type: AWS::S3::Bucket 52 | Properties: 53 | BucketName: 54 | Ref: chaosDomain 55 | AccessControl: PublicRead 56 | WebsiteConfiguration: 57 | IndexDocument: index.html 58 | ErrorDocument: 404.html 59 | WWWBucket: 60 | Type: AWS::S3::Bucket 61 | Properties: 62 | BucketName: !Sub 63 | - www.${Domain} 64 | - Domain: !Ref chaosDomain 65 | AccessControl: BucketOwnerFullControl 66 | WebsiteConfiguration: 67 | RedirectAllRequestsTo: 68 | HostName: !Ref RootBucket 69 | myDNS: 70 | Type: AWS::Route53::RecordSetGroup 71 | Properties: 72 | HostedZoneName: !Sub 73 | - ${Domain}. 74 | - Domain: !Ref chaosDomain 75 | Comment: Zone apex alias. 76 | RecordSets: 77 | - 78 | Name: !Sub 79 | - www.${Domain} 80 | - Domain: !Ref chaosDomain 81 | Type: A 82 | AliasTarget: 83 | HostedZoneId: !FindInMap [ RegionMap, !Ref 'AWS::Region', S3hostedzoneID] 84 | DNSName: !FindInMap [ RegionMap, !Ref 'AWS::Region', websiteendpoint] 85 | - 86 | Name: !Ref chaosDomain 87 | Type: A 88 | AliasTarget: 89 | HostedZoneId: !FindInMap [ RegionMap, !Ref 'AWS::Region', S3hostedzoneID] 90 | DNSName: !FindInMap [ RegionMap, !Ref 'AWS::Region', websiteendpoint] 91 | 92 | Outputs: 93 | WebsiteURL: 94 | Value: !GetAtt RootBucket.WebsiteURL 95 | Description: URL for website hosted on S3 -------------------------------------------------------------------------------- /client/scss/_thisisus.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | } 5 | 6 | @mixin for-phone-only { 7 | @media (max-width: 599px) { 8 | @content; 9 | } 10 | } 11 | 12 | @mixin for-tablet-portrait-up { 13 | @media (min-width: 600px) and (max-width: 924px) { 14 | @content; 15 | } 16 | } 17 | @mixin for-tablet-landscape-up { 18 | @media (min-width: 925px) { 19 | @content; 20 | } 21 | } 22 | @mixin for-desktop-up { 23 | @media (min-width: 1200px) { 24 | @content; 25 | } 26 | } 27 | @mixin for-big-desktop-up { 28 | @media (min-width: 1800px) { 29 | @content; 30 | } 31 | } 32 | 33 | #thisisus { 34 | background: linear-gradient(to top left, #e31e98 -34%, #827c82 47%); 35 | display: flex; 36 | flex-direction: column; 37 | min-height: 50vh; 38 | align-items: center; 39 | justify-content: space-evenly; 40 | font-family: "Manjari"; 41 | 42 | #meettheteam { 43 | color: #f8f8ff; 44 | display: flex; 45 | margin-bottom: 2rem; 46 | } 47 | 48 | #bios { 49 | display: flex; 50 | width: 75%; 51 | align-items: center; 52 | justify-content: space-between; 53 | @include for-phone-only { 54 | width: 100%; 55 | flex-direction: column; 56 | flex-wrap: wrap; 57 | flex: 0 0 50%; 58 | // border: 1px solid green; 59 | } 60 | @include for-tablet-portrait-up { 61 | width: 100%; 62 | flex-direction: column; 63 | flex-wrap: wrap; 64 | flex: 0 0 50%; 65 | } 66 | } 67 | 68 | .whoami { 69 | display: flex; 70 | flex-direction: column; 71 | align-items: center; 72 | width: -webkit-fill-available; 73 | @include for-phone-only { 74 | width: 25%; 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | flex-basis: 25%; 79 | margin-bottom: 2rem; 80 | // border: 1px solid salmon; 81 | } 82 | @include for-tablet-portrait-up { 83 | width: 25%; 84 | display: flex; 85 | justify-content: center; 86 | align-items: center; 87 | flex-basis: 25%; 88 | margin-bottom: 2rem; 89 | // justify-content: space-evenly; 90 | } 91 | } 92 | 93 | h6 { 94 | color: #f8f8ff; 95 | margin: auto 0; 96 | font-size: 1rem; 97 | // border: 1px solid purple; 98 | text-align: center; 99 | } 100 | 101 | hr { 102 | display: block; 103 | color: #f8f8ff; 104 | width: 8rem; 105 | } 106 | .outsidelinks { 107 | display: flex; 108 | width: 5rem; 109 | justify-content: space-between; 110 | } 111 | } 112 | 113 | #opensource { 114 | display: flex; 115 | flex-direction: column; 116 | flex-wrap: wrap; 117 | justify-content: center; 118 | align-items: center; 119 | margin-top: 3rem; 120 | 121 | p { 122 | font-size: 2rem; 123 | font-weight: 900; 124 | color: #f8f8ff; 125 | margin-bottom: 0.5rem; 126 | } 127 | 128 | h6 { 129 | color: #f8f8ff; 130 | } 131 | } 132 | 133 | #finalgitlink { 134 | display: inline-block; 135 | font-weight: 400; 136 | color: #f8f8ff; 137 | text-align: center; 138 | vertical-align: middle; 139 | background-color: transparent; 140 | border: 1px solid #f8f8ff; 141 | font-size: 1rem; 142 | line-height: 1.5; 143 | width: 7em; 144 | border-radius: 0.25rem; 145 | margin-top: 2rem; 146 | margin-bottom: 2rem; 147 | 148 | @include for-phone-only { 149 | display: inline-block; 150 | font-weight: 400; 151 | color: #f8f8ff; 152 | text-align: center; 153 | vertical-align: middle; 154 | background-color: transparent; 155 | border: 1px solid #f8f8ff; 156 | padding: 0.375rem 0.75rem; 157 | font-size: 1rem; 158 | line-height: 1.5; 159 | border-radius: 0.25rem; 160 | margin-top: 2rem; 161 | margin-bottom: 2rem; 162 | } 163 | } 164 | 165 | #finalgitlink:hover { 166 | color: black; 167 | background-color: white; 168 | } 169 | -------------------------------------------------------------------------------- /client/scss/_instructions.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | @mixin for-phone-to-tablet { 8 | @media (max-width: 924px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin for-tablet-landscape-up { 14 | @media (min-width: 925px) { 15 | @content; 16 | } 17 | } 18 | @mixin for-desktop-up { 19 | @media (min-width: 1200px) { 20 | @content; 21 | } 22 | } 23 | @mixin for-big-desktop-up { 24 | @media (min-width: 1800px) { 25 | @content; 26 | } 27 | } 28 | 29 | #instructions { 30 | margin-top: 0; 31 | margin-left: 3rem; 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: center; 35 | align-items: center; 36 | font-family: "Manjari"; 37 | 38 | #steponeparent { 39 | display: flex; 40 | min-width: 100%; 41 | justify-content: space-between; 42 | margin-top: 4rem; 43 | margin-bottom: 5rem; 44 | 45 | @include for-phone-to-tablet { 46 | display: block; 47 | float: none; 48 | width: 100%; 49 | 50 | img { 51 | width: 100%; 52 | } 53 | } 54 | } 55 | 56 | #stepone { 57 | align-self: flex-start; 58 | } 59 | 60 | #planningphase { 61 | margin-right: 4rem; 62 | } 63 | 64 | #steptwoparent { 65 | display: flex; 66 | flex-direction: row-reverse; 67 | min-width: 100%; 68 | justify-content: space-between; 69 | margin-bottom: 5rem; 70 | @include for-phone-to-tablet { 71 | display: block; 72 | float: none; 73 | width: 100%; 74 | img { 75 | width: 90%; 76 | } 77 | } 78 | } 79 | 80 | #steptwo { 81 | align-self: flex-start; 82 | margin-right: 8rem; 83 | } 84 | 85 | #stepthreeparent { 86 | display: flex; 87 | min-width: 100%; 88 | justify-content: space-around; 89 | // margin-left: 7rem; 90 | margin-bottom: 5rem; 91 | // margin-right: 10rem; 92 | @include for-phone-to-tablet { 93 | display: block; 94 | float: none; 95 | width: 100%; 96 | img { 97 | width: 90%; 98 | height: auto; 99 | } 100 | } 101 | } 102 | 103 | #stepthree { 104 | align-self: flex-start; 105 | padding-top: 5rem; 106 | } 107 | 108 | #download { 109 | display: inline-block; 110 | font-weight: 400; 111 | color: black; 112 | text-align: center; 113 | vertical-align: middle; 114 | background-color: transparent; 115 | border: 1px solid black; 116 | padding: 0.5rem 1rem; 117 | font-size: 1rem; 118 | line-height: 1.5; 119 | width: auto; 120 | border-radius: 0.25rem; 121 | margin-top: 3rem; 122 | margin-bottom: 1rem; 123 | 124 | @include for-phone-to-tablet { 125 | display: inline-block; 126 | font-weight: 400; 127 | color: black; 128 | text-align: center; 129 | vertical-align: middle; 130 | background-color: transparent; 131 | border: 1px solid black; 132 | padding: 0.375rem 0.75rem; 133 | font-size: 1rem; 134 | line-height: 1.5; 135 | border-radius: 0.25rem; 136 | } 137 | } 138 | 139 | #download:hover { 140 | color: #e31e98; 141 | background-color: black; 142 | } 143 | 144 | #uploadlink { 145 | display: inline-block; 146 | font-weight: 400; 147 | color: fuchsia; 148 | text-align: center; 149 | vertical-align: middle; 150 | background-color: transparent; 151 | border: 1px solid black; 152 | padding: 0.5rem 1rem; 153 | font-size: 1rem; 154 | line-height: 1.5; 155 | width: auto; 156 | border-radius: 0.25rem; 157 | margin-top: 1rem; 158 | margin-bottom: 3rem; 159 | 160 | @include for-phone-to-tablet { 161 | display: inline-block; 162 | font-weight: 400; 163 | color: black; 164 | text-align: center; 165 | vertical-align: middle; 166 | background-color: transparent; 167 | border: 1px solid black; 168 | padding: 0.375rem 0.75rem; 169 | font-size: 1rem; 170 | line-height: 1.5; 171 | border-radius: 0.25rem; 172 | } 173 | 174 | a { 175 | text-decoration: none; 176 | color: black; 177 | } 178 | } 179 | 180 | #uploadlink:hover { 181 | background-color: grey; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /aws/chaos-website-deployer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const AWS = require('aws-sdk'); 3 | require('dotenv').config(); 4 | 5 | module.exports = { 6 | init: () => new Promise((resolve, reject) => { 7 | AWS.config.getCredentials((err) => { 8 | if (err) { 9 | reject(err.stack); 10 | } else { 11 | console.log('Access key:', AWS.config.credentials.accessKeyId); 12 | console.log('Secret access key:', AWS.config.credentials.secretAccessKey); 13 | console.log('Region', process.env.AWS_REGION); 14 | resolve(); 15 | } 16 | }); 17 | }), 18 | stackExists: (name) => new Promise((resolve, reject) => { 19 | const cloudformation = new AWS.CloudFormation({ apiVersion: '2010-05-15' }); 20 | const currentStacksQueryParams = { StackStatusFilter: ['CREATE_COMPLETE', 'CREATE_IN_PROGRESS', 'DELETE_FAILED', 'ROLLBACK_COMPLETE'] }; 21 | cloudformation.listStacks(currentStacksQueryParams, (err, data) => { 22 | if (err) { 23 | reject(err.stack); // an error occurred 24 | } else { 25 | for (let i = 0; i < data.StackSummaries.length; i += 1) { 26 | if (data.StackSummaries[i].StackName === name) { 27 | resolve(true); 28 | } 29 | } 30 | resolve(false); 31 | } 32 | }); 33 | }), 34 | createStack: (name, templatePath, templateParams = []) => new Promise((resolve, reject) => { 35 | const template = fs.readFileSync(templatePath, 'utf8'); 36 | const cloudformation = new AWS.CloudFormation({ apiVersion: '2010-05-15' }); 37 | 38 | const createParams = { 39 | StackName: name, 40 | TemplateBody: template, 41 | Capabilities: ['CAPABILITY_IAM'], 42 | Parameters: templateParams, 43 | }; 44 | 45 | cloudformation.createStack(createParams, (err) => { 46 | if (err) { 47 | reject(new Error(`FAILED: to create stack "${name}"\n${err.stack}`)); 48 | } else { 49 | console.log(`--waiting-- while creation of stack ${name} completes`); 50 | cloudformation.waitFor('stackCreateComplete', { StackName: createParams.StackName }, (err, data) => { 51 | if (err) { 52 | reject(new Error(`--FAILED-- to create stack "${name}"\n${err.stack}`)); 53 | } else { 54 | console.log(`--success-- created stack "${name}"`); 55 | resolve(); 56 | } 57 | }); 58 | } 59 | }); 60 | }), 61 | createObject: (bucketName, objectName, objectData, options = {}) => new Promise((resolve, reject) => { 62 | const s3 = new AWS.S3(); 63 | const createObjParams = { 64 | Body: objectData, 65 | Bucket: bucketName, 66 | Key: objectName, 67 | ...options 68 | }; 69 | s3.putObject(createObjParams, (err, data) => { 70 | if (err) { 71 | reject(new Error(`--FAILED-- to create object "${objectName}"\n${err.stack}`)); 72 | } else { 73 | console.log(`--success-- created object "${objectName}"`); 74 | resolve(); 75 | } 76 | }); 77 | }), 78 | deleteStack: (name) => new Promise((resolve, reject) => { 79 | const cloudformation = new AWS.CloudFormation({ apiVersion: '2010-05-15' }); 80 | const deleteParams = { 81 | StackName: name, 82 | }; 83 | cloudformation.deleteStack(deleteParams, (err) => { 84 | if (err) { 85 | reject(new Error(`--FAILED-- to delete stack "${name}"\n${err.stack}`)); 86 | } else { 87 | console.log(`--waiting-- while deletion of stack ${name} completes`); 88 | cloudformation.waitFor('stackDeleteComplete', deleteParams, (err, data) => { 89 | if (err) { 90 | reject(new Error(`--FAILED-- to delete stack "${name}"\n${err.stack}`)); 91 | } else { 92 | console.log(`--success-- deleted stack "${name}"`); 93 | resolve(); 94 | } 95 | }); 96 | } 97 | }); 98 | }), 99 | deleteS3Objects: (bucketName) => new Promise((resolve, reject) => { 100 | const s3 = new AWS.S3(); 101 | 102 | s3.listBuckets((err, data) => { 103 | if (err) { 104 | reject(new Error(`--FAILED-- to check if bucket "${bucketName}" exists\n${err.stack}`)); 105 | } else { 106 | const namesOfBucketsThatExist = data.Buckets.map((element) => element.Name === bucketName); 107 | if (!namesOfBucketsThatExist.includes(bucketName)) resolve(); 108 | const listObjectsParams = { 109 | Bucket: bucketName, 110 | }; 111 | 112 | s3.listObjectsV2(listObjectsParams, (err, data) => { 113 | if (err) { 114 | reject(new Error(`--FAILED-- to get object keys for bucket "${bucketName}"\n${err.stack}`)); 115 | } else { 116 | const s3BucketKeys = data.Contents.map((element) => ({ Key: element.Key })); 117 | const deleteObjectsParams = { 118 | Bucket: bucketName, 119 | Delete: { 120 | Objects: s3BucketKeys, 121 | Quiet: true, 122 | }, 123 | }; 124 | 125 | s3.deleteObjects(deleteObjectsParams, (err) => { 126 | if (err) { 127 | reject(new Error(`--FAILED-- to delete objects for bucket "${bucketName}"\n${err.stack}`)); 128 | } else { 129 | console.log(`--success-- deleted objects for bucket "${bucketName}"`); 130 | resolve(); 131 | } 132 | }); 133 | } 134 | }); 135 | } 136 | }); 137 | }), 138 | }; 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Chaos QoaLa** 2 | ChaosQoaLa is a chaos engineering tool for injecting failure into JavaScript backed GraphQL end points. ChaosQoaLa can be used to add latency, and/or knock out specific data sections of a GraphQL server's responses. The extent of the chaos effect can be controlled via a "blast radius" configuration parameter so only a specified portion of responses are affected. There are three fundamental components. The first component is a piece of named middleware you will integrate into your GraphQL server implementation. This "**Agent**" sits in the server and will cause chaos on demand when it is sent instructions to do so. The "**Controller**" is a CLI tool which is installed on a test user's personal machine/VM and can be used to configure, run, and stop a chaos experiment. The "**Site**" (www.chaosqoala.io) can be used to upload results files 3 | generated by the Controller for visualization. 4 | 5 | ## **Supported GraphQL Servers** 6 | 7 | - Express GraphQL 8 | - Apollo Server Express 9 | 10 | ## **Installation** 11 | 12 | **Agent** 13 | 14 | 15 | # npm install chaosqoaloa-agent 16 | Once installed in your server's codebase the agent can be integrated in 3 lines of code (between the koalas). 17 | 18 | > Express Graph QL example: 19 | 20 | ![enter image description here](https://cqlimage.s3-us-west-1.amazonaws.com/Express+Server.png) 21 | 22 | > Apollo Server Express example: 23 | 24 | ![enter image description here](https://cqlimage.s3-us-west-1.amazonaws.com/Apollo+Server.png) 25 | 26 | 27 | 28 | **Controller** 29 | 30 | 31 | Install on each test users' machine. 32 | 33 | # npm install chaosqoaloa 34 | 35 | 36 | ## **Steady State Integration** 37 | A key metric in chaos engineering experiments is 'steady state'. Steady state is a measure of how well a system is performing. Different systems will be measured by different metrics, for example Netflix uses 'number of plays' to evaluate how well their systems or service is performing. ChaosQoaLa requires access to an endpoint that can be called when the chaos experiment starts, and again when the experiment ends. The end point must be stateful and respond to the stop invocation with steady state data points for the duration of the test run. The results are expected to be in a JSON array in the format: 38 | 39 | [ { "timeOfResult": "2019-09-10T04:34:22.290Z", "result": 97 }, 40 | { "timeOfResult": "2019-09-10T04:34:22.390Z", "result": 79 }, 41 | {.... ] 42 | 43 | ## **Using ChaosQoaLa** 44 | 45 | **Configuring a test run** 46 | 47 | 48 | To configure a test run enter the following command on a user's machine with the Controller installed 49 | 50 | # chaosqoala configure 51 | The CLI tool will display a series of questions: 52 | 53 | Please enter the URI of the Socket.io port over which Chaos will be sent and received 54 | By default the Agent listens on port 1025 of the GraphQL server in which it has been installed. Enter the fully qualified path of port 1025 on the GraphQL server, for example https://gql.example.com:1025 55 | 56 | Please enter the URI of GraphQL service 57 | Enter the fully qualified path of the GraphQL server, for example https://gql.example.com 58 | 59 | Please enter your desired blast radius 60 | Enter a number between 0.0 and 1.0 to represent the % of responses that will be affected by chaos, for example a figure of 0.33 will result in 33% of responses from the server being subjected to failure injection. 61 | 62 | Please enter the amount of time you would like your data to be delayed (in milliseconds) 63 | Enter the number of milliseconds of latency to introduce to responses chosen for failure injection, use a value of zero in order to switch off latency injection. 64 | 65 | Please enter how long you would like the ChaosQoala to run (in minutes). 66 | Enter the duration of the test run in minutes. Test runs can be terminated at any point by pressing any key in the Controller CLI during test execution. 67 | 68 | Please enter the steady state start URL 69 | 70 | Enter the fully qualified URI for the steady state service you have implemented, this URI will be invoked when the chaos experiment is started. For example: https://api.example.com/steadystate/start 71 | 72 | Please enter the steady state start HTTP verb 73 | Enter the http verb used to invoke the steady state start route, GET/POST/PUT etc. 74 | 75 | Please enter the steady state stop URL 76 | Enter the fully qualified URI for the steady state service you have implemented, this URI will invoked when the chaos experiment is stopped. For example: https://api.example.com/steadystate/stop 77 | 78 | Please enter the steady state stop HTTP verb 79 | Enter the http verb used to invoke the steady state stop route, eg GET/POST/PUT etc. 80 | 81 | **Configuring query knockout** 82 | 83 | 84 | Once configure has been ran the Controller will write the configuration to its package.json. The Controller also inspects the GraphQL end point entered and extracts a list of available queries. To knock out data for a GraphQL query during an experiment just toggle the booleans in the affectedQueries object of the package.json: 85 | 86 | "chaosConfig": { 87 | . . . 88 | "affectedQueries": { 89 | "dontKnockMeOut": false, 90 | "knockMeOut": true 91 | }, 92 | }, 93 | **Running an experiment** 94 | 95 | 96 | Once you are happy with the configuration the experiment can be started with the start command 97 | 98 | # chaosqoala start 99 | When the experiment ends a results file will be generated on the machine the Controller is running on - this file (*timestamp*_results.json) can be visualized on the ChaosQoaLa site via the upload link. 100 | ## **License** 101 | This project is licensed under the MIT License. 102 | 103 | ## **Authors** 104 | - Jacob Ory: github.com/jakeory 105 | - Simon Maharai: github.com/Simon-IM 106 | - Nicolas Venegas Parker: github.com/nicvhub 107 | - Samantha Wessel: github.com/sw8wm2013 108 | ## **Acknowledgements** 109 | Huge thanks to Natalie Klein [linkedin.com/in/nataliesklein](https://www.linkedin.com/in/nataliesklein) for all the input, advice, and mentorship 110 | -------------------------------------------------------------------------------- /aws/cloud-formation-lambda-api.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Lambda function to support ChaosQoala Website Data analysis and translation to Chart js data structure" 3 | 4 | Parameters: 5 | chaosLambdaS3Bucket: 6 | Description: The name of the bucket to store the lambda function code during deployment 7 | Type: String 8 | regionalCertificateArn: 9 | Description: The certificate generated from ACM for you custom hosting domain (example.com) 10 | Type: String 11 | customHostedZoneId: 12 | Description: The hosted zone id for you custom hosting domain (example.com) 13 | Type: String 14 | regionHostedZoneId: 15 | Description: The AWS region hosted zone id 16 | Type: String 17 | chaosDomainApi: 18 | Description: Name of the lambda end point i.e api.example.com 19 | Type: String 20 | 21 | Resources: 22 | customDomainForAPI: 23 | Type: AWS::ApiGateway::DomainName 24 | Properties: 25 | DomainName: !Ref chaosDomainApi 26 | EndpointConfiguration: 27 | Types: 28 | - REGIONAL 29 | RegionalCertificateArn: 30 | Ref: regionalCertificateArn 31 | 32 | apiGateway: 33 | Type: "AWS::ApiGateway::RestApi" 34 | Properties: 35 | Name: "chaos-qoala-api" 36 | Description: "Chaos Qoala API" 37 | EndpointConfiguration: 38 | Types: 39 | - REGIONAL 40 | 41 | apiGatewayRootMethod: 42 | Type: "AWS::ApiGateway::Method" 43 | Properties: 44 | AuthorizationType: "NONE" 45 | HttpMethod: "POST" 46 | Integration: 47 | IntegrationHttpMethod: "POST" 48 | Type: "AWS_PROXY" 49 | Uri: !Sub 50 | - "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations" 51 | - lambdaArn: !GetAtt "lambdaFunction.Arn" 52 | ResourceId: !GetAtt "apiGateway.RootResourceId" 53 | RestApiId: !Ref "apiGateway" 54 | 55 | OptionsMethod: 56 | Type: AWS::ApiGateway::Method 57 | Properties: 58 | AuthorizationType: NONE 59 | RestApiId: !Ref "apiGateway" 60 | ResourceId: !GetAtt "apiGateway.RootResourceId" 61 | HttpMethod: OPTIONS 62 | Integration: 63 | IntegrationResponses: 64 | - StatusCode: 200 65 | ResponseParameters: 66 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" 67 | method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" 68 | method.response.header.Access-Control-Allow-Origin: "'*'" 69 | ResponseTemplates: 70 | application/json: '' 71 | PassthroughBehavior: WHEN_NO_MATCH 72 | RequestTemplates: 73 | application/json: '{"statusCode": 200}' 74 | Type: MOCK 75 | MethodResponses: 76 | - StatusCode: 200 77 | ResponseModels: 78 | application/json: 'Empty' 79 | ResponseParameters: 80 | method.response.header.Access-Control-Allow-Headers: true 81 | method.response.header.Access-Control-Allow-Methods: true 82 | method.response.header.Access-Control-Allow-Origin: true 83 | 84 | apiGatewayDeployment: 85 | Type: "AWS::ApiGateway::Deployment" 86 | DependsOn: 87 | - "apiGatewayRootMethod" 88 | Properties: 89 | RestApiId: !Ref "apiGateway" 90 | StageName: logs 91 | 92 | mappingCustomDomainToChaosAPI: 93 | Type: AWS::ApiGateway::BasePathMapping 94 | DependsOn: 95 | - "customDomainForAPI" 96 | - "apiGateway" 97 | Properties: 98 | BasePath: "transform" 99 | DomainName: !Ref chaosDomainApi 100 | RestApiId: !Ref "apiGateway" 101 | 102 | route53RecordSet: 103 | Type: AWS::Route53::RecordSet 104 | DependsOn: 105 | - "mappingCustomDomainToChaosAPI" 106 | Properties: 107 | AliasTarget: 108 | DNSName: !GetAtt "customDomainForAPI.RegionalDomainName" 109 | EvaluateTargetHealth: false 110 | HostedZoneId: 111 | Ref: regionHostedZoneId 112 | Name: !Ref chaosDomainApi 113 | HostedZoneId: 114 | Ref: customHostedZoneId 115 | Type: "A" 116 | 117 | lambdaFunction: 118 | Type: "AWS::Lambda::Function" 119 | Properties: 120 | Description: "function that turns a json chaos results log into data consumable by chart js" 121 | Code: 122 | S3Bucket: !Ref "chaosLambdaS3Bucket" 123 | S3Key: "lambda.zip" 124 | FunctionName: "chaos-qoala-data-analysis-and-transform" 125 | Handler: "lambda.handler" 126 | MemorySize: 128 127 | Role: !GetAtt "lambdaIAMRole.Arn" 128 | Runtime: "nodejs8.10" 129 | Timeout: 300 130 | 131 | lambdaApiGatewayInvoke: 132 | Type: "AWS::Lambda::Permission" 133 | Properties: 134 | Action: "lambda:InvokeFunction" 135 | FunctionName: !GetAtt "lambdaFunction.Arn" 136 | Principal: "apigateway.amazonaws.com" 137 | SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/POST/" 138 | 139 | lambdaIAMRole: 140 | Type: "AWS::IAM::Role" 141 | Properties: 142 | AssumeRolePolicyDocument: 143 | Version: "2012-10-17" 144 | Statement: 145 | - Action: 146 | - "sts:AssumeRole" 147 | Effect: "Allow" 148 | Principal: 149 | Service: 150 | - "lambda.amazonaws.com" 151 | Policies: 152 | - PolicyDocument: 153 | Version: "2012-10-17" 154 | Statement: 155 | - Action: 156 | - "logs:CreateLogGroup" 157 | - "logs:CreateLogStream" 158 | - "logs:PutLogEvents" 159 | Effect: "Allow" 160 | Resource: 161 | - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/chaos-qoala-data-analysis-and-transform:*" 162 | PolicyName: "lambda" 163 | 164 | lambdaLogGroup: 165 | Type: "AWS::Logs::LogGroup" 166 | Properties: 167 | LogGroupName: !Sub "/aws/lambda/chaos-qoala-data-analysis-and-transform" 168 | RetentionInDays: 90 169 | 170 | Outputs: 171 | apiGatewayInvokeURL: 172 | Value: !Sub "https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/logs" 173 | 174 | lambdaArn: 175 | Value: !GetAtt "lambdaFunction.Arn" -------------------------------------------------------------------------------- /client/scss/_main.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | @mixin for-phone-only { 8 | @media (max-width: 599px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin for-tablet-portrait-up { 14 | @media (min-width: 600px) and (max-width: 924px) { 15 | @content; 16 | } 17 | } 18 | @mixin for-tablet-landscape-up { 19 | @media (min-width: 925px) { 20 | @content; 21 | } 22 | } 23 | @mixin for-desktop-up { 24 | @media (min-width: 1200px) { 25 | @content; 26 | } 27 | } 28 | @mixin for-big-desktop-up { 29 | @media (min-width: 1800px) { 30 | @content; 31 | } 32 | } 33 | 34 | #main { 35 | //background: linear-gradient(to bottom right, #e31e98 -15%, #827c82 68%); 36 | min-width: 100vw; 37 | left: 0; 38 | background: linear-gradient(to top left, #e31e98 -34%, #827c82 47%); 39 | display: flex; 40 | flex-flow: column nowrap; 41 | justify-content: center; 42 | align-content: center; 43 | min-height: 75vh; 44 | 45 | @include for-phone-only { 46 | display: flex; 47 | } 48 | 49 | @include for-tablet-portrait-up { 50 | display: flex; 51 | } 52 | 53 | @include for-phone-only { 54 | display: flex; 55 | } 56 | 57 | #mainpglogo { 58 | width: 9rem; 59 | height: auto; 60 | align-self: center; 61 | position: relative; 62 | } 63 | 64 | #mainqoala { 65 | width: 5rem; 66 | height: auto; 67 | align-self: center; 68 | padding-right: 1.5rem; 69 | vertical-align: sub; 70 | @include for-phone-only { 71 | margin-top: 2rem; 72 | margin: auto 0; 73 | width: 3.5rem; 74 | height: auto; 75 | } 76 | @include for-tablet-portrait-up { 77 | margin-top: 2rem; 78 | margin: auto 0; 79 | width: 3.5rem; 80 | height: auto; 81 | } 82 | } 83 | 84 | #missionstatement { 85 | align-self: center; 86 | font-weight: 800; 87 | font-family: "Manjari"; 88 | font-size: 5rem; 89 | color: rgba(243, 239, 239, 0.89); 90 | font-size: 1.2em; 91 | @include for-phone-only { 92 | font-size: 0.7em; 93 | margin-top: 0.1rem; 94 | } 95 | @include for-tablet-portrait-up { 96 | font-size: 0.7em; 97 | margin-top: 0.1rem; 98 | } 99 | } 100 | 101 | #title { 102 | align-self: center; 103 | font-weight: 800; 104 | font-family: "Manjari"; 105 | font-size: 5rem; 106 | color: rgba(243, 239, 239, 0.89); 107 | margin-top: 1rem; 108 | margin-bottom: 0.1rem; 109 | height: 6rem; 110 | @include for-phone-only { 111 | font-size: 1.8em; 112 | margin-top: 5rem; 113 | margin-bottom: 0.2rem; 114 | } 115 | @include for-tablet-portrait-up { 116 | font-size: 1.8em; 117 | margin-top: 5rem; 118 | margin-bottom: 0.2rem; 119 | } 120 | } 121 | 122 | p { 123 | align-self: center; 124 | font-weight: 800; 125 | font-family: "Manjari"; 126 | font-size: 5rem; 127 | color: rgba(243, 239, 239, 0.89); 128 | @include for-phone-only { 129 | font-size: 1.99em; 130 | margin-top: 5rem; 131 | } 132 | @include for-tablet-portrait-up { 133 | font-size: 1.99em; 134 | margin-top: 5rem; 135 | } 136 | } 137 | 138 | span { 139 | font-size: 6rem; 140 | font-weight: 900; 141 | align-self: center; 142 | @include for-phone-only { 143 | font-size: 1.5em; 144 | margin: auto 0; 145 | } 146 | 147 | @include for-tablet-portrait-up { 148 | font-size: 1.5em; 149 | margin: auto 0; 150 | } 151 | } 152 | #starp { 153 | font-size: 1rem; 154 | display: inline-block; 155 | font-weight: 400; 156 | align-self: center; 157 | margin-top: 5rem; 158 | @include for-phone-only { 159 | font-size: 1em; 160 | margin-top: 5rem; 161 | } 162 | 163 | @include for-tablet-portrait-up { 164 | font-size: 1em; 165 | margin-top: 5rem; 166 | } 167 | } 168 | 169 | #starp { 170 | font-size: 1.5rem; 171 | display: inline-block; 172 | font-weight: 400; 173 | align-self: center; 174 | @include for-phone-only { 175 | align-content: center; 176 | font-size: 1.3em; 177 | margin-top: 5rem; 178 | } 179 | 180 | @include for-tablet-portrait-up { 181 | align-content: center; 182 | font-size: 1.3em; 183 | margin-top: 5rem; 184 | } 185 | 186 | a { 187 | align-self: center; 188 | text-decoration: none; 189 | } 190 | 191 | #gitbutton { 192 | padding: 0.2rem 0.4rem; 193 | line-height: 0.1rem; 194 | border-radius: 5px; 195 | vertical-align: top; 196 | @include for-phone-only { 197 | font-weight: 400; 198 | text-align: center; 199 | vertical-align: middle; 200 | user-select: none; 201 | padding: 0.1rem 0.1rem; 202 | font-size: 0.3rem; 203 | line-height: 1.3; 204 | border-radius: 0.25rem; 205 | } 206 | 207 | @include for-tablet-portrait-up { 208 | font-weight: 400; 209 | text-align: center; 210 | vertical-align: middle; 211 | user-select: none; 212 | padding: 0.1rem 0.1rem; 213 | font-size: 0.3rem; 214 | line-height: 1.3; 215 | border-radius: 0.25rem; 216 | } 217 | } 218 | } 219 | 220 | #downloadbutton { 221 | display: flex; 222 | justify-content: center; 223 | text-decoration: none; 224 | } 225 | 226 | .star { 227 | width: 1em; 228 | height: auto; 229 | margin-right: 0.3em; 230 | align-self: center; 231 | } 232 | 233 | #npmbutton { 234 | display: inline-block; 235 | font-weight: 400; 236 | color: #f8f8ff; 237 | text-align: center; 238 | vertical-align: middle; 239 | background-color: transparent; 240 | border: 1px solid #f8f8ff; 241 | padding: 0.5rem 1rem; 242 | font-size: 1rem; 243 | line-height: 1.5; 244 | width: auto; 245 | border-radius: 0.25rem; 246 | 247 | @include for-phone-only { 248 | display: inline-block; 249 | font-weight: 400; 250 | color: #f8f8ff; 251 | text-align: center; 252 | vertical-align: middle; 253 | background-color: transparent; 254 | border: 1px solid #f8f8ff; 255 | padding: 0.375rem 0.75rem; 256 | font-size: 1rem; 257 | line-height: 1.5; 258 | border-radius: 0.25rem; 259 | } 260 | @include for-tablet-portrait-up { 261 | display: inline-block; 262 | font-weight: 400; 263 | color: #f8f8ff; 264 | text-align: center; 265 | vertical-align: middle; 266 | background-color: transparent; 267 | border: 1px solid #f8f8ff; 268 | padding: 0.375rem 0.75rem; 269 | font-size: 1rem; 270 | line-height: 1.5; 271 | border-radius: 0.25rem; 272 | } 273 | } 274 | 275 | #npmbutton:hover { 276 | color: black; 277 | background-color: white; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /chaos-qoala-agent/index.js: -------------------------------------------------------------------------------- 1 | // some of the code below was taken from this handy npm module 2 | // https://github.com/zuojiang/modify-response-middleware/tree/master/src 3 | 4 | 5 | // GLOBAL SETUP OF WEB SOCKET CHANNEL, AND MODULE SCOPED VARIABLES 6 | // --------------------------------------------------------------- 7 | 8 | const zlib = require('zlib'); 9 | const app = require('express')(); 10 | const chaosSocketServer = require('http').Server(app); 11 | const io = require('socket.io')(chaosSocketServer); 12 | 13 | // intialize module flags - these values will be dynamically 14 | // changed via configuration info sent over socket.io 15 | 16 | // master on/off switch 17 | let ENSUE_CHAOS = false; 18 | // length of any latency injection 19 | let DELAY_RESPONSE_MS = 0; 20 | // map of queries and if they will be targets for data knockout 21 | let QUERY_TARGETS = {}; 22 | // the blast radius of the current experiemnt 23 | let BLAST_RADIUS = 0; 24 | // this is calculated per message - but scoped at module level for ease of implementation 25 | let CHAOS_CHANCE = 0; 26 | // web socket address of config and data streaming 27 | // use a port number above 1024 for linux compatibility 28 | const CHAOS_PORT = 1025; 29 | 30 | // start the socket channel on port 1025 31 | // configuration information will be sent to this channel and 32 | // live streaming results data will be sent back to the controller 33 | // process during chaos experiments. 34 | chaosSocketServer.listen(CHAOS_PORT); 35 | 36 | io.on('connection', (socket) => { 37 | // all chaos data is sent and received on the topic 'eucalyptus' 38 | // if data arrives on this port then it is a config message from the controller 39 | socket.on('eucalyptus', (config, acknowledge) => { 40 | // extract and assign configuration vals 41 | const { 42 | ensueChaos, 43 | delay, 44 | blastRadius, 45 | affectedQueries, 46 | } = config; 47 | 48 | ENSUE_CHAOS = ensueChaos; 49 | BLAST_RADIUS = blastRadius; 50 | DELAY_RESPONSE_MS = delay; 51 | QUERY_TARGETS = affectedQueries; 52 | 53 | // invoke callback sent by the controller to acknowledge receipt of config 54 | acknowledge(); 55 | }); 56 | }); 57 | 58 | // HELPER FUNCTIONS 59 | // ---------------- 60 | 61 | // encoding and decoding functions for servers implementing compression 62 | // of response data, in such cases we need to de-compress before altering 63 | // any content during chaos experiments then re-compress 64 | 65 | function encoding(res, content) { 66 | switch (res.getHeader('content-encoding')) { 67 | case 'gzip': 68 | return zlib.gzipSync(content); 69 | case 'deflate': 70 | return zlib.deflateSync(content); 71 | default: return content; 72 | } 73 | } 74 | function decoding(res, content) { 75 | switch (res.getHeader('content-encoding')) { 76 | case 'gzip': 77 | return zlib.gunzipSync(content); 78 | case 'deflate': 79 | return zlib.inflateSync(content); 80 | default: return content; 81 | } 82 | } 83 | 84 | // FUNCTION THAT CAN BE USED TO CREATE A PIECE OF MIDDLEWARE 85 | // THAT RE-WRITES THE EXPRESS RESPONSE API FUNCTIONS SO THAT 86 | // RATHER THAN SENDING DATA BACK TO THE CLIENT THE DATA IS PASSED 87 | // TO THE 'modify' CALL BACK WHICH MAY MODIFY THE RESPONSE 88 | // BEFORE IT IS SENT BACK AS NORMAL TO THE CLIENT 89 | // ------------------------------------------------------------- 90 | 91 | const modifyRes = function responseHijackingMiddlewareCreator(modify) { 92 | // note the signature of the returned function - just like a regular named middleware 93 | return (req, res, next) => { 94 | // preserve then original res.end function 95 | const expressAPIEnd = res.end; 96 | // create a buffer and a helper function to write to it 97 | const list = []; 98 | res.write = (chunk) => { 99 | list.push(chunk); 100 | }; 101 | 102 | // IMPORTANT re-write express res.end so instead of sending data it just 103 | // exposes response data to the callback provided, then repackages the 104 | // potentially altered response and sends it back to the client 105 | // via the original express API res.end implementation. 106 | res.end = async function newExpressResEndFunction(chunk) { 107 | // data will be exposed to the callback function via this variable 108 | let content; 109 | // if data being supplied in this call to res.end then add to the buffer 110 | if (chunk) list.push(chunk); 111 | if (Buffer.isBuffer(list[0])) content = Buffer.concat(list); 112 | else content = list.join(''); 113 | // decode data if required 114 | let decodedContent = decoding(res, content); 115 | 116 | // this is the IMPORTANT line where the code calls the callback responsible 117 | // for altering the content data 118 | decodedContent = await modify(decodedContent, req, res); 119 | 120 | // re encode the data if required 121 | if (Buffer.isBuffer(decodedContent) || typeof decodedContent === 'string') { 122 | content = encoding(res, decodedContent); 123 | // reset the content length to an adjusted value 124 | if (!res.headersSent) { 125 | res.setHeader('content-length', content.length); 126 | } 127 | } 128 | // IMPORTANT invoke original express res.send function 129 | expressAPIEnd.call(res, content); 130 | }; 131 | if (next) next(); 132 | }; 133 | }; 134 | 135 | // eslint-disable-next-line no-unused-vars 136 | const chaos = modifyRes(async (content, req, res) => { 137 | // create a promisi-fied timeout function to inflict latency 138 | function addLatency(ms) { 139 | return new Promise((resolve) => setTimeout(resolve, ms)); 140 | } 141 | try { 142 | // attempt to parse the data in to json 143 | const data = JSON.parse(content.toString()); 144 | // if currently running a chaos experiment 145 | if (ENSUE_CHAOS === true) { 146 | // to determine if this response will be affected - 'roll the dice' 147 | // by getting a manual number and comparing against the blast radius 148 | // the bigger the blast radius value is, the greater the chances the number 149 | // will be less than it and a chaos effect will be seen 150 | CHAOS_CHANCE = Math.random(); 151 | const affectMessageWithChaos = (CHAOS_CHANCE < BLAST_RADIUS); 152 | 153 | // construct a logging data packet for this response 154 | const agentData = { 155 | timeOfResponse: new Date().toJSON(), 156 | chaosResponse: affectMessageWithChaos, 157 | }; 158 | 159 | // send the logging data packet back to the controller 160 | io.emit('eucalyptus', agentData); 161 | 162 | // if we are affecting this message 163 | if (affectMessageWithChaos) { 164 | // add latency 165 | await addLatency(DELAY_RESPONSE_MS); 166 | // knock out a section of data based on configuation against queries 167 | const dataSectionsOfResults = Object.keys(data.data); 168 | dataSectionsOfResults.forEach((name) => { 169 | // if a data section is on the knock out list remove the data from the response 170 | if (QUERY_TARGETS[name]) delete data.data[name]; 171 | }); 172 | } 173 | } 174 | // return altered data 175 | return Buffer.from(JSON.stringify(data)); 176 | } catch (err) { 177 | // if data not parsable as json it will error and be caught in this section 178 | // so just pass back unaffected content as this response is probably unrelated 179 | // to GraphQL 180 | return content; 181 | } 182 | }); 183 | 184 | module.exports = { chaos, chaosSocketServer }; 185 | -------------------------------------------------------------------------------- /aws/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.5.5", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", 10 | "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.0.0" 14 | } 15 | }, 16 | "@babel/highlight": { 17 | "version": "7.5.0", 18 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", 19 | "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", 20 | "dev": true, 21 | "requires": { 22 | "chalk": "^2.0.0", 23 | "esutils": "^2.0.2", 24 | "js-tokens": "^4.0.0" 25 | } 26 | }, 27 | "acorn": { 28 | "version": "7.0.0", 29 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.0.0.tgz", 30 | "integrity": "sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==", 31 | "dev": true 32 | }, 33 | "acorn-jsx": { 34 | "version": "5.0.2", 35 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", 36 | "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", 37 | "dev": true 38 | }, 39 | "adm-zip": { 40 | "version": "0.4.13", 41 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", 42 | "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==" 43 | }, 44 | "ajv": { 45 | "version": "6.10.2", 46 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", 47 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", 48 | "dev": true, 49 | "requires": { 50 | "fast-deep-equal": "^2.0.1", 51 | "fast-json-stable-stringify": "^2.0.0", 52 | "json-schema-traverse": "^0.4.1", 53 | "uri-js": "^4.2.2" 54 | } 55 | }, 56 | "ansi-escapes": { 57 | "version": "3.2.0", 58 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", 59 | "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", 60 | "dev": true 61 | }, 62 | "ansi-regex": { 63 | "version": "3.0.0", 64 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 65 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 66 | "dev": true 67 | }, 68 | "ansi-styles": { 69 | "version": "3.2.1", 70 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 71 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 72 | "dev": true, 73 | "requires": { 74 | "color-convert": "^1.9.0" 75 | } 76 | }, 77 | "argparse": { 78 | "version": "1.0.10", 79 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 80 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 81 | "dev": true, 82 | "requires": { 83 | "sprintf-js": "~1.0.2" 84 | } 85 | }, 86 | "array-includes": { 87 | "version": "3.0.3", 88 | "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", 89 | "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", 90 | "dev": true, 91 | "requires": { 92 | "define-properties": "^1.1.2", 93 | "es-abstract": "^1.7.0" 94 | } 95 | }, 96 | "astral-regex": { 97 | "version": "1.0.0", 98 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 99 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 100 | "dev": true 101 | }, 102 | "aws-sdk": { 103 | "version": "2.524.0", 104 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.524.0.tgz", 105 | "integrity": "sha512-nCnKEExs1OqwNG6sIrRy+Lj8Zt2M6BTa6YlEMQ1gIpdwznaAN9XyTnm+MDc3iYsSl58MOBwiKpY8d6CONuxuDw==", 106 | "requires": { 107 | "buffer": "4.9.1", 108 | "events": "1.1.1", 109 | "ieee754": "1.1.8", 110 | "jmespath": "0.15.0", 111 | "querystring": "0.2.0", 112 | "sax": "1.2.1", 113 | "url": "0.10.3", 114 | "uuid": "3.3.2", 115 | "xml2js": "0.4.19" 116 | } 117 | }, 118 | "balanced-match": { 119 | "version": "1.0.0", 120 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 121 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 122 | "dev": true 123 | }, 124 | "base64-js": { 125 | "version": "1.3.1", 126 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 127 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 128 | }, 129 | "brace-expansion": { 130 | "version": "1.1.11", 131 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 132 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 133 | "dev": true, 134 | "requires": { 135 | "balanced-match": "^1.0.0", 136 | "concat-map": "0.0.1" 137 | } 138 | }, 139 | "buffer": { 140 | "version": "4.9.1", 141 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 142 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 143 | "requires": { 144 | "base64-js": "^1.0.2", 145 | "ieee754": "^1.1.4", 146 | "isarray": "^1.0.0" 147 | } 148 | }, 149 | "callsites": { 150 | "version": "3.1.0", 151 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 152 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 153 | "dev": true 154 | }, 155 | "chalk": { 156 | "version": "2.4.2", 157 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 158 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 159 | "dev": true, 160 | "requires": { 161 | "ansi-styles": "^3.2.1", 162 | "escape-string-regexp": "^1.0.5", 163 | "supports-color": "^5.3.0" 164 | } 165 | }, 166 | "chardet": { 167 | "version": "0.7.0", 168 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 169 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", 170 | "dev": true 171 | }, 172 | "cli-cursor": { 173 | "version": "2.1.0", 174 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 175 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 176 | "dev": true, 177 | "requires": { 178 | "restore-cursor": "^2.0.0" 179 | } 180 | }, 181 | "cli-width": { 182 | "version": "2.2.0", 183 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", 184 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", 185 | "dev": true 186 | }, 187 | "color-convert": { 188 | "version": "1.9.3", 189 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 190 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 191 | "dev": true, 192 | "requires": { 193 | "color-name": "1.1.3" 194 | } 195 | }, 196 | "color-name": { 197 | "version": "1.1.3", 198 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 199 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 200 | "dev": true 201 | }, 202 | "concat-map": { 203 | "version": "0.0.1", 204 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 205 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 206 | "dev": true 207 | }, 208 | "confusing-browser-globals": { 209 | "version": "1.0.8", 210 | "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.8.tgz", 211 | "integrity": "sha512-lI7asCibVJ6Qd3FGU7mu4sfG4try4LX3+GVS+Gv8UlrEf2AeW57piecapnog2UHZSbcX/P/1UDWVaTsblowlZg==", 212 | "dev": true 213 | }, 214 | "contains-path": { 215 | "version": "0.1.0", 216 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 217 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 218 | "dev": true 219 | }, 220 | "cross-spawn": { 221 | "version": "6.0.5", 222 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 223 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 224 | "dev": true, 225 | "requires": { 226 | "nice-try": "^1.0.4", 227 | "path-key": "^2.0.1", 228 | "semver": "^5.5.0", 229 | "shebang-command": "^1.2.0", 230 | "which": "^1.2.9" 231 | }, 232 | "dependencies": { 233 | "semver": { 234 | "version": "5.7.1", 235 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 236 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 237 | "dev": true 238 | } 239 | } 240 | }, 241 | "debug": { 242 | "version": "4.1.1", 243 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 244 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 245 | "dev": true, 246 | "requires": { 247 | "ms": "^2.1.1" 248 | } 249 | }, 250 | "deep-is": { 251 | "version": "0.1.3", 252 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 253 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 254 | "dev": true 255 | }, 256 | "define-properties": { 257 | "version": "1.1.3", 258 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 259 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 260 | "dev": true, 261 | "requires": { 262 | "object-keys": "^1.0.12" 263 | } 264 | }, 265 | "doctrine": { 266 | "version": "3.0.0", 267 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 268 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 269 | "dev": true, 270 | "requires": { 271 | "esutils": "^2.0.2" 272 | } 273 | }, 274 | "dotenv": { 275 | "version": "8.1.0", 276 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz", 277 | "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==" 278 | }, 279 | "emoji-regex": { 280 | "version": "7.0.3", 281 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 282 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 283 | "dev": true 284 | }, 285 | "error-ex": { 286 | "version": "1.3.2", 287 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 288 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 289 | "dev": true, 290 | "requires": { 291 | "is-arrayish": "^0.2.1" 292 | } 293 | }, 294 | "es-abstract": { 295 | "version": "1.14.2", 296 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.14.2.tgz", 297 | "integrity": "sha512-DgoQmbpFNOofkjJtKwr87Ma5EW4Dc8fWhD0R+ndq7Oc456ivUfGOOP6oAZTTKl5/CcNMP+EN+e3/iUzgE0veZg==", 298 | "dev": true, 299 | "requires": { 300 | "es-to-primitive": "^1.2.0", 301 | "function-bind": "^1.1.1", 302 | "has": "^1.0.3", 303 | "has-symbols": "^1.0.0", 304 | "is-callable": "^1.1.4", 305 | "is-regex": "^1.0.4", 306 | "object-inspect": "^1.6.0", 307 | "object-keys": "^1.1.1", 308 | "string.prototype.trimleft": "^2.0.0", 309 | "string.prototype.trimright": "^2.0.0" 310 | } 311 | }, 312 | "es-to-primitive": { 313 | "version": "1.2.0", 314 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", 315 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", 316 | "dev": true, 317 | "requires": { 318 | "is-callable": "^1.1.4", 319 | "is-date-object": "^1.0.1", 320 | "is-symbol": "^1.0.2" 321 | } 322 | }, 323 | "escape-string-regexp": { 324 | "version": "1.0.5", 325 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 326 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 327 | "dev": true 328 | }, 329 | "eslint": { 330 | "version": "6.3.0", 331 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.3.0.tgz", 332 | "integrity": "sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==", 333 | "dev": true, 334 | "requires": { 335 | "@babel/code-frame": "^7.0.0", 336 | "ajv": "^6.10.0", 337 | "chalk": "^2.1.0", 338 | "cross-spawn": "^6.0.5", 339 | "debug": "^4.0.1", 340 | "doctrine": "^3.0.0", 341 | "eslint-scope": "^5.0.0", 342 | "eslint-utils": "^1.4.2", 343 | "eslint-visitor-keys": "^1.1.0", 344 | "espree": "^6.1.1", 345 | "esquery": "^1.0.1", 346 | "esutils": "^2.0.2", 347 | "file-entry-cache": "^5.0.1", 348 | "functional-red-black-tree": "^1.0.1", 349 | "glob-parent": "^5.0.0", 350 | "globals": "^11.7.0", 351 | "ignore": "^4.0.6", 352 | "import-fresh": "^3.0.0", 353 | "imurmurhash": "^0.1.4", 354 | "inquirer": "^6.4.1", 355 | "is-glob": "^4.0.0", 356 | "js-yaml": "^3.13.1", 357 | "json-stable-stringify-without-jsonify": "^1.0.1", 358 | "levn": "^0.3.0", 359 | "lodash": "^4.17.14", 360 | "minimatch": "^3.0.4", 361 | "mkdirp": "^0.5.1", 362 | "natural-compare": "^1.4.0", 363 | "optionator": "^0.8.2", 364 | "progress": "^2.0.0", 365 | "regexpp": "^2.0.1", 366 | "semver": "^6.1.2", 367 | "strip-ansi": "^5.2.0", 368 | "strip-json-comments": "^3.0.1", 369 | "table": "^5.2.3", 370 | "text-table": "^0.2.0", 371 | "v8-compile-cache": "^2.0.3" 372 | } 373 | }, 374 | "eslint-config-airbnb-base": { 375 | "version": "14.0.0", 376 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", 377 | "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", 378 | "dev": true, 379 | "requires": { 380 | "confusing-browser-globals": "^1.0.7", 381 | "object.assign": "^4.1.0", 382 | "object.entries": "^1.1.0" 383 | } 384 | }, 385 | "eslint-import-resolver-node": { 386 | "version": "0.3.2", 387 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", 388 | "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", 389 | "dev": true, 390 | "requires": { 391 | "debug": "^2.6.9", 392 | "resolve": "^1.5.0" 393 | }, 394 | "dependencies": { 395 | "debug": { 396 | "version": "2.6.9", 397 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 398 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 399 | "dev": true, 400 | "requires": { 401 | "ms": "2.0.0" 402 | } 403 | }, 404 | "ms": { 405 | "version": "2.0.0", 406 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 407 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 408 | "dev": true 409 | } 410 | } 411 | }, 412 | "eslint-module-utils": { 413 | "version": "2.4.1", 414 | "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", 415 | "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", 416 | "dev": true, 417 | "requires": { 418 | "debug": "^2.6.8", 419 | "pkg-dir": "^2.0.0" 420 | }, 421 | "dependencies": { 422 | "debug": { 423 | "version": "2.6.9", 424 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 425 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 426 | "dev": true, 427 | "requires": { 428 | "ms": "2.0.0" 429 | } 430 | }, 431 | "ms": { 432 | "version": "2.0.0", 433 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 434 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 435 | "dev": true 436 | } 437 | } 438 | }, 439 | "eslint-plugin-import": { 440 | "version": "2.18.2", 441 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", 442 | "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", 443 | "dev": true, 444 | "requires": { 445 | "array-includes": "^3.0.3", 446 | "contains-path": "^0.1.0", 447 | "debug": "^2.6.9", 448 | "doctrine": "1.5.0", 449 | "eslint-import-resolver-node": "^0.3.2", 450 | "eslint-module-utils": "^2.4.0", 451 | "has": "^1.0.3", 452 | "minimatch": "^3.0.4", 453 | "object.values": "^1.1.0", 454 | "read-pkg-up": "^2.0.0", 455 | "resolve": "^1.11.0" 456 | }, 457 | "dependencies": { 458 | "debug": { 459 | "version": "2.6.9", 460 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 461 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 462 | "dev": true, 463 | "requires": { 464 | "ms": "2.0.0" 465 | } 466 | }, 467 | "doctrine": { 468 | "version": "1.5.0", 469 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 470 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 471 | "dev": true, 472 | "requires": { 473 | "esutils": "^2.0.2", 474 | "isarray": "^1.0.0" 475 | } 476 | }, 477 | "ms": { 478 | "version": "2.0.0", 479 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 480 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 481 | "dev": true 482 | } 483 | } 484 | }, 485 | "eslint-scope": { 486 | "version": "5.0.0", 487 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", 488 | "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", 489 | "dev": true, 490 | "requires": { 491 | "esrecurse": "^4.1.0", 492 | "estraverse": "^4.1.1" 493 | } 494 | }, 495 | "eslint-utils": { 496 | "version": "1.4.2", 497 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", 498 | "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", 499 | "dev": true, 500 | "requires": { 501 | "eslint-visitor-keys": "^1.0.0" 502 | } 503 | }, 504 | "eslint-visitor-keys": { 505 | "version": "1.1.0", 506 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", 507 | "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", 508 | "dev": true 509 | }, 510 | "espree": { 511 | "version": "6.1.1", 512 | "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", 513 | "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", 514 | "dev": true, 515 | "requires": { 516 | "acorn": "^7.0.0", 517 | "acorn-jsx": "^5.0.2", 518 | "eslint-visitor-keys": "^1.1.0" 519 | } 520 | }, 521 | "esprima": { 522 | "version": "4.0.1", 523 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 524 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 525 | "dev": true 526 | }, 527 | "esquery": { 528 | "version": "1.0.1", 529 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", 530 | "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", 531 | "dev": true, 532 | "requires": { 533 | "estraverse": "^4.0.0" 534 | } 535 | }, 536 | "esrecurse": { 537 | "version": "4.2.1", 538 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", 539 | "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", 540 | "dev": true, 541 | "requires": { 542 | "estraverse": "^4.1.0" 543 | } 544 | }, 545 | "estraverse": { 546 | "version": "4.3.0", 547 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 548 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 549 | "dev": true 550 | }, 551 | "esutils": { 552 | "version": "2.0.3", 553 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 554 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 555 | "dev": true 556 | }, 557 | "events": { 558 | "version": "1.1.1", 559 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 560 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 561 | }, 562 | "external-editor": { 563 | "version": "3.1.0", 564 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", 565 | "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", 566 | "dev": true, 567 | "requires": { 568 | "chardet": "^0.7.0", 569 | "iconv-lite": "^0.4.24", 570 | "tmp": "^0.0.33" 571 | } 572 | }, 573 | "fast-deep-equal": { 574 | "version": "2.0.1", 575 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 576 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 577 | "dev": true 578 | }, 579 | "fast-json-stable-stringify": { 580 | "version": "2.0.0", 581 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 582 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 583 | "dev": true 584 | }, 585 | "fast-levenshtein": { 586 | "version": "2.0.6", 587 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 588 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 589 | "dev": true 590 | }, 591 | "figures": { 592 | "version": "2.0.0", 593 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", 594 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", 595 | "dev": true, 596 | "requires": { 597 | "escape-string-regexp": "^1.0.5" 598 | } 599 | }, 600 | "file-entry-cache": { 601 | "version": "5.0.1", 602 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", 603 | "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", 604 | "dev": true, 605 | "requires": { 606 | "flat-cache": "^2.0.1" 607 | } 608 | }, 609 | "find-up": { 610 | "version": "2.1.0", 611 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 612 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 613 | "dev": true, 614 | "requires": { 615 | "locate-path": "^2.0.0" 616 | } 617 | }, 618 | "flat-cache": { 619 | "version": "2.0.1", 620 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", 621 | "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", 622 | "dev": true, 623 | "requires": { 624 | "flatted": "^2.0.0", 625 | "rimraf": "2.6.3", 626 | "write": "1.0.3" 627 | } 628 | }, 629 | "flatted": { 630 | "version": "2.0.1", 631 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", 632 | "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", 633 | "dev": true 634 | }, 635 | "fs.realpath": { 636 | "version": "1.0.0", 637 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 638 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 639 | "dev": true 640 | }, 641 | "function-bind": { 642 | "version": "1.1.1", 643 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 644 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 645 | "dev": true 646 | }, 647 | "functional-red-black-tree": { 648 | "version": "1.0.1", 649 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 650 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 651 | "dev": true 652 | }, 653 | "glob": { 654 | "version": "7.1.4", 655 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", 656 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", 657 | "dev": true, 658 | "requires": { 659 | "fs.realpath": "^1.0.0", 660 | "inflight": "^1.0.4", 661 | "inherits": "2", 662 | "minimatch": "^3.0.4", 663 | "once": "^1.3.0", 664 | "path-is-absolute": "^1.0.0" 665 | } 666 | }, 667 | "glob-parent": { 668 | "version": "5.0.0", 669 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", 670 | "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", 671 | "dev": true, 672 | "requires": { 673 | "is-glob": "^4.0.1" 674 | } 675 | }, 676 | "globals": { 677 | "version": "11.12.0", 678 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 679 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 680 | "dev": true 681 | }, 682 | "graceful-fs": { 683 | "version": "4.2.2", 684 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", 685 | "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", 686 | "dev": true 687 | }, 688 | "has": { 689 | "version": "1.0.3", 690 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 691 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 692 | "dev": true, 693 | "requires": { 694 | "function-bind": "^1.1.1" 695 | } 696 | }, 697 | "has-flag": { 698 | "version": "3.0.0", 699 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 700 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 701 | "dev": true 702 | }, 703 | "has-symbols": { 704 | "version": "1.0.0", 705 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", 706 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", 707 | "dev": true 708 | }, 709 | "hosted-git-info": { 710 | "version": "2.8.4", 711 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", 712 | "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", 713 | "dev": true 714 | }, 715 | "iconv-lite": { 716 | "version": "0.4.24", 717 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 718 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 719 | "dev": true, 720 | "requires": { 721 | "safer-buffer": ">= 2.1.2 < 3" 722 | } 723 | }, 724 | "ieee754": { 725 | "version": "1.1.8", 726 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 727 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 728 | }, 729 | "ignore": { 730 | "version": "4.0.6", 731 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 732 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 733 | "dev": true 734 | }, 735 | "import-fresh": { 736 | "version": "3.1.0", 737 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", 738 | "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", 739 | "dev": true, 740 | "requires": { 741 | "parent-module": "^1.0.0", 742 | "resolve-from": "^4.0.0" 743 | } 744 | }, 745 | "imurmurhash": { 746 | "version": "0.1.4", 747 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 748 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 749 | "dev": true 750 | }, 751 | "inflight": { 752 | "version": "1.0.6", 753 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 754 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 755 | "dev": true, 756 | "requires": { 757 | "once": "^1.3.0", 758 | "wrappy": "1" 759 | } 760 | }, 761 | "inherits": { 762 | "version": "2.0.4", 763 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 764 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 765 | "dev": true 766 | }, 767 | "inquirer": { 768 | "version": "6.5.2", 769 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", 770 | "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", 771 | "dev": true, 772 | "requires": { 773 | "ansi-escapes": "^3.2.0", 774 | "chalk": "^2.4.2", 775 | "cli-cursor": "^2.1.0", 776 | "cli-width": "^2.0.0", 777 | "external-editor": "^3.0.3", 778 | "figures": "^2.0.0", 779 | "lodash": "^4.17.12", 780 | "mute-stream": "0.0.7", 781 | "run-async": "^2.2.0", 782 | "rxjs": "^6.4.0", 783 | "string-width": "^2.1.0", 784 | "strip-ansi": "^5.1.0", 785 | "through": "^2.3.6" 786 | } 787 | }, 788 | "is-arrayish": { 789 | "version": "0.2.1", 790 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 791 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 792 | "dev": true 793 | }, 794 | "is-callable": { 795 | "version": "1.1.4", 796 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", 797 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", 798 | "dev": true 799 | }, 800 | "is-date-object": { 801 | "version": "1.0.1", 802 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 803 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 804 | "dev": true 805 | }, 806 | "is-extglob": { 807 | "version": "2.1.1", 808 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 809 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 810 | "dev": true 811 | }, 812 | "is-fullwidth-code-point": { 813 | "version": "2.0.0", 814 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 815 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 816 | "dev": true 817 | }, 818 | "is-glob": { 819 | "version": "4.0.1", 820 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 821 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 822 | "dev": true, 823 | "requires": { 824 | "is-extglob": "^2.1.1" 825 | } 826 | }, 827 | "is-promise": { 828 | "version": "2.1.0", 829 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 830 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", 831 | "dev": true 832 | }, 833 | "is-regex": { 834 | "version": "1.0.4", 835 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 836 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 837 | "dev": true, 838 | "requires": { 839 | "has": "^1.0.1" 840 | } 841 | }, 842 | "is-symbol": { 843 | "version": "1.0.2", 844 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", 845 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", 846 | "dev": true, 847 | "requires": { 848 | "has-symbols": "^1.0.0" 849 | } 850 | }, 851 | "isarray": { 852 | "version": "1.0.0", 853 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 854 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 855 | }, 856 | "isexe": { 857 | "version": "2.0.0", 858 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 859 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 860 | "dev": true 861 | }, 862 | "jmespath": { 863 | "version": "0.15.0", 864 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 865 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 866 | }, 867 | "js-tokens": { 868 | "version": "4.0.0", 869 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 870 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 871 | "dev": true 872 | }, 873 | "js-yaml": { 874 | "version": "3.13.1", 875 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 876 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 877 | "dev": true, 878 | "requires": { 879 | "argparse": "^1.0.7", 880 | "esprima": "^4.0.0" 881 | } 882 | }, 883 | "json-schema-traverse": { 884 | "version": "0.4.1", 885 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 886 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 887 | "dev": true 888 | }, 889 | "json-stable-stringify-without-jsonify": { 890 | "version": "1.0.1", 891 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 892 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 893 | "dev": true 894 | }, 895 | "levn": { 896 | "version": "0.3.0", 897 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 898 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 899 | "dev": true, 900 | "requires": { 901 | "prelude-ls": "~1.1.2", 902 | "type-check": "~0.3.2" 903 | } 904 | }, 905 | "load-json-file": { 906 | "version": "2.0.0", 907 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 908 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 909 | "dev": true, 910 | "requires": { 911 | "graceful-fs": "^4.1.2", 912 | "parse-json": "^2.2.0", 913 | "pify": "^2.0.0", 914 | "strip-bom": "^3.0.0" 915 | } 916 | }, 917 | "locate-path": { 918 | "version": "2.0.0", 919 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 920 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 921 | "dev": true, 922 | "requires": { 923 | "p-locate": "^2.0.0", 924 | "path-exists": "^3.0.0" 925 | } 926 | }, 927 | "lodash": { 928 | "version": "4.17.15", 929 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 930 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", 931 | "dev": true 932 | }, 933 | "mimic-fn": { 934 | "version": "1.2.0", 935 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 936 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", 937 | "dev": true 938 | }, 939 | "minimatch": { 940 | "version": "3.0.4", 941 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 942 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 943 | "dev": true, 944 | "requires": { 945 | "brace-expansion": "^1.1.7" 946 | } 947 | }, 948 | "minimist": { 949 | "version": "0.0.8", 950 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 951 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 952 | "dev": true 953 | }, 954 | "mkdirp": { 955 | "version": "0.5.1", 956 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 957 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 958 | "dev": true, 959 | "requires": { 960 | "minimist": "0.0.8" 961 | } 962 | }, 963 | "ms": { 964 | "version": "2.1.2", 965 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 966 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 967 | "dev": true 968 | }, 969 | "mute-stream": { 970 | "version": "0.0.7", 971 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 972 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", 973 | "dev": true 974 | }, 975 | "natural-compare": { 976 | "version": "1.4.0", 977 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 978 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 979 | "dev": true 980 | }, 981 | "nice-try": { 982 | "version": "1.0.5", 983 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 984 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 985 | "dev": true 986 | }, 987 | "normalize-package-data": { 988 | "version": "2.5.0", 989 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 990 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 991 | "dev": true, 992 | "requires": { 993 | "hosted-git-info": "^2.1.4", 994 | "resolve": "^1.10.0", 995 | "semver": "2 || 3 || 4 || 5", 996 | "validate-npm-package-license": "^3.0.1" 997 | }, 998 | "dependencies": { 999 | "semver": { 1000 | "version": "5.7.1", 1001 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1002 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 1003 | "dev": true 1004 | } 1005 | } 1006 | }, 1007 | "object-inspect": { 1008 | "version": "1.6.0", 1009 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", 1010 | "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", 1011 | "dev": true 1012 | }, 1013 | "object-keys": { 1014 | "version": "1.1.1", 1015 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 1016 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 1017 | "dev": true 1018 | }, 1019 | "object.assign": { 1020 | "version": "4.1.0", 1021 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 1022 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 1023 | "dev": true, 1024 | "requires": { 1025 | "define-properties": "^1.1.2", 1026 | "function-bind": "^1.1.1", 1027 | "has-symbols": "^1.0.0", 1028 | "object-keys": "^1.0.11" 1029 | } 1030 | }, 1031 | "object.entries": { 1032 | "version": "1.1.0", 1033 | "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", 1034 | "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", 1035 | "dev": true, 1036 | "requires": { 1037 | "define-properties": "^1.1.3", 1038 | "es-abstract": "^1.12.0", 1039 | "function-bind": "^1.1.1", 1040 | "has": "^1.0.3" 1041 | } 1042 | }, 1043 | "object.values": { 1044 | "version": "1.1.0", 1045 | "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", 1046 | "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", 1047 | "dev": true, 1048 | "requires": { 1049 | "define-properties": "^1.1.3", 1050 | "es-abstract": "^1.12.0", 1051 | "function-bind": "^1.1.1", 1052 | "has": "^1.0.3" 1053 | } 1054 | }, 1055 | "once": { 1056 | "version": "1.4.0", 1057 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1058 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1059 | "dev": true, 1060 | "requires": { 1061 | "wrappy": "1" 1062 | } 1063 | }, 1064 | "onetime": { 1065 | "version": "2.0.1", 1066 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 1067 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 1068 | "dev": true, 1069 | "requires": { 1070 | "mimic-fn": "^1.0.0" 1071 | } 1072 | }, 1073 | "optionator": { 1074 | "version": "0.8.2", 1075 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 1076 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 1077 | "dev": true, 1078 | "requires": { 1079 | "deep-is": "~0.1.3", 1080 | "fast-levenshtein": "~2.0.4", 1081 | "levn": "~0.3.0", 1082 | "prelude-ls": "~1.1.2", 1083 | "type-check": "~0.3.2", 1084 | "wordwrap": "~1.0.0" 1085 | } 1086 | }, 1087 | "os-tmpdir": { 1088 | "version": "1.0.2", 1089 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 1090 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 1091 | "dev": true 1092 | }, 1093 | "p-limit": { 1094 | "version": "1.3.0", 1095 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", 1096 | "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", 1097 | "dev": true, 1098 | "requires": { 1099 | "p-try": "^1.0.0" 1100 | } 1101 | }, 1102 | "p-locate": { 1103 | "version": "2.0.0", 1104 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 1105 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 1106 | "dev": true, 1107 | "requires": { 1108 | "p-limit": "^1.1.0" 1109 | } 1110 | }, 1111 | "p-try": { 1112 | "version": "1.0.0", 1113 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", 1114 | "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", 1115 | "dev": true 1116 | }, 1117 | "parent-module": { 1118 | "version": "1.0.1", 1119 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1120 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1121 | "dev": true, 1122 | "requires": { 1123 | "callsites": "^3.0.0" 1124 | } 1125 | }, 1126 | "parse-json": { 1127 | "version": "2.2.0", 1128 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 1129 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 1130 | "dev": true, 1131 | "requires": { 1132 | "error-ex": "^1.2.0" 1133 | } 1134 | }, 1135 | "path-exists": { 1136 | "version": "3.0.0", 1137 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 1138 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 1139 | "dev": true 1140 | }, 1141 | "path-is-absolute": { 1142 | "version": "1.0.1", 1143 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1144 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1145 | "dev": true 1146 | }, 1147 | "path-key": { 1148 | "version": "2.0.1", 1149 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 1150 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 1151 | "dev": true 1152 | }, 1153 | "path-parse": { 1154 | "version": "1.0.6", 1155 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 1156 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 1157 | "dev": true 1158 | }, 1159 | "path-type": { 1160 | "version": "2.0.0", 1161 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 1162 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 1163 | "dev": true, 1164 | "requires": { 1165 | "pify": "^2.0.0" 1166 | } 1167 | }, 1168 | "pify": { 1169 | "version": "2.3.0", 1170 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1171 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1172 | "dev": true 1173 | }, 1174 | "pkg-dir": { 1175 | "version": "2.0.0", 1176 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", 1177 | "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", 1178 | "dev": true, 1179 | "requires": { 1180 | "find-up": "^2.1.0" 1181 | } 1182 | }, 1183 | "prelude-ls": { 1184 | "version": "1.1.2", 1185 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1186 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 1187 | "dev": true 1188 | }, 1189 | "progress": { 1190 | "version": "2.0.3", 1191 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 1192 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 1193 | "dev": true 1194 | }, 1195 | "punycode": { 1196 | "version": "1.3.2", 1197 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 1198 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 1199 | }, 1200 | "querystring": { 1201 | "version": "0.2.0", 1202 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 1203 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 1204 | }, 1205 | "read-pkg": { 1206 | "version": "2.0.0", 1207 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 1208 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 1209 | "dev": true, 1210 | "requires": { 1211 | "load-json-file": "^2.0.0", 1212 | "normalize-package-data": "^2.3.2", 1213 | "path-type": "^2.0.0" 1214 | } 1215 | }, 1216 | "read-pkg-up": { 1217 | "version": "2.0.0", 1218 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 1219 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 1220 | "dev": true, 1221 | "requires": { 1222 | "find-up": "^2.0.0", 1223 | "read-pkg": "^2.0.0" 1224 | } 1225 | }, 1226 | "regexpp": { 1227 | "version": "2.0.1", 1228 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", 1229 | "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", 1230 | "dev": true 1231 | }, 1232 | "resolve": { 1233 | "version": "1.12.0", 1234 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", 1235 | "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", 1236 | "dev": true, 1237 | "requires": { 1238 | "path-parse": "^1.0.6" 1239 | } 1240 | }, 1241 | "resolve-from": { 1242 | "version": "4.0.0", 1243 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1244 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1245 | "dev": true 1246 | }, 1247 | "restore-cursor": { 1248 | "version": "2.0.0", 1249 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 1250 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 1251 | "dev": true, 1252 | "requires": { 1253 | "onetime": "^2.0.0", 1254 | "signal-exit": "^3.0.2" 1255 | } 1256 | }, 1257 | "rimraf": { 1258 | "version": "2.6.3", 1259 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 1260 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 1261 | "dev": true, 1262 | "requires": { 1263 | "glob": "^7.1.3" 1264 | } 1265 | }, 1266 | "run-async": { 1267 | "version": "2.3.0", 1268 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", 1269 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", 1270 | "dev": true, 1271 | "requires": { 1272 | "is-promise": "^2.1.0" 1273 | } 1274 | }, 1275 | "rxjs": { 1276 | "version": "6.5.3", 1277 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", 1278 | "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", 1279 | "dev": true, 1280 | "requires": { 1281 | "tslib": "^1.9.0" 1282 | } 1283 | }, 1284 | "safer-buffer": { 1285 | "version": "2.1.2", 1286 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1287 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1288 | "dev": true 1289 | }, 1290 | "sax": { 1291 | "version": "1.2.1", 1292 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 1293 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 1294 | }, 1295 | "semver": { 1296 | "version": "6.3.0", 1297 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1298 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1299 | "dev": true 1300 | }, 1301 | "shebang-command": { 1302 | "version": "1.2.0", 1303 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1304 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1305 | "dev": true, 1306 | "requires": { 1307 | "shebang-regex": "^1.0.0" 1308 | } 1309 | }, 1310 | "shebang-regex": { 1311 | "version": "1.0.0", 1312 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1313 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1314 | "dev": true 1315 | }, 1316 | "signal-exit": { 1317 | "version": "3.0.2", 1318 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1319 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1320 | "dev": true 1321 | }, 1322 | "slice-ansi": { 1323 | "version": "2.1.0", 1324 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", 1325 | "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", 1326 | "dev": true, 1327 | "requires": { 1328 | "ansi-styles": "^3.2.0", 1329 | "astral-regex": "^1.0.0", 1330 | "is-fullwidth-code-point": "^2.0.0" 1331 | } 1332 | }, 1333 | "spdx-correct": { 1334 | "version": "3.1.0", 1335 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", 1336 | "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", 1337 | "dev": true, 1338 | "requires": { 1339 | "spdx-expression-parse": "^3.0.0", 1340 | "spdx-license-ids": "^3.0.0" 1341 | } 1342 | }, 1343 | "spdx-exceptions": { 1344 | "version": "2.2.0", 1345 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", 1346 | "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", 1347 | "dev": true 1348 | }, 1349 | "spdx-expression-parse": { 1350 | "version": "3.0.0", 1351 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", 1352 | "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", 1353 | "dev": true, 1354 | "requires": { 1355 | "spdx-exceptions": "^2.1.0", 1356 | "spdx-license-ids": "^3.0.0" 1357 | } 1358 | }, 1359 | "spdx-license-ids": { 1360 | "version": "3.0.5", 1361 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", 1362 | "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", 1363 | "dev": true 1364 | }, 1365 | "sprintf-js": { 1366 | "version": "1.0.3", 1367 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1368 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1369 | "dev": true 1370 | }, 1371 | "string-width": { 1372 | "version": "2.1.1", 1373 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1374 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1375 | "dev": true, 1376 | "requires": { 1377 | "is-fullwidth-code-point": "^2.0.0", 1378 | "strip-ansi": "^4.0.0" 1379 | }, 1380 | "dependencies": { 1381 | "strip-ansi": { 1382 | "version": "4.0.0", 1383 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1384 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1385 | "dev": true, 1386 | "requires": { 1387 | "ansi-regex": "^3.0.0" 1388 | } 1389 | } 1390 | } 1391 | }, 1392 | "string.prototype.trimleft": { 1393 | "version": "2.1.0", 1394 | "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", 1395 | "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", 1396 | "dev": true, 1397 | "requires": { 1398 | "define-properties": "^1.1.3", 1399 | "function-bind": "^1.1.1" 1400 | } 1401 | }, 1402 | "string.prototype.trimright": { 1403 | "version": "2.1.0", 1404 | "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", 1405 | "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", 1406 | "dev": true, 1407 | "requires": { 1408 | "define-properties": "^1.1.3", 1409 | "function-bind": "^1.1.1" 1410 | } 1411 | }, 1412 | "strip-ansi": { 1413 | "version": "5.2.0", 1414 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1415 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1416 | "dev": true, 1417 | "requires": { 1418 | "ansi-regex": "^4.1.0" 1419 | }, 1420 | "dependencies": { 1421 | "ansi-regex": { 1422 | "version": "4.1.0", 1423 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 1424 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 1425 | "dev": true 1426 | } 1427 | } 1428 | }, 1429 | "strip-bom": { 1430 | "version": "3.0.0", 1431 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1432 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1433 | "dev": true 1434 | }, 1435 | "strip-json-comments": { 1436 | "version": "3.0.1", 1437 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", 1438 | "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", 1439 | "dev": true 1440 | }, 1441 | "supports-color": { 1442 | "version": "5.5.0", 1443 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1444 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1445 | "dev": true, 1446 | "requires": { 1447 | "has-flag": "^3.0.0" 1448 | } 1449 | }, 1450 | "table": { 1451 | "version": "5.4.6", 1452 | "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", 1453 | "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", 1454 | "dev": true, 1455 | "requires": { 1456 | "ajv": "^6.10.2", 1457 | "lodash": "^4.17.14", 1458 | "slice-ansi": "^2.1.0", 1459 | "string-width": "^3.0.0" 1460 | }, 1461 | "dependencies": { 1462 | "string-width": { 1463 | "version": "3.1.0", 1464 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 1465 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 1466 | "dev": true, 1467 | "requires": { 1468 | "emoji-regex": "^7.0.1", 1469 | "is-fullwidth-code-point": "^2.0.0", 1470 | "strip-ansi": "^5.1.0" 1471 | } 1472 | } 1473 | } 1474 | }, 1475 | "text-table": { 1476 | "version": "0.2.0", 1477 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1478 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1479 | "dev": true 1480 | }, 1481 | "through": { 1482 | "version": "2.3.8", 1483 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1484 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 1485 | "dev": true 1486 | }, 1487 | "tmp": { 1488 | "version": "0.0.33", 1489 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1490 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1491 | "dev": true, 1492 | "requires": { 1493 | "os-tmpdir": "~1.0.2" 1494 | } 1495 | }, 1496 | "tslib": { 1497 | "version": "1.10.0", 1498 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 1499 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", 1500 | "dev": true 1501 | }, 1502 | "type-check": { 1503 | "version": "0.3.2", 1504 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1505 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1506 | "dev": true, 1507 | "requires": { 1508 | "prelude-ls": "~1.1.2" 1509 | } 1510 | }, 1511 | "uri-js": { 1512 | "version": "4.2.2", 1513 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1514 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1515 | "dev": true, 1516 | "requires": { 1517 | "punycode": "^2.1.0" 1518 | }, 1519 | "dependencies": { 1520 | "punycode": { 1521 | "version": "2.1.1", 1522 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1523 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1524 | "dev": true 1525 | } 1526 | } 1527 | }, 1528 | "url": { 1529 | "version": "0.10.3", 1530 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 1531 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 1532 | "requires": { 1533 | "punycode": "1.3.2", 1534 | "querystring": "0.2.0" 1535 | } 1536 | }, 1537 | "uuid": { 1538 | "version": "3.3.2", 1539 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1540 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 1541 | }, 1542 | "v8-compile-cache": { 1543 | "version": "2.1.0", 1544 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", 1545 | "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", 1546 | "dev": true 1547 | }, 1548 | "validate-npm-package-license": { 1549 | "version": "3.0.4", 1550 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 1551 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 1552 | "dev": true, 1553 | "requires": { 1554 | "spdx-correct": "^3.0.0", 1555 | "spdx-expression-parse": "^3.0.0" 1556 | } 1557 | }, 1558 | "which": { 1559 | "version": "1.3.1", 1560 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1561 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1562 | "dev": true, 1563 | "requires": { 1564 | "isexe": "^2.0.0" 1565 | } 1566 | }, 1567 | "wordwrap": { 1568 | "version": "1.0.0", 1569 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 1570 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 1571 | "dev": true 1572 | }, 1573 | "wrappy": { 1574 | "version": "1.0.2", 1575 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1576 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1577 | "dev": true 1578 | }, 1579 | "write": { 1580 | "version": "1.0.3", 1581 | "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", 1582 | "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", 1583 | "dev": true, 1584 | "requires": { 1585 | "mkdirp": "^0.5.1" 1586 | } 1587 | }, 1588 | "xml2js": { 1589 | "version": "0.4.19", 1590 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 1591 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 1592 | "requires": { 1593 | "sax": ">=0.6.0", 1594 | "xmlbuilder": "~9.0.1" 1595 | } 1596 | }, 1597 | "xmlbuilder": { 1598 | "version": "9.0.7", 1599 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 1600 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 1601 | } 1602 | } 1603 | } 1604 | --------------------------------------------------------------------------------