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

Feature/11596 cloud watch logs data protection #11599

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/providers/aws/guide/functions.md
Expand Up @@ -591,6 +591,8 @@ You can opt out of the default behavior by setting `disableLogs: true`

You can also specify the duration for CloudWatch log retention by setting `logRetentionInDays`.

You can specify the DataProtectionPolicy for the LogGroup by setting `logDataProtectionPolicy`. On how to define the policy consult the [aws docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data-start.html).

```yml
functions:
hello:
Expand All @@ -599,6 +601,8 @@ functions:
goodBye:
handler: handler.goodBye
logRetentionInDays: 14
logDataProtectionPolicy:
Name: data-protection-policy
```

## Versioning Deployed Functions
Expand Down
4 changes: 4 additions & 0 deletions docs/providers/aws/guide/serverless.yml.md
Expand Up @@ -128,6 +128,10 @@ provider:
# Duration for CloudWatch log retention (default: forever)
# Valid values: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html
logRetentionInDays: 14
# Policy defining how to monitor and mask sensitive data in CloudWatch logs
# Policy format: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data-start.html
logDataProtectionPolicy:
Name: data-protection-policy
# KMS key ARN to use for encryption for all functions
kmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash
# Version of hashing algorithm used by Serverless Framework for function packaging
Expand Down
1 change: 1 addition & 0 deletions lib/plugins/aws/custom-resources/index.js
Expand Up @@ -197,6 +197,7 @@ async function addCustomResourceToService(awsProvider, resourceName, iamRoleStat
Properties: {
LogGroupName: awsProvider.naming.getLogGroupName(absoluteFunctionName),
RetentionInDays: awsProvider.getLogRetentionInDays(),
DataProtectionPolicy: awsProvider.getLogDataProtectionPolicy(),
},
},
});
Expand Down
Expand Up @@ -113,5 +113,10 @@ function getLogGroupResource(service, stage, provider) {
resource.Properties.RetentionInDays = logRetentionInDays;
}

const logDataProtectionPolicy = provider.getLogDataProtectionPolicy();
if (logDataProtectionPolicy) {
resource.Properties.DataProtectionPolicy = logDataProtectionPolicy;
}

return resource;
}
5 changes: 5 additions & 0 deletions lib/plugins/aws/package/compile/events/http-api.js
Expand Up @@ -143,6 +143,11 @@ class HttpApiEvents {
resource.Properties.RetentionInDays = logRetentionInDays;
}

const logDataProtectionPolicy = this.provider.getLogDataProtectionPolicy();
if (logDataProtectionPolicy) {
resource.Properties.DataProtectionPolicy = logDataProtectionPolicy;
}

this.cfTemplate.Resources[this.provider.naming.getHttpApiLogGroupLogicalId()] = resource;
}
compileStage() {
Expand Down
Expand Up @@ -108,5 +108,9 @@ function getLogGroupResource(service, stage, provider) {
if (logRetentionInDays) {
resource.Properties.RetentionInDays = logRetentionInDays;
}
const logDataProtectionPolicy = provider.getLogDataProtectionPolicy();
if (logDataProtectionPolicy) {
resource.Properties.DataProtectionPolicy = logDataProtectionPolicy;
}
return resource;
}
6 changes: 6 additions & 0 deletions lib/plugins/aws/package/lib/merge-iam-templates.js
Expand Up @@ -32,6 +32,12 @@ module.exports = {
newLogGroup[logGroupLogicalId].Properties.RetentionInDays = logRetentionInDays;
}

const logDataProtectionPolicy =
functionObject.logDataProtectionPolicy || this.provider.getLogDataProtectionPolicy();
if (logDataProtectionPolicy) {
newLogGroup[logGroupLogicalId].Properties.DataProtectionPolicy = logDataProtectionPolicy;
}

_.merge(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newLogGroup
Expand Down
21 changes: 21 additions & 0 deletions lib/plugins/aws/provider.js
Expand Up @@ -675,6 +675,17 @@ class AwsProvider {
3288, 3653,
],
},
awsLogDataProtectionPolicy: {
type: 'object',
properties: {
Name: { type: 'string' },
Description: { type: 'string' },
Version: { type: 'string' },
Statement: { type: 'array' },
},
additionalProperties: false,
required: ['Name', 'Version', 'Statement'],
},
awsResourceCondition: { type: 'string' },
awsResourceDependsOn: { type: 'array', items: { type: 'string' } },
awsResourcePolicyResource: {
Expand Down Expand Up @@ -1105,6 +1116,9 @@ class AwsProvider {
logRetentionInDays: {
$ref: '#/definitions/awsLogRetentionInDays',
},
logDataProtectionPolicy: {
$ref: '#/definitions/awsLogDataProtectionPolicy',
},
logs: {
type: 'object',
properties: {
Expand Down Expand Up @@ -1374,6 +1388,9 @@ class AwsProvider {
logRetentionInDays: {
$ref: '#/definitions/awsLogRetentionInDays',
},
logDataProtectionPolicy: {
$ref: '#/definitions/awsLogDataProtectionPolicy',
},
maximumEventAge: { type: 'integer', minimum: 60, maximum: 21600 },
maximumRetryAttempts: { type: 'integer', minimum: 0, maximum: 2 },
memorySize: { $ref: '#/definitions/awsLambdaMemorySize' },
Expand Down Expand Up @@ -1883,6 +1900,10 @@ class AwsProvider {
return this.serverless.service.provider.logRetentionInDays;
}

getLogDataProtectionPolicy() {
return this.serverless.service.provider.logDataProtectionPolicy;
}

getStageSourceValue() {
const values = this.getValues(this, [
['options', 'stage'],
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/programmatic/http-api/serverless.yml
Expand Up @@ -7,6 +7,10 @@ provider:
name: aws
runtime: nodejs12.x
logRetentionInDays: 14
logDataProtectionPolicy:
Name: data-protection-policy
Version: 2021-06-01
Statement: []

functions:
foo:
Expand Down
Expand Up @@ -379,4 +379,32 @@ describe('test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/stage/
},
});
});

it('should set DataProtectionPolicy if provider.logDataProtectionPolicy is set', async () => {
const policy = {
Name: 'data-protection-policy',
Version: '2021-06-01',
Statement: [],
};
const { cfTemplate, awsNaming, serverless } = await runServerless({
fixture: 'api-gateway',
command: 'package',
configExt: {
provider: {
logs: {
restApi: true,
},
logDataProtectionPolicy: policy,
},
},
});

expect(cfTemplate.Resources[awsNaming.getApiGatewayLogGroupLogicalId()]).to.deep.equal({
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: `/aws/api-gateway/${serverless.service.service}-dev`,
DataProtectionPolicy: policy,
},
});
});
});
Expand Up @@ -247,7 +247,13 @@ describe('lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js',

describe('logs with full custom options', () => {
let resource;
let logGroupResource;
const customLogFormat = ['$context.identity.sourceIp', '$context.requestId'].join(' ');
const logDataProtectionPolicy = {
Name: 'data-protection-policy',
Version: '2021-06-01',
Statement: [],
};

before(async () => {
const { cfTemplate, awsNaming } = await runServerless({
Expand All @@ -263,12 +269,14 @@ describe('lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js',
format: customLogFormat,
},
},
logDataProtectionPolicy,
},
},
command: 'package',
});
const stageLogicalId = awsNaming.getWebsocketsStageLogicalId();
resource = cfTemplate.Resources[stageLogicalId];
logGroupResource = cfTemplate.Resources[awsNaming.getWebsocketsLogGroupLogicalId()];
});

it('should set accessLogging off', async () => {
Expand All @@ -282,5 +290,11 @@ describe('lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js',
it('should set fullExecutionData true', async () => {
expect(resource.Properties.DefaultRouteSettings.DataTraceEnabled).to.equal(true);
});

it('should set DataProtectionPolicy', () => {
expect(logGroupResource.Properties.DataProtectionPolicy).to.deep.equal(
logDataProtectionPolicy
);
});
});
});
31 changes: 31 additions & 0 deletions test/unit/lib/plugins/aws/package/lib/merge-iam-templates.test.js
Expand Up @@ -306,6 +306,11 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => {
subnetIds: ['xxx'],
},
logRetentionInDays: 5,
logDataProtectionPolicy: {
Name: 'data-protection-policy',
Version: '2021-06-01',
Statement: [],
},
},
},
});
Expand Down Expand Up @@ -429,6 +434,12 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => {
expect(iamResource.Properties.LogGroupName).to.be.equal(`/aws/lambda/${service}-dev-basic`);
});

it('should support `provider.logDataProtectionPolicy`', () => {
const normalizedName = naming.getLogGroupLogicalId('basic');
const iamResource = cfResources[normalizedName];
expect(iamResource.Properties.DataProtectionPolicy.Name).to.equal('data-protection-policy');
});

it('should support `provider.iam.role.tags`', () => {
const IamRoleLambdaExecution = naming.getRoleLogicalId();
const iamResource = cfResources[IamRoleLambdaExecution];
Expand Down Expand Up @@ -477,6 +488,14 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => {
handler: 'index.handler',
logRetentionInDays: 5,
},
fnLogDataProtectionPolicy: {
handler: 'index.handler',
logDataProtectionPolicy: {
Name: 'data-protection-policy',
Version: '2021-06-01',
Statement: [],
},
},
fnWithVpc: {
handler: 'index.handler',
vpc: {
Expand Down Expand Up @@ -531,6 +550,18 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => {
);
});

it('should support `functions[].logDataProtectionPolicy`', async () => {
const functionName = serverless.service.getFunction('fnLogDataProtectionPolicy').name;
const normalizedName = naming.getLogGroupLogicalId('fnLogDataProtectionPolicy');
const logResource = cfResources[normalizedName];

expect(logResource.Type).to.be.equal('AWS::Logs::LogGroup');
expect(logResource.Properties.DataProtectionPolicy.Name).to.equal('data-protection-policy');
expect(logResource.Properties.LogGroupName).to.be.equal(
naming.getLogGroupName(functionName)
);
});

it('should not have allow rights to put logs for custom named function when disableLogs option is enabled', async () => {
expect(
cfResources[naming.getRoleLogicalId()].Properties.Policies[0].PolicyDocument.Statement[0]
Expand Down