diff --git a/exports.js b/exports.js index 5f3172e125..1c8c58b62f 100644 --- a/exports.js +++ b/exports.js @@ -495,6 +495,7 @@ module.exports = { 'lambdaLogGroups' : require(__dirname + '/plugins/aws/lambda/lambdaLogGroups.js'), 'lambdaTracingEnabled' : require(__dirname + '/plugins/aws/lambda/lambdaTracingEnabled.js'), 'lambdaHasTags' : require(__dirname + '/plugins/aws/lambda/lambdaHasTags.js'), + 'lambdaFuncUrlNotInUse' : require(__dirname + '/plugins/aws/lambda/lambdaFuncUrlNotInUse.js'), 'lambdaUniqueExecutionRole' : require(__dirname + '/plugins/aws/lambda/lambdaUniqueExecutionRole.js'), 'webServerPublicAccess' : require(__dirname + '/plugins/aws/mwaa/webServerPublicAccess.js'), diff --git a/helpers/aws/api.js b/helpers/aws/api.js index 2b6402a1af..c133f9c23c 100644 --- a/helpers/aws/api.js +++ b/helpers/aws/api.js @@ -2621,6 +2621,12 @@ var postcalls = [ filterKey: 'FunctionName', filterValue: 'FunctionName', }, + listFunctionUrlConfigs: { + reliesOnService: 'lambda', + reliesOnCall: 'listFunctions', + filterKey: 'FunctionName', + filterValue: 'FunctionName', + }, sendIntegration: { enabled: true } diff --git a/helpers/aws/api_multipart.js b/helpers/aws/api_multipart.js index 8249da0f0e..231c8a4ecc 100644 --- a/helpers/aws/api_multipart.js +++ b/helpers/aws/api_multipart.js @@ -1984,6 +1984,12 @@ var postcalls = [ filterKey: 'FunctionName', filterValue: 'FunctionName', }, + listFunctionUrlConfigs: { + reliesOnService: 'lambda', + reliesOnCall: 'listFunctions', + filterKey: 'FunctionName', + filterValue: 'FunctionName', + }, sendIntegration: { enabled: true } diff --git a/plugins/aws/lambda/lambdaFuncUrlNotInUse.js b/plugins/aws/lambda/lambdaFuncUrlNotInUse.js new file mode 100644 index 0000000000..9a433e14c6 --- /dev/null +++ b/plugins/aws/lambda/lambdaFuncUrlNotInUse.js @@ -0,0 +1,65 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Lambda Function URL Not In Use', + category: 'Lambda', + domain: 'Serverless', + severity: 'Medium', + description: 'Ensure that AWS Lambda functions are not configured with function URLs for HTTP(S) endpoints.', + more_info: 'A function URL is a dedicated HTTP(S) endpoint created for your Amazon Lambda function. You can use a function URL to invoke your Lambda function. But it can lead to some security risks depending on the security configuration and intention of the function.', + link: 'https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html', + recommended_action: 'Modify Lambda function configurations and delete function url.', + apis: ['Lambda:listFunctions','Lambda:listFunctionUrlConfigs'], + realtime_triggers: ['lambda:CreateFunction','lambda:UpdateFunctionConfiguration','lambda:DeleteFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.lambda, function(region, rcb){ + var listFunctions = helpers.addSource(cache, source, + ['lambda', 'listFunctions', region]); + + if (!listFunctions) return rcb(); + + if (listFunctions.err || !listFunctions.data) { + helpers.addResult(results, 3, + `Unable to query for Lambda functions: ${helpers.addError(listFunctions)}`, region); + return rcb(); + } + + if (!listFunctions.data.length) { + helpers.addResult(results, 0, 'No Lambda functions found', region); + return rcb(); + } + + for (var lambdaFunc of listFunctions.data) { + + if (!lambdaFunc.FunctionArn || !lambdaFunc.FunctionName) continue; + var resource = lambdaFunc.FunctionArn; + + var urlConfigs = helpers.addSource(cache, source, + ['lambda', 'listFunctionUrlConfigs', region, lambdaFunc.FunctionName]); + + if (!urlConfigs || urlConfigs.err || !urlConfigs.data) { + helpers.addResult(results, 3, + `Unable to query for Lambda function URL configs: ${helpers.addError(urlConfigs)}`, region, resource); + continue; + } + + if (urlConfigs.data.FunctionUrlConfigs && + urlConfigs.data.FunctionUrlConfigs.length){ + helpers.addResult(results, 2, 'Lambda function Url is configured', region, resource); + } else { + helpers.addResult(results, 0, 'Lambda function Url is not configured', region, resource); + } + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/aws/lambda/lambdaFuncUrlNotInUse.spec.js b/plugins/aws/lambda/lambdaFuncUrlNotInUse.spec.js new file mode 100644 index 0000000000..5664228c0f --- /dev/null +++ b/plugins/aws/lambda/lambdaFuncUrlNotInUse.spec.js @@ -0,0 +1,135 @@ +var expect = require('chai').expect; +var lambdaFunctionURLNotInUse = require('./lambdaFuncUrlNotInUse'); + +const createCache = (lambdaData, functionUrlConfigs) => { + return { + lambda: { + listFunctions: { + 'us-east-1': { + err: null, + data: lambdaData + } + }, + listFunctionUrlConfigs: functionUrlConfigs + } + }; +}; + +describe('Lambda Function URL Not in Use', function () { + describe('run', function () { + it('should return unknown result if unable to list the lambda functions', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Unable to query for Lambda functions'); + done(); + }; + + const cache = createCache(null, {}); + + lambdaFunctionURLNotInUse.run(cache, {}, callback); + }); + + it('should return passing result if no lambda function found in region', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('No Lambda functions found'); + done(); + }; + + const cache = createCache([], {}); + + lambdaFunctionURLNotInUse.run(cache, {}, callback); + }); + + it('should return passing result if lambda function URL is not configured', function (done) { + const lambdaData = [ + { + "FunctionName": "test-lambda", + "FunctionArn": "arn:aws:lambda:us-east-1:000011112222:function:test-lambda" + } + ]; + + const functionUrlConfigs = { + 'us-east-1': { + 'test-lambda': { + 'err': null, + 'data': { + 'FunctionUrlConfigs': [] + } + } + } + }; + + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Lambda function Url is not configured'); + done(); + }; + + const cache = createCache(lambdaData, functionUrlConfigs); + + lambdaFunctionURLNotInUse.run(cache, {}, callback); + }); + + it('should return failing result if lambda function URL is configured', function (done) { + const lambdaData = [ + { + "FunctionName": "test-lambda", + "FunctionArn": "arn:aws:lambda:us-east-1:000011112222:function:test-lambda" + } + ]; + + const functionUrlConfigs = { + 'us-east-1': { + 'test-lambda': { + 'err': null, + 'data': { + 'FunctionUrlConfigs': [{ + FunctionUrl: "https://tetsuewfebwfweffesvvs.lambda-url.us-east-1.on.aws/", + }] + } + } + } + }; + + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Lambda function Url is configured'); + done(); + }; + + const cache = createCache(lambdaData, functionUrlConfigs); + + lambdaFunctionURLNotInUse.run(cache, {}, callback); + }); + + it('should return unknown result if unable to list the lambda function url config', function (done) { + const lambdaData = [ + { + "FunctionName": "test-lambda", + "FunctionArn": "arn:aws:lambda:us-east-1:000011112222:function:test-lambda" + } + ]; + + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Unable to query for Lambda function URL configs: Unable to obtain data'); + done(); + }; + + const cache = createCache(lambdaData, null); + + lambdaFunctionURLNotInUse.run(cache, {}, callback); + }); + }); +});