├── .eslintrc.js ├── .gitignore ├── README.md ├── cleanup ├── function.json └── index.js ├── host.json ├── identity-manager ├── function.json └── index.js ├── meta-manager ├── function.json └── index.js ├── package-lock.json ├── package.json ├── sandbox-provisioning ├── function.json └── index.js └── setup ├── README.md ├── main.yml └── vars.yml.sample /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 4 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "unix" 18 | ], 19 | "quotes": [ 20 | "error", 21 | "single" 22 | ], 23 | "semi": [ 24 | "error", 25 | "always" 26 | ] 27 | } 28 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.retry 3 | vars.yml 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sherlock 2 | 3 | *Integration testing sandbox provisioning tool for Microsoft Azure* 4 | 5 | ## Index 6 | 7 | - [Overview](#overview) 8 | - [Setup with Ansible](#setup-with-ansible) 9 | - [Setup manually](#setup-manually) 10 | - [Database](#database) 11 | - [Metadata Service](#metadata-service) 12 | - [Usage](#usage) 13 | 14 | ## Overview 15 | 16 | **What does Sherlock provide for me?** 17 | 18 | Sherlock will create one or more resource groups in a subscription, and create a corresponding service principal that has rights *only* in that/those resource group(s). There is also a cleanup process that will routinely run to delete resource groups and service principals from past integration test runs. In essence, this is a turn-key solution that requires no administration overhead for an integration testing environment. 19 | 20 | **What is Sherlock built with?** 21 | 22 | This tool is an Azure Function app, with two functions: the first one is a web API that listens for requests to create a sandbox environment (and respond with the necessary connection information). The second Function is a cleanup process that is the cron job to remove sandbox environments in the subscription when they expire. 23 | 24 | ## Setup with Ansible 25 | 26 | [Ansible playbook setup guide](setup/) 27 | 28 | ## Setup Manually 29 | 30 | To quickly and easily standup Sherlock in your Azure Subscription, I highly recommend you use the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). The following steps assume that you have the Azure CLI installed and logged in for your subscription. 31 | 32 | 1. Fork [this repository](https://github.com/tstringer/sherlock) into your GitHub account. I *highly* recommend that you fork this repository into your own GitHub account. I will continue active development on Sherlock and in order to ensure I don't introduce breaking changes into your integration testing environment, you should have a downstream fork so you can pick and choose when you merge updates (fixes, etc.) 33 | 1. Create a resource group for Sherlock: `$ az group create -n sherlock-rg -l eastus` 34 | 1. Create a storage account: `$ az storage account create -g sherlock-rg -n sherlockstor -l eastus --sku Standard_LRS` 35 | 1. Create the Azure Function App: `$ az functionapp create -g sherlock-rg -n sherlockinttest -s sherlockstor -u https://github.com/tstringer/sherlock.git --consumption-plan-location eastus` (you will need to create a unique name for your Function App) 36 | 1. Configure Sherlock (see the [Configuration section](#configuration) below) 37 | 38 | ### Configuration 39 | 40 | 1. Set the client ID app setting for Sherlock: `$ az functionapp config appsettings set -g sherlock-rg -n sherlockinttest --settings AZURE_CLIENT_ID=` (this is going to be the service principal application ID that you have to prestage in your Azure AD tenant) 41 | 1. Set the client secret for Sherlock: `$ az functionapp config appsettings set -g sherlock-rg -n sherlockinttest --settings AZURE_CLIENT_SECRET=` 42 | 1. Set the subscription ID: `$ az functionapp config appsettings set -g sherlock-rg -n sherlockinttest --settings AZURE_SUBSCRIPTION_ID=` 43 | 1. Set the tenant ID for your Azure AD: `$ az functionapp config appsettings set -g sherlock-rg -n sherlockinttest --settings AZURE_TENANT_ID=` 44 | 1. Set the prefix for Sherlock: `$ az functionapp config appsettings set -g sherlock-rg -n sherlockinttest --settings RES_PREFIX=sherlock` (this will be the prefix that is used to name provisioned resource groups and service principals) 45 | 46 | ## Queue Setup and Configuration 47 | 48 | Sherlock utilizes queueing for pooling identities. This queue is provided by Azure Storage, and therefore you need to setup the account prior to using Sherlock. 49 | 50 | 1. Create a general purpose storage account in an Azure subscription 51 | 1. On the Sherlock Function App, set the following environment variables: 52 | - SHERLOCK_IDENTITY_STORAGE_ACCOUNT - set this to the Azure storage account 53 | - SHERLOCK_IDENTITY_STORAGE_KEY - set this to the storage key 54 | 55 | :bulb: Note, you don't have to prestage the queue. The `identity-manager` Function will create it if it doesn't already exist 56 | 57 | ## Database 58 | 59 | Starting in v0.4.0, Sherlock now uses persistent storage for metadata, moving away from resource group tags. The storage is a PostgreSQL database. Set the following Azure Function app setting environment variables to their appropriate value: 60 | 61 | - `PG_HOST`: the postgres hostname 62 | - `PG_DATABASE`: the database name 63 | - `PG_USER`: the role to connect to postgres 64 | - `PG_PASSWORD`: the role's password 65 | 66 | ## Metadata Service 67 | 68 | With the inception of the metadata service (`meta-manager` Azure Function), you need to set the following Azure Function app setting environment variables: 69 | 70 | - `META_URL`: the URL to the `meta-manager` Azure Function (can be retrieved from the portal) 71 | - `META_KEY`: the Azure Function auth key for the `meta-manager` Function 72 | 73 | ## Usage 74 | 75 | Once you have Sherlock setup and configured (see above), you only need to make a POST request to Sherlock. The request will look like: `https://.azurewebsites.net/api/sandbox-provisioning?code=`, where `function_app_name` is the name of the Azure Function App you used when you created it above (in my case, I used `sherlockinttest` but you would have a different name). 76 | 77 | The `key` is either the existing Function key that was created with the Azure Function was created, or a newly generated key (it is recommended to create a new key for each user and integration testing framework so that it is a more secure implementation, allowing you to revoke a key without affecting more users/clients). To create a new key you will have to use the Azure Portal. Navigate to the portal, and go to your Azure Function. Click on the **Manage** section for the `sandbox-provisioning` Function. Here you can view existing keys as well as create new keys. 78 | 79 | **Request parameters** (all optional) 80 | 81 | - *rgcount*: the amount of resource groups that need to be created (default **1**) 82 | - *region*: the location to create the resource groups in (default **eastus**) 83 | - *duration*: amount of time (in minutes) that the resource group (and corresponding principal) needs to be preserved for (default **30 minutes**). After this elapsed time, the cleanup Function will delete the resource groups and any resources in the resource group(s) 84 | - *prefix*: prefix the resource group (appended to `RES_PREFIX` or the default 'sherlock') with a request-level prefix 85 | 86 | **Examples** 87 | 88 | - Create a single resource group in East US that should live for 30 minutes: `$ curl "https://.azurewebsites.net/api/sandbox-provisioning?code="` 89 | - Create two resource groups in West US that should live for 2 hours: `$ curl "https://.azurewebsites.net/api/sandbox-provisioning?code=®ion=westus&duration=120&rgcount=2"` 90 | 91 | **Response** 92 | 93 | Sherlock, if successfully run, will respond with the following: 94 | 95 | - *resourceGroupNames*: an array of the name(s) of the resource group(s) that were created 96 | - *clientId*: the ID of the service principal that was created 97 | - *clientSecret*: the secret/password of the service principal that was created 98 | - *subscriptionId*: the subscription ID for the current Azure subscription 99 | - *tenantId*: the tenant ID of the current Azure AD tenant 100 | -------------------------------------------------------------------------------- /cleanup/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "name": "cleanupTimer", 6 | "type": "timerTrigger", 7 | "direction": "in", 8 | "schedule": "0 */1 * * * *" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /cleanup/index.js: -------------------------------------------------------------------------------- 1 | const azurerm = require('azure-arm-resource'); 2 | const GraphRbacManagementClient = require('azure-graph'); 3 | const msrest = require('ms-rest-azure'); 4 | const request = require('request'); 5 | 6 | function resourceGroupPrefixesToDelete() { 7 | return new Promise((resolve, reject) => { 8 | const metaKey = process.env['META_KEY']; 9 | const metaUrl = process.env['META_URL']; 10 | request(`${metaUrl}/?code=${metaKey}`, (err, res, body) => { 11 | if (err) { 12 | reject(err); 13 | return; 14 | } 15 | resolve(JSON.parse(body)); 16 | }); 17 | }); 18 | } 19 | 20 | function deleteRgPrefixEntry(rgPrefix) { 21 | return new Promise((resolve, reject) => { 22 | const metaUrl = process.env['META_URL']; 23 | const metaKey = process.env['META_KEY']; 24 | request.delete(`${metaUrl}/?code=${metaKey}&rgprefix=${rgPrefix}`, err => { 25 | if (err) { 26 | reject(err); 27 | return; 28 | } 29 | resolve(); 30 | }); 31 | }); 32 | } 33 | 34 | function cleanup(logger) { 35 | const clientId = process.env['AZURE_CLIENT_ID']; 36 | const clientSecret = process.env['AZURE_CLIENT_SECRET']; 37 | const subscriptionId = process.env['AZURE_SUBSCRIPTION_ID']; 38 | const tenantId = process.env['AZURE_TENANT_ID']; 39 | let resClientCached; 40 | let deletedResourceGroups = []; 41 | let deleteApplications = []; 42 | let rowsCached; 43 | let rgPrefixEntriesToDeleteCachedOperations = []; 44 | 45 | const credsForGraph = new msrest.ApplicationTokenCredentials( 46 | clientId, 47 | tenantId, 48 | clientSecret, 49 | { tokenAudience: 'graph' } 50 | ); 51 | 52 | const graphClient = new GraphRbacManagementClient(credsForGraph, tenantId); 53 | 54 | return resourceGroupPrefixesToDelete() 55 | .then(rows => { 56 | rowsCached = rows; 57 | return msrest.loginWithServicePrincipalSecret(clientId, clientSecret, tenantId); 58 | }) 59 | .then(creds => { 60 | const resClient = new azurerm.ResourceManagementClient(creds, subscriptionId); 61 | resClientCached = resClient; 62 | return resClient.resourceGroups.list(); 63 | }) 64 | .then(resourceGroups => { 65 | let deleteResourceGroupOperations = []; 66 | for(let i = 0; i < resourceGroups.length; i++) { 67 | const output = rowsCached.filter(row => { 68 | return resourceGroups[i].name.substring(0, row.resource_group_prefix.length) === row.resource_group_prefix; 69 | }); 70 | if (output.length > 0) { 71 | rgPrefixEntriesToDeleteCachedOperations.push(deleteRgPrefixEntry(output[0].resource_group_prefix)); 72 | deleteApplications.push(output[0].application_object_id); 73 | logger(`Deleting ${resourceGroups[i].name} with expiration of ${output[0].expiration_datetime}`); 74 | deleteResourceGroupOperations.push(resClientCached.resourceGroups.beginDeleteMethod(resourceGroups[i].name)); 75 | deletedResourceGroups.push(resourceGroups[i].name); 76 | } 77 | } 78 | logger(`deleting ${deleteResourceGroupOperations.length} resource group(s)`); 79 | return Promise.all(deleteResourceGroupOperations); 80 | }) 81 | .then(() => Promise.all(rgPrefixEntriesToDeleteCachedOperations)) 82 | .then(() => { 83 | const applicationsToDeleteOperations = deleteApplications.map(appObjectIdToDelete => { 84 | logger(`deleting ${appObjectIdToDelete}`); 85 | return graphClient.applications.deleteMethod(appObjectIdToDelete); 86 | }); 87 | return Promise.all(applicationsToDeleteOperations); 88 | }); 89 | } 90 | 91 | module.exports = function (context, cleanupTimer) { 92 | if(cleanupTimer.isPastDue) 93 | { 94 | context.log('Past due condition met'); 95 | } 96 | 97 | cleanup(context.log) 98 | .then(() => { 99 | context.log('Cleanup iteration ran successfully'); 100 | context.done(); 101 | }) 102 | .catch(err => { 103 | context.log('Cleanup iteration failed'); 104 | context.log(err); 105 | context.done(); 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /host.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /identity-manager/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "name": "identityTimer", 6 | "type": "timerTrigger", 7 | "direction": "in", 8 | "schedule": "0 */5 * * * *" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /identity-manager/index.js: -------------------------------------------------------------------------------- 1 | const GraphRbacManagementClient = require('azure-graph'); 2 | const msrest = require('ms-rest-azure'); 3 | const azureStorage = require('azure-storage'); 4 | 5 | function populateServicePrincipalsQueue(logger) { 6 | return new Promise((resolve, reject) => { 7 | const clientId = process.env['AZURE_CLIENT_ID']; 8 | const clientSecret = process.env['AZURE_CLIENT_SECRET']; 9 | const tenantId = process.env['AZURE_TENANT_ID']; 10 | const identityStorageAccount = process.env['SHERLOCK_IDENTITY_STORAGE_ACCOUNT']; 11 | const identityStorageKey = process.env['SHERLOCK_IDENTITY_STORAGE_KEY']; 12 | const queueName = 'identity'; 13 | const desiredSpCount = process.env['SHERLOCK_DESIRED_SP_COUNT'] || 10; 14 | 15 | logger(`Populating ${queueName} with a desired count of ${desiredSpCount} service principals(s)`); 16 | 17 | const credsForGraph = new msrest.ApplicationTokenCredentials( 18 | clientId, 19 | tenantId, 20 | clientSecret, 21 | { tokenAudience: 'graph' } 22 | ); 23 | 24 | const graphClient = new GraphRbacManagementClient( 25 | credsForGraph, 26 | tenantId 27 | ); 28 | 29 | const queueService = azureStorage.createQueueService( 30 | identityStorageAccount, 31 | identityStorageKey 32 | ); 33 | queueService.createQueueIfNotExists(queueName, err => { 34 | logger('Called createQueueIfNotExists()'); 35 | if (err) { 36 | logger('Error calling createQueueIfNotExists()'); 37 | logger(err); 38 | reject(err); 39 | return; 40 | } 41 | 42 | queueService.getQueueMetadata(queueName, (err, results) => { 43 | if (err) { 44 | logger('Error calling getQueueMetadata()'); 45 | logger(err); 46 | reject(err); 47 | return; 48 | } 49 | logger(`Queue length: ${results.approximateMessageCount}`); 50 | if (results.approximateMessageCount < desiredSpCount) { 51 | createIdentities(graphClient, desiredSpCount - results.approximateMessageCount, logger) 52 | .then((identities) => { 53 | logger('Created identities'); 54 | logger(identities); 55 | identities.forEach(identity => { 56 | queueService.createMessage( 57 | queueName, 58 | `${identity.spObjectId} ${identity.appId} ${identity.appObjectId}`, 59 | err => { 60 | if (err) { 61 | reject(err); 62 | return; 63 | } 64 | logger(`Successfully inserted ${identity.spObjectId} in queue`); 65 | resolve(); 66 | } 67 | ); 68 | }); 69 | }) 70 | .catch(err => { 71 | logger('Error creating identites'); 72 | logger(err); 73 | reject(err); 74 | }); 75 | } 76 | else { 77 | resolve(); 78 | } 79 | }); 80 | }); 81 | }); 82 | } 83 | 84 | function createServicePrincipal(graphClient, applicationId) { 85 | return graphClient.servicePrincipals.create({ 86 | appId: applicationId, 87 | accountEnabled: true 88 | }); 89 | } 90 | 91 | function createApplication(graphClient, appName) { 92 | const endDate = new Date(); 93 | endDate.setFullYear(endDate.getFullYear() + 1); 94 | return graphClient.applications.create({ 95 | availableToOtherTenants: false, 96 | displayName: appName, 97 | identifierUris: [ `http://${appName}` ] 98 | }); 99 | } 100 | 101 | function randomName() { 102 | return `sherlock${Math.random().toString(36).slice(-8)}`; 103 | } 104 | 105 | function createIdentities(graphClient, count, logger) { 106 | logger(`Entering createIdentities() to provision ${count} new identity(ies)`); 107 | let newIdentities = []; 108 | for (let i = 0; i < count; i++) { 109 | newIdentities.push({ 110 | name: randomName() 111 | }); 112 | } 113 | 114 | return Promise.all( 115 | newIdentities.map((identity, idx) => { 116 | return createApplication(graphClient, identity.name) 117 | .then(app => { 118 | logger(`Application ${app.appId} created`); 119 | return createServicePrincipal(graphClient, app.appId) 120 | .then(sp => { 121 | logger(`Service principal ${sp.objectId} created`); 122 | newIdentities[idx].spObjectId = sp.objectId; 123 | newIdentities[idx].appId = app.appId; 124 | newIdentities[idx].appObjectId = app.objectId; 125 | }); 126 | }); 127 | }) 128 | ).then(() => { 129 | logger('Sleeping for 60 seconds'); 130 | return new Promise(resolve => { 131 | setTimeout(resolve, 60000); 132 | }); 133 | }).then(() => { 134 | logger(`Created ${newIdentities.length} new identity(ies)`); 135 | return newIdentities; 136 | }); 137 | } 138 | 139 | module.exports = function (context, identityTimer) { 140 | context.log('Entering function execution'); 141 | if (identityTimer.isPastDue) 142 | { 143 | context.log('Past due condition met'); 144 | context.done(); 145 | return; 146 | } 147 | 148 | populateServicePrincipalsQueue(context.log) 149 | .then(() => { 150 | context.log('Service principal queue population ran successfully'); 151 | context.done(); 152 | }) 153 | .catch(err => { 154 | context.log('Service principal queue population failed'); 155 | context.log(err); 156 | context.done(); 157 | }); 158 | }; 159 | -------------------------------------------------------------------------------- /meta-manager/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /meta-manager/index.js: -------------------------------------------------------------------------------- 1 | const pg = require('pg'); 2 | const moment = require('moment'); 3 | 4 | function pgConfig() { 5 | return { 6 | host: process.env['PG_HOST'], 7 | user: process.env['PG_USER'], 8 | password: process.env['PG_PASSWORD'], 9 | database: process.env['PG_DATABASE'], 10 | port: 5432, 11 | ssl: true 12 | }; 13 | } 14 | 15 | function expirationDate(durationMin) { 16 | const currentDate = new Date(); 17 | const newDate = moment(currentDate).add(durationMin, 'm'); 18 | return newDate.utc().toString(); 19 | } 20 | 21 | function pgErrorHandler(err) { 22 | if (err.code !== 'ECONNRESET') { 23 | throw err; 24 | } 25 | } 26 | 27 | function getMetaInfoByRgPrefix(rgPrefix) { 28 | const pgClient = new pg.Client(pgConfig()); 29 | const query = ` 30 | select 31 | resource_group_prefix, 32 | application_object_id, 33 | expiration_datetime 34 | from sandbox 35 | where resource_group_prefix = '${rgPrefix}'; 36 | `; 37 | 38 | pgClient.on('error', pgErrorHandler); 39 | 40 | return pgClient.connect() 41 | .then(() => pgClient.query(query)) 42 | .then(res => res.rows); 43 | } 44 | 45 | function getMetaInfoAll() { 46 | const pgClient = new pg.Client(pgConfig()); 47 | const query = ` 48 | select 49 | resource_group_prefix, 50 | application_object_id, 51 | expiration_datetime 52 | from sandbox; 53 | `; 54 | 55 | pgClient.on('error', pgErrorHandler); 56 | 57 | return pgClient.connect() 58 | .then(() => pgClient.query(query)) 59 | .then(res => res.rows); 60 | } 61 | 62 | function getMetaInfo() { 63 | const pgClient = new pg.Client(pgConfig()); 64 | const query = ` 65 | select 66 | resource_group_prefix, 67 | application_object_id, 68 | expiration_datetime 69 | from sandbox 70 | where expiration_datetime < now(); 71 | `; 72 | 73 | pgClient.on('error', pgErrorHandler); 74 | 75 | return pgClient.connect() 76 | .then(() => pgClient.query(query)) 77 | .then(res => res.rows); 78 | } 79 | 80 | function deleteRgPrefix(rgPrefix) { 81 | const pgClient = new pg.Client(pgConfig()); 82 | const query = ` 83 | delete from sandbox 84 | where resource_group_prefix = '${rgPrefix}'; 85 | `; 86 | 87 | pgClient.on('error', pgErrorHandler); 88 | 89 | return pgClient.connect() 90 | .then(() => pgClient.query(query)) 91 | .then(() => pgClient.end()); 92 | } 93 | 94 | function addMetaInfo(resourceGroupPrefix, applicationObjectId, expiresOn) { 95 | const pgClient = new pg.Client(pgConfig()); 96 | const query = ` 97 | insert into public.sandbox (resource_group_prefix, application_object_id, expiration_datetime) 98 | values ('${resourceGroupPrefix}', '${applicationObjectId}', '${expiresOn}'); 99 | `; 100 | 101 | pgClient.on('error', pgErrorHandler); 102 | 103 | return pgClient.connect() 104 | .then(() => pgClient.query(query)) 105 | .then(() => pgClient.end()); 106 | } 107 | 108 | module.exports = function (context, req) { 109 | context.log('JavaScript HTTP trigger function processed a request.'); 110 | 111 | if (req.method == 'GET' && (req.query.all || (req.body && req.body.all))) { 112 | if (req.query.rgprefix || (req.body && req.body.rgprefix)) { 113 | context.log(`User requested metadata for prefix ${req.query.rgprefix || req.body.rgprefix}`); 114 | getMetaInfoByRgPrefix(req.query.rgprefix || req.body.rgprefix) 115 | .then(data => { 116 | context.log('retrieved the following data: '); 117 | context.log(data); 118 | context.res = { body: data }; 119 | context.done(); 120 | }); 121 | } 122 | else { 123 | context.log('User requested to get all metadata'); 124 | getMetaInfoAll() 125 | .then(data => { 126 | context.log('retrieved the following data: '); 127 | context.log(data); 128 | context.res = { body: data }; 129 | context.done(); 130 | }); 131 | } 132 | return; 133 | } 134 | 135 | if (req.method === 'DELETE') { 136 | if (!req.query.rgprefix && !(req.body && req.body.rgprefix)) { 137 | context.res = { status: 400, body: 'To delete a resource you must pass the rgprefix' }; 138 | context.done(); 139 | return; 140 | } 141 | deleteRgPrefix(req.query.rgprefix || req.body.rgprefix) 142 | .then(() => context.done()) 143 | .catch((err) => { 144 | context.log(`Error deleting ${req.query.rgprefix || req.body.rgprefix}`); 145 | context.log(err); 146 | context.res = { status: 400, body: `Error delete ${req.query.rgprefix || req.body.rgprefix}` }; 147 | context.done(); 148 | }); 149 | return; 150 | } 151 | 152 | if (!req.query.rgprefix && !(req.body && req.body.rgprefix)) { 153 | // in this case just retrieve the data 154 | getMetaInfo() 155 | .then(data => { 156 | context.res = { body: data }; 157 | context.done(); 158 | }); 159 | return; 160 | } 161 | else if (!req.query.appobjid && !(req.body && req.body.appobjid)) { 162 | context.log('Application object id not passed'); 163 | context.res = { status: 400, body: 'Application object id not passed' }; 164 | context.done(); 165 | return; 166 | } 167 | else if (!req.query.expire && !(req.body && req.body.expire)) { 168 | context.log('Expiration date not passed'); 169 | context.res = { status: 400, body: 'Expiration date not passed' }; 170 | context.done(); 171 | return; 172 | } 173 | 174 | const rgprefix = req.query.rgprefix || req.body.rgprefix; 175 | const appobjid = req.query.appobjid || req.body.appobjid; 176 | const expire = req.query.expire || req.body.expire; 177 | 178 | addMetaInfo(rgprefix, appobjid, expirationDate(expire)) 179 | .then(() => { 180 | context.log('completed successfully'); 181 | context.res = { body: 'completely successfully' }; 182 | context.done(); 183 | }) 184 | .catch(err => { 185 | context.log('whoops there was an error'); 186 | context.log(err); 187 | context.res = { status: 400, body: 'error!' }; 188 | context.done(); 189 | }); 190 | }; 191 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sherlock", 3 | "version": "0.5.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/form-data": { 8 | "version": "0.0.33", 9 | "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", 10 | "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", 11 | "requires": { 12 | "@types/node": "7.0.33" 13 | } 14 | }, 15 | "@types/node": { 16 | "version": "7.0.33", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.33.tgz", 18 | "integrity": "sha512-8fVvl6Yyk3jZvSYxRMS9/AmZJ5RXCOP9N4xSlykyBViVESu751pxHYTN14Embn1Fem78YwEHdC7p7KGQQpwunw==" 19 | }, 20 | "@types/request": { 21 | "version": "0.0.45", 22 | "resolved": "https://registry.npmjs.org/@types/request/-/request-0.0.45.tgz", 23 | "integrity": "sha512-OIIREjT58pnpfJjEY5PeBEuRtRR2ED4DF1Ez3Dj9474kCqEKfE+iNAYyM/P3RxxDjNxBhipo+peNBW0S/7Wrzg==", 24 | "requires": { 25 | "@types/form-data": "0.0.33", 26 | "@types/node": "7.0.33" 27 | } 28 | }, 29 | "@types/uuid": { 30 | "version": "2.0.30", 31 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-2.0.30.tgz", 32 | "integrity": "sha512-nmSJ0AL2mZAi68dYb7nKSIJ9YiM68eToP3Ugz66Ou7qkSatIptDM6CvOUmj4epMKWvO9gr9fFBxEEJkromp1Qg==", 33 | "requires": { 34 | "@types/node": "7.0.33" 35 | } 36 | }, 37 | "adal-node": { 38 | "version": "0.1.22", 39 | "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.22.tgz", 40 | "integrity": "sha1-vne9LqiooKAxhLQmEY/Rb3n2NyU=", 41 | "requires": { 42 | "async": "2.5.0", 43 | "date-utils": "1.2.21", 44 | "jws": "3.1.4", 45 | "node-uuid": "1.4.7", 46 | "request": "2.83.0", 47 | "underscore": "1.8.3", 48 | "xmldom": "0.1.27", 49 | "xpath.js": "1.0.7" 50 | }, 51 | "dependencies": { 52 | "async": { 53 | "version": "2.5.0", 54 | "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", 55 | "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", 56 | "requires": { 57 | "lodash": "4.17.4" 58 | } 59 | }, 60 | "node-uuid": { 61 | "version": "1.4.7", 62 | "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", 63 | "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=" 64 | } 65 | } 66 | }, 67 | "ajv": { 68 | "version": "5.2.3", 69 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", 70 | "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", 71 | "requires": { 72 | "co": "4.6.0", 73 | "fast-deep-equal": "1.0.0", 74 | "json-schema-traverse": "0.3.1", 75 | "json-stable-stringify": "1.0.1" 76 | } 77 | }, 78 | "ansi-regex": { 79 | "version": "2.1.1", 80 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 81 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 82 | }, 83 | "ansi-styles": { 84 | "version": "2.2.1", 85 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 86 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" 87 | }, 88 | "asn1": { 89 | "version": "0.2.3", 90 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 91 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 92 | }, 93 | "assert-plus": { 94 | "version": "1.0.0", 95 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 96 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 97 | }, 98 | "async": { 99 | "version": "0.2.7", 100 | "resolved": "https://registry.npmjs.org/async/-/async-0.2.7.tgz", 101 | "integrity": "sha1-RMXuFRrs5sS/U2TPx8KP5OWPGN8=" 102 | }, 103 | "asynckit": { 104 | "version": "0.4.0", 105 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 106 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 107 | }, 108 | "aws-sign2": { 109 | "version": "0.7.0", 110 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 111 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 112 | }, 113 | "aws4": { 114 | "version": "1.6.0", 115 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", 116 | "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" 117 | }, 118 | "azure-arm-authorization": { 119 | "version": "4.0.0-preview", 120 | "resolved": "https://registry.npmjs.org/azure-arm-authorization/-/azure-arm-authorization-4.0.0-preview.tgz", 121 | "integrity": "sha1-qYEXPRSIFzhyQg2QP3ZSLLwxv9w=", 122 | "requires": { 123 | "ms-rest": "2.2.1", 124 | "ms-rest-azure": "2.2.1" 125 | } 126 | }, 127 | "azure-arm-resource": { 128 | "version": "2.0.0-preview", 129 | "resolved": "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-2.0.0-preview.tgz", 130 | "integrity": "sha1-lYAQoY+V/4LpqZU3E7oMydJxEks=", 131 | "requires": { 132 | "ms-rest": "2.2.1", 133 | "ms-rest-azure": "2.2.1" 134 | } 135 | }, 136 | "azure-graph": { 137 | "version": "2.1.0-preview", 138 | "resolved": "https://registry.npmjs.org/azure-graph/-/azure-graph-2.1.0-preview.tgz", 139 | "integrity": "sha1-IAWrt22Rk8v7kPJe6Sgjzeh9T18=", 140 | "requires": { 141 | "ms-rest": "2.2.1", 142 | "ms-rest-azure": "2.2.1" 143 | } 144 | }, 145 | "azure-storage": { 146 | "version": "2.2.2", 147 | "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.2.2.tgz", 148 | "integrity": "sha1-yD+/PE5eYf7P048XS+aBEzw/nhU=", 149 | "requires": { 150 | "browserify-mime": "1.2.9", 151 | "json-edm-parser": "0.1.2", 152 | "md5.js": "1.3.4", 153 | "readable-stream": "2.0.6", 154 | "request": "2.74.0", 155 | "underscore": "1.8.3", 156 | "uuid": "3.1.0", 157 | "validator": "3.35.0", 158 | "xml2js": "0.2.7", 159 | "xmlbuilder": "0.4.3" 160 | }, 161 | "dependencies": { 162 | "assert-plus": { 163 | "version": "0.2.0", 164 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", 165 | "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" 166 | }, 167 | "async": { 168 | "version": "2.5.0", 169 | "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", 170 | "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", 171 | "requires": { 172 | "lodash": "4.17.4" 173 | } 174 | }, 175 | "aws-sign2": { 176 | "version": "0.6.0", 177 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", 178 | "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" 179 | }, 180 | "boom": { 181 | "version": "2.10.1", 182 | "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", 183 | "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", 184 | "requires": { 185 | "hoek": "2.16.3" 186 | } 187 | }, 188 | "caseless": { 189 | "version": "0.11.0", 190 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", 191 | "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" 192 | }, 193 | "cryptiles": { 194 | "version": "2.0.5", 195 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", 196 | "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", 197 | "requires": { 198 | "boom": "2.10.1" 199 | } 200 | }, 201 | "form-data": { 202 | "version": "1.0.1", 203 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", 204 | "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", 205 | "requires": { 206 | "async": "2.5.0", 207 | "combined-stream": "1.0.5", 208 | "mime-types": "2.1.17" 209 | } 210 | }, 211 | "har-validator": { 212 | "version": "2.0.6", 213 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", 214 | "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", 215 | "requires": { 216 | "chalk": "1.1.3", 217 | "commander": "2.11.0", 218 | "is-my-json-valid": "2.16.1", 219 | "pinkie-promise": "2.0.1" 220 | } 221 | }, 222 | "hawk": { 223 | "version": "3.1.3", 224 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", 225 | "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", 226 | "requires": { 227 | "boom": "2.10.1", 228 | "cryptiles": "2.0.5", 229 | "hoek": "2.16.3", 230 | "sntp": "1.0.9" 231 | } 232 | }, 233 | "hoek": { 234 | "version": "2.16.3", 235 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", 236 | "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" 237 | }, 238 | "http-signature": { 239 | "version": "1.1.1", 240 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", 241 | "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", 242 | "requires": { 243 | "assert-plus": "0.2.0", 244 | "jsprim": "1.4.1", 245 | "sshpk": "1.13.1" 246 | } 247 | }, 248 | "node-uuid": { 249 | "version": "1.4.8", 250 | "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", 251 | "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" 252 | }, 253 | "qs": { 254 | "version": "6.2.3", 255 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz", 256 | "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=" 257 | }, 258 | "request": { 259 | "version": "2.74.0", 260 | "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz", 261 | "integrity": "sha1-dpPKdou7DqXIzgjAhKRe+gW4kqs=", 262 | "requires": { 263 | "aws-sign2": "0.6.0", 264 | "aws4": "1.6.0", 265 | "bl": "1.1.2", 266 | "caseless": "0.11.0", 267 | "combined-stream": "1.0.5", 268 | "extend": "3.0.1", 269 | "forever-agent": "0.6.1", 270 | "form-data": "1.0.1", 271 | "har-validator": "2.0.6", 272 | "hawk": "3.1.3", 273 | "http-signature": "1.1.1", 274 | "is-typedarray": "1.0.0", 275 | "isstream": "0.1.2", 276 | "json-stringify-safe": "5.0.1", 277 | "mime-types": "2.1.17", 278 | "node-uuid": "1.4.8", 279 | "oauth-sign": "0.8.2", 280 | "qs": "6.2.3", 281 | "stringstream": "0.0.5", 282 | "tough-cookie": "2.3.3", 283 | "tunnel-agent": "0.4.3" 284 | } 285 | }, 286 | "sntp": { 287 | "version": "1.0.9", 288 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", 289 | "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", 290 | "requires": { 291 | "hoek": "2.16.3" 292 | } 293 | }, 294 | "tunnel-agent": { 295 | "version": "0.4.3", 296 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", 297 | "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" 298 | } 299 | } 300 | }, 301 | "base64url": { 302 | "version": "2.0.0", 303 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", 304 | "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" 305 | }, 306 | "bcrypt-pbkdf": { 307 | "version": "1.0.1", 308 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", 309 | "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", 310 | "optional": true, 311 | "requires": { 312 | "tweetnacl": "0.14.5" 313 | } 314 | }, 315 | "bl": { 316 | "version": "1.1.2", 317 | "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", 318 | "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", 319 | "requires": { 320 | "readable-stream": "2.0.6" 321 | } 322 | }, 323 | "boom": { 324 | "version": "4.3.1", 325 | "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", 326 | "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", 327 | "requires": { 328 | "hoek": "4.2.0" 329 | } 330 | }, 331 | "browserify-mime": { 332 | "version": "1.2.9", 333 | "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", 334 | "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" 335 | }, 336 | "buffer-equal-constant-time": { 337 | "version": "1.0.1", 338 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 339 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 340 | }, 341 | "buffer-writer": { 342 | "version": "1.0.1", 343 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz", 344 | "integrity": "sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=" 345 | }, 346 | "caseless": { 347 | "version": "0.12.0", 348 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 349 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 350 | }, 351 | "chalk": { 352 | "version": "1.1.3", 353 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 354 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 355 | "requires": { 356 | "ansi-styles": "2.2.1", 357 | "escape-string-regexp": "1.0.5", 358 | "has-ansi": "2.0.0", 359 | "strip-ansi": "3.0.1", 360 | "supports-color": "2.0.0" 361 | } 362 | }, 363 | "co": { 364 | "version": "4.6.0", 365 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 366 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 367 | }, 368 | "combined-stream": { 369 | "version": "1.0.5", 370 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", 371 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", 372 | "requires": { 373 | "delayed-stream": "1.0.0" 374 | } 375 | }, 376 | "commander": { 377 | "version": "2.11.0", 378 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 379 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" 380 | }, 381 | "core-util-is": { 382 | "version": "1.0.2", 383 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 384 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 385 | }, 386 | "cryptiles": { 387 | "version": "3.1.2", 388 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", 389 | "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", 390 | "requires": { 391 | "boom": "5.2.0" 392 | }, 393 | "dependencies": { 394 | "boom": { 395 | "version": "5.2.0", 396 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 397 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 398 | "requires": { 399 | "hoek": "4.2.0" 400 | } 401 | } 402 | } 403 | }, 404 | "dashdash": { 405 | "version": "1.14.1", 406 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 407 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 408 | "requires": { 409 | "assert-plus": "1.0.0" 410 | } 411 | }, 412 | "date-utils": { 413 | "version": "1.2.21", 414 | "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", 415 | "integrity": "sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q=" 416 | }, 417 | "delayed-stream": { 418 | "version": "1.0.0", 419 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 420 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 421 | }, 422 | "duplexer": { 423 | "version": "0.1.1", 424 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 425 | "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" 426 | }, 427 | "ecc-jsbn": { 428 | "version": "0.1.1", 429 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 430 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 431 | "optional": true, 432 | "requires": { 433 | "jsbn": "0.1.1" 434 | } 435 | }, 436 | "ecdsa-sig-formatter": { 437 | "version": "1.0.9", 438 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", 439 | "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", 440 | "requires": { 441 | "base64url": "2.0.0", 442 | "safe-buffer": "5.1.1" 443 | } 444 | }, 445 | "escape-string-regexp": { 446 | "version": "1.0.5", 447 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 448 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 449 | }, 450 | "extend": { 451 | "version": "3.0.1", 452 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 453 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 454 | }, 455 | "extsprintf": { 456 | "version": "1.3.0", 457 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 458 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 459 | }, 460 | "fast-deep-equal": { 461 | "version": "1.0.0", 462 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", 463 | "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" 464 | }, 465 | "forever-agent": { 466 | "version": "0.6.1", 467 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 468 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 469 | }, 470 | "form-data": { 471 | "version": "2.3.1", 472 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", 473 | "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", 474 | "requires": { 475 | "asynckit": "0.4.0", 476 | "combined-stream": "1.0.5", 477 | "mime-types": "2.1.17" 478 | } 479 | }, 480 | "generate-function": { 481 | "version": "2.0.0", 482 | "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", 483 | "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" 484 | }, 485 | "generate-object-property": { 486 | "version": "1.2.0", 487 | "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", 488 | "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", 489 | "requires": { 490 | "is-property": "1.0.2" 491 | } 492 | }, 493 | "getpass": { 494 | "version": "0.1.7", 495 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 496 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 497 | "requires": { 498 | "assert-plus": "1.0.0" 499 | } 500 | }, 501 | "har-schema": { 502 | "version": "2.0.0", 503 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 504 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 505 | }, 506 | "har-validator": { 507 | "version": "5.0.3", 508 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 509 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", 510 | "requires": { 511 | "ajv": "5.2.3", 512 | "har-schema": "2.0.0" 513 | } 514 | }, 515 | "has-ansi": { 516 | "version": "2.0.0", 517 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 518 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 519 | "requires": { 520 | "ansi-regex": "2.1.1" 521 | } 522 | }, 523 | "hash-base": { 524 | "version": "3.0.4", 525 | "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", 526 | "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", 527 | "requires": { 528 | "inherits": "2.0.3", 529 | "safe-buffer": "5.1.1" 530 | } 531 | }, 532 | "hawk": { 533 | "version": "6.0.2", 534 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", 535 | "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", 536 | "requires": { 537 | "boom": "4.3.1", 538 | "cryptiles": "3.1.2", 539 | "hoek": "4.2.0", 540 | "sntp": "2.0.2" 541 | } 542 | }, 543 | "hoek": { 544 | "version": "4.2.0", 545 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", 546 | "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" 547 | }, 548 | "http-signature": { 549 | "version": "1.2.0", 550 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 551 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 552 | "requires": { 553 | "assert-plus": "1.0.0", 554 | "jsprim": "1.4.1", 555 | "sshpk": "1.13.1" 556 | } 557 | }, 558 | "inherits": { 559 | "version": "2.0.3", 560 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 561 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 562 | }, 563 | "is-buffer": { 564 | "version": "1.1.5", 565 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", 566 | "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" 567 | }, 568 | "is-my-json-valid": { 569 | "version": "2.16.1", 570 | "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", 571 | "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", 572 | "requires": { 573 | "generate-function": "2.0.0", 574 | "generate-object-property": "1.2.0", 575 | "jsonpointer": "4.0.1", 576 | "xtend": "4.0.1" 577 | } 578 | }, 579 | "is-property": { 580 | "version": "1.0.2", 581 | "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", 582 | "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" 583 | }, 584 | "is-stream": { 585 | "version": "1.1.0", 586 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 587 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 588 | }, 589 | "is-typedarray": { 590 | "version": "1.0.0", 591 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 592 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 593 | }, 594 | "isarray": { 595 | "version": "1.0.0", 596 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 597 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 598 | }, 599 | "isstream": { 600 | "version": "0.1.2", 601 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 602 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 603 | }, 604 | "js-string-escape": { 605 | "version": "1.0.1", 606 | "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", 607 | "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=" 608 | }, 609 | "jsbn": { 610 | "version": "0.1.1", 611 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 612 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 613 | "optional": true 614 | }, 615 | "json-edm-parser": { 616 | "version": "0.1.2", 617 | "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", 618 | "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", 619 | "requires": { 620 | "jsonparse": "1.2.0" 621 | } 622 | }, 623 | "json-schema": { 624 | "version": "0.2.3", 625 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 626 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 627 | }, 628 | "json-schema-traverse": { 629 | "version": "0.3.1", 630 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 631 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 632 | }, 633 | "json-stable-stringify": { 634 | "version": "1.0.1", 635 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 636 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 637 | "requires": { 638 | "jsonify": "0.0.0" 639 | } 640 | }, 641 | "json-stringify-safe": { 642 | "version": "5.0.1", 643 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 644 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 645 | }, 646 | "jsonify": { 647 | "version": "0.0.0", 648 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 649 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" 650 | }, 651 | "jsonparse": { 652 | "version": "1.2.0", 653 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", 654 | "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" 655 | }, 656 | "jsonpointer": { 657 | "version": "4.0.1", 658 | "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", 659 | "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" 660 | }, 661 | "jsprim": { 662 | "version": "1.4.1", 663 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 664 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 665 | "requires": { 666 | "assert-plus": "1.0.0", 667 | "extsprintf": "1.3.0", 668 | "json-schema": "0.2.3", 669 | "verror": "1.10.0" 670 | } 671 | }, 672 | "jwa": { 673 | "version": "1.1.5", 674 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", 675 | "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", 676 | "requires": { 677 | "base64url": "2.0.0", 678 | "buffer-equal-constant-time": "1.0.1", 679 | "ecdsa-sig-formatter": "1.0.9", 680 | "safe-buffer": "5.1.1" 681 | } 682 | }, 683 | "jws": { 684 | "version": "3.1.4", 685 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", 686 | "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", 687 | "requires": { 688 | "base64url": "2.0.0", 689 | "jwa": "1.1.5", 690 | "safe-buffer": "5.1.1" 691 | } 692 | }, 693 | "lodash": { 694 | "version": "4.17.4", 695 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 696 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 697 | }, 698 | "md5.js": { 699 | "version": "1.3.4", 700 | "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", 701 | "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", 702 | "requires": { 703 | "hash-base": "3.0.4", 704 | "inherits": "2.0.3" 705 | } 706 | }, 707 | "mime-db": { 708 | "version": "1.30.0", 709 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 710 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 711 | }, 712 | "mime-types": { 713 | "version": "2.1.17", 714 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 715 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 716 | "requires": { 717 | "mime-db": "1.30.0" 718 | } 719 | }, 720 | "moment": { 721 | "version": "2.18.1", 722 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", 723 | "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" 724 | }, 725 | "ms-rest": { 726 | "version": "2.2.1", 727 | "resolved": "https://registry.npmjs.org/ms-rest/-/ms-rest-2.2.1.tgz", 728 | "integrity": "sha1-ZS8J3uicEV5bZyvT3k0W3MeWE3c=", 729 | "requires": { 730 | "@types/node": "7.0.33", 731 | "@types/request": "0.0.45", 732 | "@types/uuid": "2.0.30", 733 | "duplexer": "0.1.1", 734 | "is-buffer": "1.1.5", 735 | "is-stream": "1.1.0", 736 | "moment": "2.18.1", 737 | "request": "2.83.0", 738 | "through": "2.3.8", 739 | "tunnel": "0.0.5", 740 | "uuid": "3.1.0" 741 | } 742 | }, 743 | "ms-rest-azure": { 744 | "version": "2.2.1", 745 | "resolved": "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.2.1.tgz", 746 | "integrity": "sha1-ypqftJKx/hpByo5HLtwd0JIjI+Y=", 747 | "requires": { 748 | "@types/node": "7.0.33", 749 | "@types/uuid": "2.0.30", 750 | "adal-node": "0.1.22", 751 | "async": "0.2.7", 752 | "moment": "2.18.1", 753 | "ms-rest": "2.2.1", 754 | "uuid": "3.1.0" 755 | } 756 | }, 757 | "oauth-sign": { 758 | "version": "0.8.2", 759 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 760 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 761 | }, 762 | "packet-reader": { 763 | "version": "0.3.1", 764 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.3.1.tgz", 765 | "integrity": "sha1-zWLmCvjX/qinBexP+ZCHHEaHHyc=" 766 | }, 767 | "performance-now": { 768 | "version": "2.1.0", 769 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 770 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 771 | }, 772 | "pg": { 773 | "version": "7.3.0", 774 | "resolved": "https://registry.npmjs.org/pg/-/pg-7.3.0.tgz", 775 | "integrity": "sha1-J14nRm5UpkX2tKFvasrfa4Sa2Ds=", 776 | "requires": { 777 | "buffer-writer": "1.0.1", 778 | "js-string-escape": "1.0.1", 779 | "packet-reader": "0.3.1", 780 | "pg-connection-string": "0.1.3", 781 | "pg-pool": "2.0.3", 782 | "pg-types": "1.12.1", 783 | "pgpass": "1.0.2", 784 | "semver": "4.3.2" 785 | } 786 | }, 787 | "pg-connection-string": { 788 | "version": "0.1.3", 789 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", 790 | "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" 791 | }, 792 | "pg-pool": { 793 | "version": "2.0.3", 794 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.3.tgz", 795 | "integrity": "sha1-wCIDLIlJ8xKk+R+2QJzgQHa+Mlc=" 796 | }, 797 | "pg-types": { 798 | "version": "1.12.1", 799 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.12.1.tgz", 800 | "integrity": "sha1-1kCH45A7WP+q0nnnWVxSIIoUw9I=", 801 | "requires": { 802 | "postgres-array": "1.0.2", 803 | "postgres-bytea": "1.0.0", 804 | "postgres-date": "1.0.3", 805 | "postgres-interval": "1.1.1" 806 | } 807 | }, 808 | "pgpass": { 809 | "version": "1.0.2", 810 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 811 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 812 | "requires": { 813 | "split": "1.0.1" 814 | } 815 | }, 816 | "pinkie": { 817 | "version": "2.0.4", 818 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 819 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" 820 | }, 821 | "pinkie-promise": { 822 | "version": "2.0.1", 823 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 824 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 825 | "requires": { 826 | "pinkie": "2.0.4" 827 | } 828 | }, 829 | "postgres-array": { 830 | "version": "1.0.2", 831 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.2.tgz", 832 | "integrity": "sha1-jgsy6wO/d6XAp4UeBEHBaaJWojg=" 833 | }, 834 | "postgres-bytea": { 835 | "version": "1.0.0", 836 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 837 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 838 | }, 839 | "postgres-date": { 840 | "version": "1.0.3", 841 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.3.tgz", 842 | "integrity": "sha1-4tiXAu/bJY/52c7g/pG9BpdSV6g=" 843 | }, 844 | "postgres-interval": { 845 | "version": "1.1.1", 846 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.1.1.tgz", 847 | "integrity": "sha512-OkuCi9t/3CZmeQreutGgx/OVNv9MKHGIT5jH8KldQ4NLYXkvmT9nDVxEuCENlNwhlGPE374oA/xMqn05G49pHA==", 848 | "requires": { 849 | "xtend": "4.0.1" 850 | } 851 | }, 852 | "process-nextick-args": { 853 | "version": "1.0.7", 854 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 855 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" 856 | }, 857 | "punycode": { 858 | "version": "1.4.1", 859 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 860 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 861 | }, 862 | "qs": { 863 | "version": "6.5.1", 864 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 865 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 866 | }, 867 | "readable-stream": { 868 | "version": "2.0.6", 869 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", 870 | "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", 871 | "requires": { 872 | "core-util-is": "1.0.2", 873 | "inherits": "2.0.3", 874 | "isarray": "1.0.0", 875 | "process-nextick-args": "1.0.7", 876 | "string_decoder": "0.10.31", 877 | "util-deprecate": "1.0.2" 878 | } 879 | }, 880 | "request": { 881 | "version": "2.83.0", 882 | "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", 883 | "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", 884 | "requires": { 885 | "aws-sign2": "0.7.0", 886 | "aws4": "1.6.0", 887 | "caseless": "0.12.0", 888 | "combined-stream": "1.0.5", 889 | "extend": "3.0.1", 890 | "forever-agent": "0.6.1", 891 | "form-data": "2.3.1", 892 | "har-validator": "5.0.3", 893 | "hawk": "6.0.2", 894 | "http-signature": "1.2.0", 895 | "is-typedarray": "1.0.0", 896 | "isstream": "0.1.2", 897 | "json-stringify-safe": "5.0.1", 898 | "mime-types": "2.1.17", 899 | "oauth-sign": "0.8.2", 900 | "performance-now": "2.1.0", 901 | "qs": "6.5.1", 902 | "safe-buffer": "5.1.1", 903 | "stringstream": "0.0.5", 904 | "tough-cookie": "2.3.3", 905 | "tunnel-agent": "0.6.0", 906 | "uuid": "3.1.0" 907 | } 908 | }, 909 | "safe-buffer": { 910 | "version": "5.1.1", 911 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 912 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 913 | }, 914 | "sax": { 915 | "version": "0.5.2", 916 | "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.2.tgz", 917 | "integrity": "sha1-c1/6o5oc/4/7lZjwIjq9sDqfsuo=" 918 | }, 919 | "semver": { 920 | "version": "4.3.2", 921 | "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", 922 | "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" 923 | }, 924 | "sntp": { 925 | "version": "2.0.2", 926 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", 927 | "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", 928 | "requires": { 929 | "hoek": "4.2.0" 930 | } 931 | }, 932 | "split": { 933 | "version": "1.0.1", 934 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 935 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 936 | "requires": { 937 | "through": "2.3.8" 938 | } 939 | }, 940 | "sshpk": { 941 | "version": "1.13.1", 942 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", 943 | "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", 944 | "requires": { 945 | "asn1": "0.2.3", 946 | "assert-plus": "1.0.0", 947 | "bcrypt-pbkdf": "1.0.1", 948 | "dashdash": "1.14.1", 949 | "ecc-jsbn": "0.1.1", 950 | "getpass": "0.1.7", 951 | "jsbn": "0.1.1", 952 | "tweetnacl": "0.14.5" 953 | } 954 | }, 955 | "string_decoder": { 956 | "version": "0.10.31", 957 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 958 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 959 | }, 960 | "stringstream": { 961 | "version": "0.0.5", 962 | "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", 963 | "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" 964 | }, 965 | "strip-ansi": { 966 | "version": "3.0.1", 967 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 968 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 969 | "requires": { 970 | "ansi-regex": "2.1.1" 971 | } 972 | }, 973 | "supports-color": { 974 | "version": "2.0.0", 975 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 976 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" 977 | }, 978 | "through": { 979 | "version": "2.3.8", 980 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 981 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 982 | }, 983 | "tough-cookie": { 984 | "version": "2.3.3", 985 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", 986 | "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", 987 | "requires": { 988 | "punycode": "1.4.1" 989 | } 990 | }, 991 | "tunnel": { 992 | "version": "0.0.5", 993 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", 994 | "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" 995 | }, 996 | "tunnel-agent": { 997 | "version": "0.6.0", 998 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 999 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1000 | "requires": { 1001 | "safe-buffer": "5.1.1" 1002 | } 1003 | }, 1004 | "tweetnacl": { 1005 | "version": "0.14.5", 1006 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1007 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1008 | "optional": true 1009 | }, 1010 | "underscore": { 1011 | "version": "1.8.3", 1012 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 1013 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" 1014 | }, 1015 | "util-deprecate": { 1016 | "version": "1.0.2", 1017 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1018 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1019 | }, 1020 | "uuid": { 1021 | "version": "3.1.0", 1022 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 1023 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 1024 | }, 1025 | "validator": { 1026 | "version": "3.35.0", 1027 | "resolved": "https://registry.npmjs.org/validator/-/validator-3.35.0.tgz", 1028 | "integrity": "sha1-PwcklALB/I/Ak8MsbkPXKnnModw=" 1029 | }, 1030 | "verror": { 1031 | "version": "1.10.0", 1032 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1033 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1034 | "requires": { 1035 | "assert-plus": "1.0.0", 1036 | "core-util-is": "1.0.2", 1037 | "extsprintf": "1.3.0" 1038 | } 1039 | }, 1040 | "xml2js": { 1041 | "version": "0.2.7", 1042 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.7.tgz", 1043 | "integrity": "sha1-GDhRi7AXQcrgh4urSRXklMMjBq8=", 1044 | "requires": { 1045 | "sax": "0.5.2" 1046 | } 1047 | }, 1048 | "xmlbuilder": { 1049 | "version": "0.4.3", 1050 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", 1051 | "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=" 1052 | }, 1053 | "xmldom": { 1054 | "version": "0.1.27", 1055 | "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", 1056 | "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" 1057 | }, 1058 | "xpath.js": { 1059 | "version": "1.0.7", 1060 | "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.0.7.tgz", 1061 | "integrity": "sha1-fpRif1QSdsvGprArXTXpQYVls+Q=" 1062 | }, 1063 | "xtend": { 1064 | "version": "4.0.1", 1065 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 1066 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 1067 | } 1068 | } 1069 | } 1070 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sherlock", 3 | "version": "0.5.2", 4 | "description": "Integration testing framework for Microsoft Azure", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/tstringer/sherlock.git" 12 | }, 13 | "keywords": [], 14 | "author": "Thomas Stringer", 15 | "license": "MIT", 16 | "dependencies": { 17 | "azure-arm-authorization": "^4.0.0-preview", 18 | "azure-arm-resource": "^2.0.0-preview", 19 | "azure-graph": "^2.1.0-preview", 20 | "azure-storage": "^2.2.2", 21 | "moment": "^2.18.1", 22 | "pg": "^7.3.0", 23 | "request": "^2.83.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sandbox-provisioning/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /sandbox-provisioning/index.js: -------------------------------------------------------------------------------- 1 | const azurerm = require('azure-arm-resource'); 2 | const msrest = require('ms-rest-azure'); 3 | const AuthClient = require('azure-arm-authorization'); 4 | const azureStorage = require('azure-storage'); 5 | const request = require('request'); 6 | const GraphRbacManagementClient = require('azure-graph'); 7 | 8 | function createResourceGroup(creds, name, region, subscriptionId) { 9 | const resClient = new azurerm.ResourceManagementClient(creds, subscriptionId); 10 | return resClient.resourceGroups.createOrUpdate( 11 | name, 12 | { location: region } 13 | ); 14 | } 15 | 16 | function getServicePrincipal() { 17 | return new Promise((resolve, reject) => { 18 | const identityStorageAccount = process.env['SHERLOCK_IDENTITY_STORAGE_ACCOUNT']; 19 | const identityStorageKey = process.env['SHERLOCK_IDENTITY_STORAGE_KEY']; 20 | const queueName = 'identity'; 21 | const queueService = azureStorage.createQueueService( 22 | identityStorageAccount, 23 | identityStorageKey 24 | ); 25 | queueService.createQueueIfNotExists(queueName, err => { 26 | if (err) { 27 | reject(err); 28 | return; 29 | } 30 | queueService.getQueueMetadata(queueName, (err, result) => { 31 | if (err) { 32 | reject(err); 33 | return; 34 | } 35 | else if (result.approximateMessageCount === 0) { 36 | reject(Error('No available service principals in the queue')); 37 | return; 38 | } 39 | queueService.getMessage(queueName, (err, message) => { 40 | if (err) { 41 | reject(err); 42 | return; 43 | } 44 | const identity_match = /(.*) (.*) (.*)/.exec(message.messageText); 45 | queueService.deleteMessage(queueName, message.messageId, message.popReceipt, err => { 46 | if (err) { 47 | reject(err); 48 | return; 49 | } 50 | resolve({ 51 | objectId: identity_match[1], 52 | appId: identity_match[2], 53 | appObjectId: identity_match[3] 54 | }); 55 | }); 56 | }); 57 | }); 58 | }); 59 | }); 60 | } 61 | 62 | function assignRolesToServicePrincipal(creds, servicePrincipal, subscriptionId, rgName, contributorRoleId, logger) { 63 | logger('In assignRolesToServicePrincipal, servicePrincipal dump:'); 64 | logger(servicePrincipal); 65 | const authClient = new AuthClient(creds, subscriptionId, null); 66 | const scope = `subscriptions/${subscriptionId}/resourceGroups/${rgName}`; 67 | const roleDefinitionId = `${scope}/providers/Microsoft.Authorization/roleDefinitions/${contributorRoleId}`; 68 | return authClient.roleAssignments.create( 69 | scope, 70 | msrest.generateUuid(), 71 | { 72 | properties: { 73 | principalId: servicePrincipal.objectId, 74 | roleDefinitionId: roleDefinitionId, 75 | scope: scope 76 | } 77 | } 78 | ); 79 | } 80 | 81 | function cacheEntityMeta(resourceGroupPrefix, applicationObjectId, expirationTimeMinutes) { 82 | return new Promise((resolve, reject) => { 83 | const metaUrl = process.env['META_URL']; 84 | const metaKey = process.env['META_KEY']; 85 | const requestUrl = `${metaUrl}/?code=${metaKey}&rgprefix=${resourceGroupPrefix}&appobjid=${applicationObjectId}&expire=${expirationTimeMinutes}`; 86 | 87 | request(requestUrl, err => { 88 | if (err) { 89 | reject(err); 90 | return; 91 | } 92 | resolve(); 93 | }); 94 | }); 95 | } 96 | 97 | function getServicePrincipalWithPassword(graphClient, servicePrincipal) { 98 | const spPassword = strongPassword(); 99 | const endDate = new Date(); 100 | endDate.setFullYear(endDate.getFullYear() + 1); 101 | return graphClient.applications.patch( 102 | servicePrincipal.appObjectId, 103 | { 104 | passwordCredentials: [{ 105 | keyId: msrest.generateUuid(), 106 | value: spPassword, 107 | endDate 108 | }] 109 | } 110 | ) 111 | .then(() => Object.assign({}, servicePrincipal, { password: spPassword })); 112 | } 113 | 114 | function strongPassword() { 115 | let password = ''; 116 | 117 | for (let i = 0; i < 8; i++) { 118 | password += Math.random().toString(36).slice(-8); 119 | } 120 | 121 | return password; 122 | } 123 | 124 | function createSandboxEntities(rgCount, region, duration, prefix, logger) { 125 | const clientId = process.env['AZURE_CLIENT_ID']; 126 | const clientSecret = process.env['AZURE_CLIENT_SECRET']; 127 | const subscriptionId = process.env['AZURE_SUBSCRIPTION_ID']; 128 | const tenantId = process.env['AZURE_TENANT_ID']; 129 | const randomNumber = Math.floor(Math.random() * 100000); 130 | const contributorRoleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'; 131 | let cachedCreds; 132 | let spCached; 133 | 134 | const rgNameWithoutSeq = `${prefix}${randomNumber}`; 135 | 136 | const credsForGraph = new msrest.ApplicationTokenCredentials( 137 | clientId, 138 | tenantId, 139 | clientSecret, 140 | { tokenAudience: 'graph' } 141 | ); 142 | 143 | const graphClient = new GraphRbacManagementClient(credsForGraph, tenantId); 144 | 145 | const rgNames = []; 146 | for (let i = 0; i < rgCount; i++) { 147 | rgNames.push(`${rgNameWithoutSeq}-${i}-rg`); 148 | } 149 | 150 | return getServicePrincipal() 151 | .then(sp => { 152 | spCached = sp; 153 | logger('Dumping service principal prior to password injection'); 154 | logger(spCached); 155 | }) 156 | .then(() => getServicePrincipalWithPassword(graphClient, spCached)) 157 | .then(sp => { 158 | // replace the cached service principal with the same 159 | // one that now has the new password injected 160 | spCached = sp; 161 | logger('Dumping service principal post password injection'); 162 | logger(spCached); 163 | }) 164 | .then(() => msrest.loginWithServicePrincipalSecret(clientId, clientSecret, tenantId)) 165 | .then(creds => { 166 | cachedCreds = creds; 167 | logger(`Creating ${rgNames.length} resource group(s)`); 168 | return Promise.all(rgNames.map(rgName => createResourceGroup(creds, rgName, region, subscriptionId))); 169 | }) 170 | .then(() => { 171 | logger('Assigning role to resource group(s)'); 172 | return Promise.all(rgNames.map(rgName => assignRolesToServicePrincipal( 173 | cachedCreds, 174 | spCached, 175 | subscriptionId, 176 | rgName, 177 | contributorRoleId, 178 | logger 179 | ))); 180 | }) 181 | .then(() => { 182 | logger('Caching metadata'); 183 | return cacheEntityMeta(rgNameWithoutSeq, spCached.appObjectId, duration); 184 | }) 185 | .then(() => { 186 | logger('Finalization... returning data to caller'); 187 | return { 188 | resourceGroupNames: rgNames, 189 | clientId: spCached.appId, 190 | clientSecret: spCached.password, 191 | subscriptionId, 192 | tenantId 193 | }; 194 | }); 195 | } 196 | 197 | function resourceGroupMetaData(rgPrefix) { 198 | return new Promise((resolve, reject) => { 199 | const metaKey = process.env['META_KEY']; 200 | const metaUrl = process.env['META_URL']; 201 | request(`${metaUrl}/?code=${metaKey}&all=true&rgprefix=${rgPrefix}`, (err, res, body) => { 202 | if (err) { 203 | reject(err); 204 | return; 205 | } 206 | resolve(JSON.parse(body)); 207 | }); 208 | }); 209 | } 210 | 211 | function deleteSandboxEnvironment(rgPrefix, logger) { 212 | const clientId = process.env['AZURE_CLIENT_ID']; 213 | const clientSecret = process.env['AZURE_CLIENT_SECRET']; 214 | const subscriptionId = process.env['AZURE_SUBSCRIPTION_ID']; 215 | const tenantId = process.env['AZURE_TENANT_ID']; 216 | let resClientCached; 217 | let deletedResourceGroups = []; 218 | let deleteApplications = []; 219 | let rowsCached; 220 | let rgPrefixEntriesToDeleteCachedOperations = []; 221 | 222 | const credsForGraph = new msrest.ApplicationTokenCredentials( 223 | clientId, 224 | tenantId, 225 | clientSecret, 226 | { tokenAudience: 'graph' } 227 | ); 228 | 229 | const graphClient = new GraphRbacManagementClient(credsForGraph, tenantId); 230 | 231 | return resourceGroupMetaData(rgPrefix) 232 | .then(rows => { 233 | rowsCached = rows; 234 | return msrest.loginWithServicePrincipalSecret(clientId, clientSecret, tenantId); 235 | }) 236 | .then(creds => { 237 | const resClient = new azurerm.ResourceManagementClient(creds, subscriptionId); 238 | resClientCached = resClient; 239 | return resClient.resourceGroups.list(); 240 | }) 241 | .then(resourceGroups => { 242 | let deleteResourceGroupOperations = []; 243 | for(let i = 0; i < resourceGroups.length; i++) { 244 | if (resourceGroups[i].name.substring(0, rowsCached[0].resource_group_prefix.length) === rowsCached[0].resource_group_prefix) { 245 | if (deleteApplications.length === 0) { 246 | deleteApplications.push(rowsCached[0].application_object_id); 247 | } 248 | logger(`Deleting ${resourceGroups[i].name}`); 249 | deleteResourceGroupOperations.push(resClientCached.resourceGroups.beginDeleteMethod(resourceGroups[i].name)); 250 | } 251 | } 252 | logger(`deleting ${deleteResourceGroupOperations.length} resource group(s)`); 253 | return Promise.all(deleteResourceGroupOperations); 254 | }) 255 | .then(() => { 256 | const applicationsToDeleteOperations = deleteApplications.map(appObjectIdToDelete => { 257 | logger(`deleting ${appObjectIdToDelete}`); 258 | return graphClient.applications.deleteMethod(appObjectIdToDelete); 259 | }); 260 | return Promise.all(applicationsToDeleteOperations); 261 | }) 262 | .then(() => { 263 | return deleteByRgPrefix(rgPrefix); 264 | }); 265 | } 266 | 267 | function deleteByRgPrefix(rgPrefix) { 268 | return new Promise((resolve, reject) => { 269 | const metaUrl = process.env['META_URL']; 270 | const metaKey = process.env['META_KEY']; 271 | 272 | request.delete(`${metaUrl}/?code=${metaKey}&rgprefix=${rgPrefix}`, err => { 273 | if (err) { 274 | reject(err); 275 | return; 276 | } 277 | resolve(); 278 | }); 279 | }); 280 | } 281 | 282 | module.exports = function (context, req) { 283 | context.log('JavaScript HTTP trigger function processed a request.'); 284 | let rgCount = 1; 285 | let region = 'eastus'; 286 | let duration = 30; // minutes 287 | let requestPrefix = ''; 288 | 289 | if (req.method === 'DELETE') { 290 | if (!req.query.rgprefix && !(req.body && req.body.rgprefix)) { 291 | context.res = { status: 400, body: 'To delete a resource you must pass rgprefix' }; 292 | context.done(); 293 | return; 294 | } 295 | deleteSandboxEnvironment(req.query.rgprefix || req.body.rgprefix, context.log) 296 | .then(() => context.done()) 297 | .catch((err) => { 298 | context.log(`Error deleting ${req.query.rgprefix || req.body.rgprefix}`); 299 | context.log(err); 300 | context.res = { status: 400, body: `Error delete ${req.query.rgprefix || req.body.rgprefix}` }; 301 | context.done(); 302 | }); 303 | return; 304 | } 305 | 306 | let prefix = process.env['RES_PREFIX'] || 'sherlock'; 307 | 308 | if (req.query.rgcount || (req.body && req.body.rgcount)) { 309 | rgCount = req.query.rgcount || req.body.rgcount; 310 | } 311 | 312 | if (req.query.region || (req.body && req.body.region)) { 313 | region = req.query.region || req.body.region; 314 | } 315 | 316 | if (req.query.duration || (req.body && req.body.duration)) { 317 | duration = req.query.duration || req.body.duration; 318 | } 319 | 320 | if (req.query.prefix || (req.body && req.body.prefix)) { 321 | requestPrefix = req.query.prefix || req.body.prefix; 322 | prefix += `-${requestPrefix}-`; 323 | } 324 | 325 | createSandboxEntities(rgCount, region, duration, prefix, context.log) 326 | .then(sandboxResult => { 327 | context.log(sandboxResult); 328 | context.res = { body: sandboxResult }; 329 | context.done(); 330 | }) 331 | .catch(err => { 332 | context.log(`Error ${err.message}`); 333 | context.log(err); 334 | context.res = { 335 | status: 400, 336 | body: err 337 | }; 338 | context.done(); 339 | }); 340 | }; 341 | -------------------------------------------------------------------------------- /setup/README.md: -------------------------------------------------------------------------------- 1 | # Setup with an Ansible Playbook 2 | 3 | This is a way to setup Sherlock in you Azure subscription, through an Ansible playbook 4 | 5 | ## Variables 6 | 7 | 1. Copy the sample vars file: `$ cp setup/vars.yml.sample setup/vars.yml` 8 | 1. Add the necessary values into the new `vars.yml` file 9 | 10 | ## Azure CLI 11 | 12 | At the moment, this playbook requires the Azure CLI to be on localhost. To install the Azure CLI you can do the following: 13 | 14 | 1. Install the Azure CLI (if you don't already have it): `$ curl -L https://aka.ms/InstallAzureCli | bash` 15 | 1. Login to your subscription: `$ az login` 16 | 17 | :bulb: If you have multiple subscriptions (viewable through `az account list`) ensure that your desired target subscription is the default: 18 | 19 | 1. List all registered subscriptions: `$ az account list --query "[*].{name: name, id: id, isDefault: isDefault}"` 20 | 1. If your target subscription isn't currently default, set it: `$ az account set -s ` 21 | 22 | :bulb: Because this playbook currently relies on a shell program, it is not idempotent at the moment (this will be changed when there is Ansible functionality for Azure Functions (coming soon)) 23 | 24 | ## Run the playbook 25 | 26 | ``` 27 | $ ansible-playbook main.yml 28 | ``` 29 | 30 | ## Get the Azure Function URL and key 31 | 32 | Navigate to the [Azure Portal](https://portal.azure.com) to retrieve the Function URL and key(s) 33 | -------------------------------------------------------------------------------- /setup/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create sherlock in azure 3 | hosts: localhost 4 | vars_files: 5 | - vars.yml 6 | 7 | tasks: 8 | - name: check for azure cli 9 | command: which az 10 | changed_when: false 11 | 12 | - name: create resource group 13 | azure_rm_resourcegroup: 14 | name: '{{ resource_group }}' 15 | location: '{{ location }}' 16 | 17 | - name: create storage account 18 | azure_rm_storageaccount: 19 | resource_group: '{{ resource_group }}' 20 | name: '{{ storage_account }}' 21 | account_type: Standard_LRS 22 | 23 | - name: create azure function app 24 | command: az functionapp create -g {{ resource_group }} -n {{ app }} -s {{ storage_account }} -u {{ git_repo_uri }} --consumption-plan-location {{ location }} 25 | 26 | - name: set app function app settings 27 | command: az functionapp config appsettings set -g {{ resource_group }} -n {{ app }} --settings \ 28 | AZURE_CLIENT_ID={{ azure_client_id }} \ 29 | AZURE_CLIENT_SECRET={{ azure_client_secret }} \ 30 | AZURE_SUBSCRIPTION_ID={{ azure_subscription_id }} \ 31 | AZURE_TENANT_ID={{ azure_tenant_id }} 32 | -------------------------------------------------------------------------------- /setup/vars.yml.sample: -------------------------------------------------------------------------------- 1 | resource_group: sherlock-rg 2 | location: eastus 3 | storage_account: sherlockstor 4 | app: sherlocktesting 5 | git_repo_uri: https://github.com/tstringer/sherlock.git 6 | azure_client_id: ... 7 | azure_client_secret: ... 8 | azure_subscription_id: ... 9 | azure_tenant_id: ... 10 | --------------------------------------------------------------------------------