Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Added infrastructure tests #696

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/deploy-integ-appstream-egress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ jobs:
run: |
cp ./main/end-to-end-tests/e2eGitHubConfig.AppStreamEgress.yml ./main/config/settings/${STAGE_NAME}.yml
./scripts/environment-deploy.sh ${STAGE_NAME}
infrastructure-test:
name: Infrastructure test
runs-on: ubuntu-18.04
needs: deploy
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install pnpm and system libraries
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install
working-directory: main/infrastructure-tests
- name: Run infrastructure tests
run: pnpm run testAppStreamEgressEnabled -- --stage=github
working-directory: ./main/infrastructure-tests
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_APPSTREAM_EGRESS }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_APPSTREAM_EGRESS }}
INFRA_TESTS_HOSTING_ACCOUNT_ID: ${{ secrets.INFRA_TESTS_HOSTING_ACCOUNT_ID }}
INFRA_TESTS_HOSTING_ACCOUNT_STACK_NAME: ${{ secrets.INFRA_TESTS_HOSTING_ACCOUNT_STACK_NAME }}
integration-test:
name: Integration test
runs-on: ubuntu-18.04
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ docs/build
*/*/*/config/settings/*.yml
!*/*/*/config/settings/.defaults.yml
!*/*/*/config/settings/example*.yml
!main/infrastructure-tests/config/settings/github.yml

# rStudio specific
source/ServiceWorkbenchOnAWS/main/solution/machine-images/config/infra/files/rstudio/*
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,47 @@ Resources:
- cloudformation:GetTemplate
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/initial-stack*'

# This role is used by `infrastructure-tests`
InfrastructureTestRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Join ['-', [Ref: Namespace, 'infrastructure-test-role']]
Path: '/'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
AWS:
- !Join [':', ['arn:aws:iam:', Ref: CentralAccountId, 'root']]
Action:
- 'sts:AssumeRole'
Condition:
StringEquals:
sts:ExternalId: !Ref ExternalId
ManagedPolicyArns:
- !Ref PolicyInfrastructureTest
PermissionsBoundary: !Ref PolicyInfrastructureTest

PolicyInfrastructureTest:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows infrastructure tests to access hosting acccount CFN resources
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cloudformation:DescribeStacks
- cloudformation:DescribeStackResources
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/initial-stack*'
- Effect: Allow
Action:
- ec2:DescribeSubnets
- ec2:DescribeSecurityGroups
- ec2:DescribeRouteTables
Resource: '*'

# VPC for launching EMR clusters into
# Just one AZ as we're aiming for transient low-cost clusters rather than HA
VPC:
Expand Down
25 changes: 25 additions & 0 deletions main/infrastructure-tests/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
"plugins": ["jest", "prettier"],
"rules": {
"prettier/prettier": ["error"],
"no-unused-vars": [
"error",
{
"varsIgnorePattern": "^_.+",
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"args": "after-used",
"ignoreRestSiblings": true
}
],
"prefer-destructuring": 0,
"no-underscore-dangle": 0,
"no-param-reassign": 0,
"class-methods-use-this": 0,
"no-use-before-define": 0
},
"env": {
"jest/globals": true
}
}
17 changes: 17 additions & 0 deletions main/infrastructure-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
**/.class
**/.DS_Store
**/coverage
**/node_modules

**/npm-debug.log
**/pnpm-debug.log
.build

local-events/*

# VisualStudioCode.gitignore
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
16 changes: 16 additions & 0 deletions main/infrastructure-tests/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"tabWidth": 2,
"printWidth": 120,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "all",
"overrides": [
{
"files": ["*.yml", "*.yaml"],
"options": {
"singleQuote": false
}
}
]
}

12 changes: 12 additions & 0 deletions main/infrastructure-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Infrastructure Tests for SWB
This test suite checks if the hosting account Cloudformation stack is set up with the correct security settings. Tests were added
to ensure that if AppStream and Egress are enabled, the stack does not have subnets and security group with internet connectivity.
nguyen102 marked this conversation as resolved.
Show resolved Hide resolved

# Prerequisites
Create a `config` file at `main/infrastructures-tests/config/settings/<STAGE>.yml`. You can use `main/infrastructures-tests/config/settings/example.yml` as
an example.

# Running tests
After the config file is created, you can run the command below in `main/infrastructures-tests/` directory to start your tests.

`pnpm run testAppStreamEgressEnabled -- --stage=<STAGE>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

const AWS = require('aws-sdk');
const setupAws = require('../../support/setupAws');
const { getCFStackResources, getStackResourcesByType } = require('../../support/utilities');

describe('Route tables', () => {
beforeAll(async () => {
await setupAws();
});

it('should not point to any internet gateways', async () => {
const stackResources = await getCFStackResources();
const ec2 = new AWS.EC2();

// Grab Route tables created for AppStream VPC
const vpcId = await getStackResourcesByType('VPC', stackResources);
const routeTablesForAppStreamVpcResponse = await ec2
.describeRouteTables({
Filters: [
{
Name: 'vpc-id',
Values: vpcId,
},
],
})
.promise();

// Get Route Tables created by the stack
const routeTableIds = await getStackResourcesByType('AWS::EC2::RouteTable', stackResources);
const workspaceRouteTableResponse = await ec2
.describeRouteTables({
Filters: [
{
Name: 'route-table-id',
Values: routeTableIds,
},
],
})
.promise();

// Check that no route tables point to an Internet Gateway
checkRouteTableDoesNotHaveIGW(routeTablesForAppStreamVpcResponse.RouteTables);
checkRouteTableDoesNotHaveIGW(workspaceRouteTableResponse.RouteTables);
});

function checkRouteTableDoesNotHaveIGW(routeTables) {
let gatewayIds = [];
routeTables.forEach(rtb => {
const ids = rtb.Routes.map(route => {
return route.GatewayId;
});
gatewayIds = [...gatewayIds, ...ids];
});

expect(
gatewayIds.some(gatewayId => {
// Check whether route table points to any Internet Gateways
return gatewayId.includes('igw-');
}),
).toEqual(false);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

const AWS = require('aws-sdk');
const setupAws = require('../../support/setupAws');
const { getStackResourcesByType } = require('../../support/utilities');

describe('Security groups', () => {
beforeAll(async () => {
await setupAws();
});

it('should have least privilege', async () => {
const ec2 = new AWS.EC2();

// Get all Security Group resources
const sgIds = await getStackResourcesByType('AWS::EC2::SecurityGroup');

// Get details about each Security Group
const securityGroupsResponse = await ec2
.describeSecurityGroups({
GroupIds: sgIds,
})
.promise();

// Check Security Group inbound rules
checkSGsDoesNotAllowInboundAccessFromEverywhere(securityGroupsResponse.SecurityGroups);
});

function checkSGsDoesNotAllowInboundAccessFromEverywhere(securityGroups) {
const cidrIpsOfInboundRules = [];
securityGroups.forEach(sg => {
sg.IpPermissions.forEach(ipPermission => {
ipPermission.IpRanges.forEach(ipRange => {
cidrIpsOfInboundRules.push(ipRange.CidrIp);
});
});
});

// Check SG does not allow inbound access from all IP addresses
expect(
cidrIpsOfInboundRules.filter(cidr => {
return cidr === '0.0.0.0/0';
}).length,
).toEqual(0);
}
});
25 changes: 25 additions & 0 deletions main/infrastructure-tests/config/settings/example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The AWS region where the service workbench application is deployed
awsRegion: us-east-1

# AWS profile with permissions to the main account. Example: default
awsProfile:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd fill out an example value for attributes here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add an example value in the comments, since I don't want people to accidentally deploy to default. I want to make sure that they purposefully pick a profile.


# Make sure that the solutionName matches the one you are using in /main/config/settings
solutionName:

# Required. Usually, this is the same as the stage name that you used when you deployed the service
# workbench application
envName:

# Make sure that the envType matches the one you are using in /main/config/settings
envType: dev

# Account id of hosting account that you would like to run infrastructure tests on
hostingAccountId: <12 Digits AWS Account Number>

# The name of the Cloudformation stack that SWB created for the hosting account
# E.g: initial-stack-1628108240381
hostingAccountStackName:

# ExternalId set for the trust relationship of the assumed role in the hosting account. By default it's `workbench`
externalId: workbench
15 changes: 15 additions & 0 deletions main/infrastructure-tests/config/settings/github.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# The AWS region where the service workbench application is deployed
awsRegion: us-east-1

# Make sure that the solutionName matches the one you are using in /main/config/settings
solutionName: sw

# Required. Usually, this is the same as the stage name that you used when you deployed the service
# workbench application
envName: tre

# Make sure that the envType matches the one you are using in /main/config/settings
envType: secure

# ExternalId set for the trust relationship of the assumed role in the hosting account. By default it's `workbench`
externalId: workbench
49 changes: 49 additions & 0 deletions main/infrastructure-tests/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
const _ = require('lodash');
const path = require('path');
const fs = require('fs-extra');
const yaml = require('js-yaml');
const parse = require('yargs-parser');

async function init() {
const parsedArgs = parse(process.argv);

// Get the stage argument either from the command line args or from the process environment variables
const stage = parsedArgs.stage || parsedArgs.s || process.env.STAGE;
if (_.isEmpty(stage)) {
throw new Error(
'No "stage" argument was passed. Please pass the stage name via the command line.\nThe "stage" is your yaml configuration file name (without .yml).\nExample: $ pnpm testAppStreamEgressEnabled -- --stage=<stage name>\n',
);
}
// Using the stage name, we can now load the configuration settings yaml file
const yamlFile = path.join(__dirname, `./config/settings/${stage}.yml`);
return yaml.load(await fs.readFile(yamlFile, 'utf8'));
}

// async function that returns the configuration
module.exports = async () => {
const settings = await init();
return {
rootDir: __dirname,
verbose: false,
notify: false,
testEnvironment: 'node',
testTimeout: 60 * 60 * 1000,
globals: {
__settings__: settings,
},
};
};
Loading