Skip to content

Commit

Permalink
[lambda][flare] Obfuscate secrets (#947)
Browse files Browse the repository at this point in the history
Obfuscate secrets
  • Loading branch information
nhulston authored Jul 3, 2023
1 parent 54f754c commit 9bddc95
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 21 deletions.
50 changes: 32 additions & 18 deletions src/commands/lambda/__tests__/__snapshots__/flare.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exports[`lambda flare AWS Lambda configuration prints config when running as a d
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -55,7 +55,7 @@ exports[`lambda flare AWS credentials continues when getAWSCredentials() returns
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -86,7 +86,7 @@ exports[`lambda flare AWS credentials requests AWS credentials when none are fou
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -145,7 +145,7 @@ exports[`lambda flare gets CloudWatch Logs does not get logs when --with-logs is
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -176,7 +176,7 @@ exports[`lambda flare gets CloudWatch Logs gets logs, saves, and sends correctly
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -217,7 +217,7 @@ exports[`lambda flare gets CloudWatch Logs prints error when getLogEvents throws
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand All @@ -244,7 +244,7 @@ exports[`lambda flare gets CloudWatch Logs prints error when getLogStreamNames t
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand All @@ -271,7 +271,7 @@ exports[`lambda flare gets CloudWatch Logs warns and skips getting logs when get
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -305,7 +305,7 @@ exports[`lambda flare gets CloudWatch Logs warns and skips log when getLogEvents
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand All @@ -330,6 +330,20 @@ exports[`lambda flare gets CloudWatch Logs warns and skips log when getLogEvents
"
`;

exports[`lambda flare maskConfig should mask API key but not whitelisted environment variables 1`] = `
Object {
"Environment": Object {
"Variables": Object {
"DD_API_KEY": "02**********33bd",
"DD_LOG_LEVEL": "debug",
"DD_SITE": "datadoghq.com",
},
},
"FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:some-function",
"FunctionName": "some-function",
}
`;

exports[`lambda flare prints correct headers prints dry-run header 1`] = `
"
[Dry Run] 🐶 Generating Lambda flare to send your configuration to Datadog...
Expand Down Expand Up @@ -357,7 +371,7 @@ exports[`lambda flare send to Datadog does not send request to Datadog when a dr
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -388,7 +402,7 @@ exports[`lambda flare send to Datadog successfully adds zip file to FormData 1`]
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -419,7 +433,7 @@ exports[`lambda flare send to Datadog successfully sends request to Datadog 1`]
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -450,7 +464,7 @@ exports[`lambda flare validates required flags extracts region from function nam
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -516,7 +530,7 @@ exports[`lambda flare validates required flags runs successfully with all requir
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -547,7 +561,7 @@ exports[`lambda flare validates required flags uses API key ENV variable and run
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -578,7 +592,7 @@ exports[`lambda flare validates required flags uses API key ENV variable and run
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand All @@ -605,7 +619,7 @@ exports[`lambda flare validates required flags uses API key ENV variable and run
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down Expand Up @@ -636,7 +650,7 @@ exports[`lambda flare validates required flags uses region ENV variable when no
{
Environment: {
Variables: {
DD_API_KEY: '02aeb762fff59ac0d5ad1536cd9633bd',
DD_API_KEY: '02**********33bd',
DD_SITE: 'datadoghq.com',
DD_LOG_LEVEL: 'debug'
}
Expand Down
40 changes: 40 additions & 0 deletions src/commands/lambda/__tests__/flare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
getAllLogs,
getLogEvents,
getLogStreamNames,
getMasking,
maskConfig,
writeFile,
zipContents,
} from '../flare'
Expand Down Expand Up @@ -244,6 +246,44 @@ describe('lambda flare', () => {
})
})

describe('getMasking', () => {
it('should mask the entire string if its length is less than 12', () => {
expect(getMasking('shortString')).toEqual('****************')
})

it('should keep the first two and last four characters for strings longer than 12 characters', () => {
const original = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'
const masked = 'ab**********wxyz'
expect(getMasking(original)).toEqual(masked)
})

it('should return empty string if input is empty', () => {
expect(getMasking('')).toEqual('')
})

it('should not mask booleans', () => {
expect(getMasking('true')).toEqual('true')
expect(getMasking('TrUe')).toEqual('TrUe')
expect(getMasking('false')).toEqual('false')
expect(getMasking('FALSE')).toEqual('FALSE')
expect(getMasking('trueee')).toEqual('****************')
})
})

describe('maskConfig', () => {
it('should mask API key but not whitelisted environment variables', () => {
const maskedConfig = maskConfig(MOCK_CONFIG)
expect(maskedConfig).toMatchSnapshot()
})

it('should return the original config if there are no environment variables', () => {
const config: any = {...MOCK_CONFIG}
config.Environment = undefined
const maskedConfig = maskConfig(config)
expect(maskedConfig).toEqual(config)
})
})

describe('deleteFolder', () => {
it('successfully deletes a folder', async () => {
deleteFolder(MOCK_FOLDER_PATH)
Expand Down
16 changes: 16 additions & 0 deletions src/commands/lambda/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,19 @@ export const AWS_SECRET_ACCESS_KEY_REG_EXP = /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{
export const AWS_SECRET_ARN_REG_EXP = /arn:aws:secretsmanager:[\w-]+:\d{12}:secret:.+/
export const DATADOG_API_KEY_REG_EXP = /(?<![a-f0-9])[a-f0-9]{32}(?![a-f0-9])/g
export const DATADOG_APP_KEY_REG_EXP = /(?<![a-f0-9])[a-f0-9]{40}(?![a-f0-9])/g

// Environment Variables whose values don't need to be
// masked in a Flare
export const SKIP_MASKING_ENV_VARS = new Set([
AWS_LAMBDA_EXEC_WRAPPER_VAR,
SITE_ENV_VAR,
LOG_LEVEL_ENV_VAR,
LAMBDA_HANDLER_ENV_VAR,
SERVICE_ENV_VAR,
VERSION_ENV_VAR,
ENVIRONMENT_ENV_VAR,
EXTRA_TAGS_ENV_VAR,
PROFILER_ENV_VAR,
PROFILER_PATH_ENV_VAR,
DOTNET_TRACER_HOME_ENV_VAR,
])
66 changes: 63 additions & 3 deletions src/commands/lambda/flare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import {
OrderBy,
OutputLogEvent,
} from '@aws-sdk/client-cloudwatch-logs'
import {LambdaClient, LambdaClientConfig} from '@aws-sdk/client-lambda'
import {FunctionConfiguration, LambdaClient, LambdaClientConfig} from '@aws-sdk/client-lambda'
import {AwsCredentialIdentity} from '@aws-sdk/types'
import axios from 'axios'
import {Command} from 'clipanion'
import FormData from 'form-data'
import JSZip from 'jszip'

import {API_KEY_ENV_VAR, AWS_DEFAULT_REGION_ENV_VAR, CI_API_KEY_ENV_VAR} from './constants'
import {API_KEY_ENV_VAR, AWS_DEFAULT_REGION_ENV_VAR, CI_API_KEY_ENV_VAR, SKIP_MASKING_ENV_VARS} from './constants'
import {getAWSCredentials, getLambdaFunctionConfig, getRegion} from './functions/commons'
import {requestAWSCredentials} from './prompt'
import * as commonRenderer from './renderers/common-renderer'
Expand All @@ -30,6 +30,8 @@ const LOGS_DIRECTORY = 'logs'
const FUNCTION_CONFIG_FILE_NAME = 'function_config.json'
const ZIP_FILE_NAME = 'lambda-flare-output.zip'
const LOG_STREAM_COUNT = 3
const FULL_OBFUSCATION = '****************'
const MIDDLE_OBFUSCATION = '**********'

export class LambdaFlareCommand extends Command {
private isDryRun = false
Expand Down Expand Up @@ -122,7 +124,7 @@ export class LambdaFlareCommand extends Command {
credentials: this.credentials,
}
const lambdaClient = new LambdaClient(lambdaClientConfig)
let config
let config: FunctionConfiguration
try {
config = await getLambdaFunctionConfig(lambdaClient, this.functionName)
} catch (err) {
Expand All @@ -134,6 +136,7 @@ export class LambdaFlareCommand extends Command {

return 1
}
config = maskConfig(config)
const configStr = util.inspect(config, false, undefined, true)
this.context.stdout.write(`\n${configStr}\n`)

Expand Down Expand Up @@ -229,6 +232,63 @@ export class LambdaFlareCommand extends Command {
}
}

/**
* Mask the environment variables in a Lambda function configuration
* @param config
*/
export const maskConfig = (config: FunctionConfiguration) => {
const environmentVariables = config.Environment?.Variables
if (!environmentVariables) {
return config
}

const maskedEnvironmentVariables: {[key: string]: string} = {}
for (const [key, value] of Object.entries(environmentVariables)) {
if (SKIP_MASKING_ENV_VARS.has(key)) {
maskedEnvironmentVariables[key] = value
continue
}
maskedEnvironmentVariables[key] = getMasking(value)
}

return {
...config,
Environment: {
...config.Environment,
Variables: maskedEnvironmentVariables,
},
}
}

/**
* Mask a string but keep the first two and last four characters
* Mask the entire string if it's short
* @param original the string to mask
* @returns the masked string
*/
export const getMasking = (original: string) => {
// Don't mask booleans
if (original.toLowerCase() === 'true' || original.toLowerCase() === 'false') {
return original
}

// Dont mask numbers
if (!isNaN(Number(original))) {
return original
}

// Mask entire string if it's short
if (original.length < 12) {
return FULL_OBFUSCATION
}

// Keep first two and last four characters if it's long
const front = original.substring(0, 2)
const end = original.substring(original.length - 4)

return front + MIDDLE_OBFUSCATION + end
}

/**
* Delete a folder and all its contents
* @param folderPath the folder to delete
Expand Down

0 comments on commit 9bddc95

Please sign in to comment.