Skip to content
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
1 change: 0 additions & 1 deletion .eslintrc.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .gitattributes

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .projen/files.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 2 additions & 23 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ const project = new awscdk.AwsCdkConstructLibrary({
authorAddress: 'jonathan.goldwasser@gmail.com',
description: 'High-level constructs for AWS CDK',
jsiiVersion: '5.x',
cdkVersion: '2.133.0',
cdkVersion: '2.232.0',
name: 'cloudstructs',
projenrcTs: true,
tsJestOptions: { transformOptions: { isolatedModules: true } },
peerDeps: [],
bundledDeps: [
'got',
'@aws/durable-execution-sdk-js',
'@slack/web-api',
'got',
'mjml',
],
devDeps: [
Expand All @@ -26,8 +28,10 @@ const project = new awscdk.AwsCdkConstructLibrary({
'@aws-sdk/client-s3',
'@aws-sdk/client-secrets-manager',
'@aws-sdk/client-sfn',
'@aws-sdk/client-sns',
'@aws-sdk/client-textract',
'@aws-sdk/lib-dynamodb',
'@aws/durable-execution-sdk-js-testing',
'@types/aws-lambda',
'@types/mjml',
'@types/tsscmp',
Expand Down Expand Up @@ -77,7 +81,7 @@ const packageExports: Record<string, string> = {
'./.jsii': './.jsii',
};
for (const dirent of fs.readdirSync('./src', { withFileTypes: true })) {
if (dirent.isDirectory()) {
if (dirent.isDirectory() && dirent.name !== 'utils') {
const construct = dirent.name;
// TODO: remove "lib" when TypeScript supports "exports"
packageExports[`./lib/${construct}`] = `./lib/${construct}/index.js`;
Expand Down
23 changes: 19 additions & 4 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions src/ssl-server-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,28 @@ export class MyStack extends Stack {
super(scope, id, props);

new SslServerTest(this, 'TestMyHost', {
registrationEmail: 'jdoe@someorganizationemail.com',
host: 'my.host'
});
}
}
```

This will create a state machine that will run a SSL server test everyday. By default, a SNS topic is
created and a notification is sent to this topic if the [grade](https://github.com/ssllabs/research/wiki/SSL-Server-Rating-Guide)
This will create a durable Lambda function that will run a SSL server test everyday. By default, a SNS
topic is created and a notification is sent to this topic if the [grade](https://github.com/ssllabs/research/wiki/SSL-Server-Rating-Guide)
of the test is below `A+`. The content of the notification is the
[test result returned by the API](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md#response-objects).
[test result returned by the API](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v4.md#response-objects).

A [free registration](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v4.md#register-for-scan-api-initiation-and-result-fetching) is required to use the the SSL Labs API.

```ts
const myTest = new SslServerTest(this, 'MyTest', {
registrationEmail: 'jdoe@someorganizationemail.com',
host: 'my.host',
});

myTest.alarmTopic.addSubscription(/* your subscription here */)
```

Use the `minimumGrade`, `alarmTopic` or `schedule` props to customize the
Use the `minimumGrade`, `alarmTopic` or `scheduleExpression` props to customize the
behavior of the construct.

<p align="center">
<img src="ssl-server-test.svg" width="50%">
</p>
70 changes: 63 additions & 7 deletions src/ssl-server-test/analyze.lambda.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,69 @@
import { DurableContext, withDurableExecution } from '@aws/durable-execution-sdk-js';
import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';
import got from 'got';
import { getEnv } from '../utils';
import { AnalyzeResponse, AnalyzeStatus, SslServerTestGrade } from './types';

const sslLabsClient = got.extend({
prefixUrl: 'https://api.ssllabs.com/api/v3',
prefixUrl: 'https://api.ssllabs.com/api/v4',
headers: { email: getEnv('REGISTRATION_EMAIL') },
searchParams: {
host: getEnv('HOST'),
},
});

export async function handler(event: Record<string, string>) {
const response = await sslLabsClient('analyze', {
searchParams: event,
}).json();
const snsClient = new SNSClient({});

return response;
}
export const handler = withDurableExecution(async (_, context: DurableContext) => {
// Start analysis
await context.step('start-analysis', async () => {
const response = await sslLabsClient('analyze', {
searchParams: { startNew: 'on' },
}).json();
context.logger.info('Started SSL analysis', response);
});

// Wait for analysis to complete
const analysis = await context.waitForCondition<AnalyzeResponse>('wait-for-completion', async (_state, waitContext) => {
const response = await sslLabsClient('analyze').json<AnalyzeResponse>();
waitContext.logger.info('Current analysis status', response);
return response;
}, {
initialState: { status: AnalyzeStatus.DNS, endpoints: [] },
waitStrategy: (state) => {
if (state.status === AnalyzeStatus.READY || state.status === AnalyzeStatus.ERROR) {
return { shouldContinue: false };
}
return { shouldContinue: true, delay: { seconds: 30 } };
},
});

if (analysis.status === AnalyzeStatus.ERROR) {
throw new Error(`Analysis failed: ${analysis.statusMessage}`);
}

const grades = Object.values(SslServerTestGrade).reverse();

const gradeIndices = analysis.endpoints
.map(e => grades.indexOf(e.grade as SslServerTestGrade))
.filter(index => index !== -1);

if (gradeIndices.length === 0) {
throw new Error('No valid grade found in analysis result');
}

const bestGradeIndex = Math.max(...gradeIndices);
const bestGrade = grades[bestGradeIndex];

if (bestGradeIndex < grades.indexOf(getEnv('MINIMUM_GRADE') as SslServerTestGrade)) {
await context.step('notify', async () => {
await snsClient.send(new PublishCommand({
TopicArn: getEnv('ALARM_TOPIC_ARN'),
Message: JSON.stringify(analysis),
Subject: `SSL grade for ${getEnv('HOST')} is below minimum grade (${bestGrade} < ${getEnv('MINIMUM_GRADE')})`,
}));
});
}

context.logger.info('SSL analysis completed successfully', analysis);
});
Loading
Loading