diff --git a/.gitignore b/.gitignore index 617d8e6..c204b87 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ aws-least-privilege-*.tgz *.policy.json #ignore log xray-scan.log +#ignore excessive report +excessive_permissions.json diff --git a/README.md b/README.md index a3d194a..72e32ac 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,16 @@ Follow the [AWS X-Ray Developer Guide](https://docs.aws.amazon.com/xray/latest/d ## X-Ray Parameter Whitelist Configuration -In order to capture what resources are accessed by AWS service calls, AWS X-Ray needs to be configured to send out parameter information as part of the subsegment traces. For example for DynamoDB we would want to know the Table being accessed and for S3 the Bucket and Key. AWS X-Ray currently provides default parameter info only for a limited set of services: DynamoDB, SQS, and Lambda. X-Ray does provide support for configuring additional services (such as S3) to include parameter info through a parameter whitelist configuration file. We've created separate projects to manage parameter whitelist configurations for the Java and Node X-Ray SDKs. For Python the same parameter whitelist file of Node can be used as specified below. +In order to capture what resources are accessed by AWS service calls, AWS X-Ray needs to be configured to send out parameter information as part of the subsegment traces. For example for DynamoDB we would want to know the Table being accessed and for S3 the Bucket and Key. AWS X-Ray currently provides default parameter info only for a limited set of services: + +* DynamoDB: All X-Ray SDKs +* SQS: All X-Ray SDKs +* Lambda: All X-Ray SDKs +* S3: + * Node SDK: version 1.2.0 and higher. + * Java SDK: As of Jan 30, 2018 has support as part of main source code, which is still not released. See [merged pull request](https://github.com/aws/aws-xray-sdk-java/pull/9). + +X-Ray does provide support for configuring additional services (such as S3 and SNS) to include parameter info through a parameter whitelist configuration file. We've created separate projects to manage parameter whitelist configurations for the Java and Node X-Ray SDKs. For Python the same parameter whitelist file of Node can be used as specified below. ### X-Ray SDK for Node @@ -69,7 +78,7 @@ Then during the initialization of your application add the following configurati const AWSXRay = require('aws-xray-sdk'); const whitelists = require('aws-xray-parameter-whitelist'); XRay.captureAWS(require('aws-sdk')); //standard capture code of X-Ray to catch AWS SDK calls -AWSXRay.appendAWSWhitelist(whitelists.s3_whitelist); +AWSXRay.appendAWSWhitelist(whitelists.sns_whitelist); ``` More info available at the project page: https://github.com/functionalone/aws-xray-parameter-whitelist-node @@ -160,6 +169,7 @@ The following services are supported as part of an X-Ray scan (unsupported servi * S3 * SQS * Lambda +* SNS If you would like to see additional services, please let us know by openning an [issue](https://github.com/functionalone/aws-least-privilege/issues). diff --git a/package-lock.json b/package-lock.json index abdc183..0326669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,6 +115,11 @@ "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=", "dev": true }, + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, "aws-sdk": { "version": "2.200.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.200.0.tgz", @@ -323,6 +328,11 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, "commander": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", @@ -560,6 +570,11 @@ "array-find-index": "1.0.2" } }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, "dargs": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", @@ -690,6 +705,11 @@ "strip-eof": "1.0.0" } }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, "figures": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", @@ -996,6 +1016,11 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "jmespath": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", @@ -1722,6 +1747,11 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, "standard-version": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-4.3.0.tgz", @@ -2029,9 +2059,9 @@ "optional": true }, "winston": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.0.tgz", - "integrity": "sha1-gIBQuT1SZh7Z+2wms/DIJnCLCu4=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.1.tgz", + "integrity": "sha512-k/+Dkzd39ZdyJHYkuaYmf4ff+7j+sCIy73UCOWHYA67/WXU+FF/Y6PF28j+Vy7qNRPHWO+dR+/+zkoQWPimPqg==", "requires": { "async": "1.0.0", "colors": "1.0.3", @@ -2039,38 +2069,6 @@ "eyes": "0.1.8", "isstream": "0.1.2", "stack-trace": "0.0.10" - }, - "dependencies": { - "async": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" - }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" - }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - } } }, "wordwrap": { diff --git a/package.json b/package.json index 5b96011..aa657a4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "bluebird": "^3.5.1", "commander": "^2.14.1", "lodash": "^4.17.5", - "winston": "^2.4.0" + "winston": "^2.4.1" }, "devDependencies": { "@types/bluebird": "^3.5.20", diff --git a/src/lib/aws-segments/dynamo.ts b/src/lib/aws-segments/dynamo.ts index 14bb37d..0ad60f0 100644 --- a/src/lib/aws-segments/dynamo.ts +++ b/src/lib/aws-segments/dynamo.ts @@ -46,12 +46,7 @@ const parseSegment: SegmentParseFunc = function(segmentDoc: any, actionsMap: Res const region = aws.region || extractRegion(functionArn); //construct the arn const arn = `arn:aws:dynamodb:${region}:*:table/${aws.table_name}`; - let actions = actionsMap.get(arn); - if(!actions) { - actions = new Set(); - actionsMap.set(arn, actions); - } - actions.add(aws.operation); + actionsMap.addActionToResource(aws.operation, arn); }; // tslint:enable:jsdoc-format diff --git a/src/lib/aws-segments/index.ts b/src/lib/aws-segments/index.ts index cf43be7..d9e5410 100644 --- a/src/lib/aws-segments/index.ts +++ b/src/lib/aws-segments/index.ts @@ -3,6 +3,7 @@ import dynamoParser from './dynamo'; import s3Parser from './s3'; import lambdaParser from './lambda'; import sqsParser from './sqs'; +import snsParser from './sns'; export type SegmentParseFunc = (segmentDoc: any, actionsMap: ResourceActionMap, functionArn: string) => void; @@ -11,6 +12,7 @@ export const AWSSegmentParsers: {[i: string]: SegmentParseFunc} = { S3: s3Parser, Lambda: lambdaParser, SQS: sqsParser, + SNS: snsParser, }; export const SUPPORTED_SERVICES = Object.getOwnPropertyNames(AWSSegmentParsers); diff --git a/src/lib/aws-segments/lambda.ts b/src/lib/aws-segments/lambda.ts index d68c26c..1f50cd0 100644 --- a/src/lib/aws-segments/lambda.ts +++ b/src/lib/aws-segments/lambda.ts @@ -47,17 +47,12 @@ const parseSegment: SegmentParseFunc = function(segmentDoc: any, actionsMap: Res if(isEmpty(aws) || !aws.function_name) { logger.warn("Couldn't extract Lambda info for segment document: ", segmentDoc); return; - } + } const op = OPS_REPLACERS[aws.operation] || aws.operation; const region = aws.region || extractRegion(functionArn); //construct the arn const arn = `arn:aws:lambda:${region}:*:function:${aws.function_name}`; - let actions = actionsMap.get(arn); - if(!actions) { - actions = new Set(); - actionsMap.set(arn, actions); - } - actions.add(op); + actionsMap.addActionToResource(op, arn); }; // tslint:enable:jsdoc-format diff --git a/src/lib/aws-segments/s3.ts b/src/lib/aws-segments/s3.ts index 329a707..70f7ccd 100644 --- a/src/lib/aws-segments/s3.ts +++ b/src/lib/aws-segments/s3.ts @@ -79,12 +79,7 @@ const parseSegment: SegmentParseFunc = function(segmentDoc: any, actionsMap: Res arn = `arn:aws:s3:::${aws.bucket_name}/*`; } } - let actions = actionsMap.get(arn); - if (!actions) { - actions = new Set(); - actionsMap.set(arn, actions); - } - actions.add(op); + actionsMap.addActionToResource(op, arn); }; export default parseSegment; diff --git a/src/lib/aws-segments/sns.ts b/src/lib/aws-segments/sns.ts new file mode 100644 index 0000000..fac25bb --- /dev/null +++ b/src/lib/aws-segments/sns.ts @@ -0,0 +1,70 @@ +import {SegmentParseFunc} from './index'; +import { ResourceActionMap } from '../xray-trace-fetcher'; +import {isEmpty} from 'lodash'; +import {logger} from '../logger'; +import { extractRegion } from './utils'; + +/** + * Map which is used to replace functions which are named differently to the IAM naming + * See also: https://docs.aws.amazon.com/IAM/latest/UserGuide/list_sns.html + */ +const OPS_REPLACERS: {[i: string]: string} = { +}; + +/** + * Global actions that work on root star + */ +const GLOBAL_ACTIONS = /^(ListTopics|ListPlatformApplications|ListSubscriptions|CreateTopic|CreatePlatformApplication)$/; + +// tslint:disable:jsdoc-format +/** + * Parse a SNS segement. SNS segment will look like this: + * +{ + "id": "4b1fd180bd9678f2", + "name": "SNS", + "start_time": 1520849681.61, + "end_time": 1520849681.714, + "http": { + "response": { + "status": 200 + } + }, + "aws": { + "operation": "ListSubscriptionsByTopic", + "region": "us-east-1", + "request_id": "0b537b2c-4c01-5f50-b619-280c195091e7", + "retries": 0, + "topic_arn": "arn:aws:sns:us-east-1:123456789012:test-topic" + }, + "namespace": "aws" +} + * + * @param segmentDoc + * @param actionsMap + */ +const parseSegment: SegmentParseFunc = function(segmentDoc: any, actionsMap: ResourceActionMap, functionArn: string) { + const aws: any = segmentDoc.aws; + let arn; + if(isEmpty(aws)) { + logger.warn("Couldn't extract SNS info for segment document: ", segmentDoc); + return; + } + const region = aws.region || extractRegion(functionArn); + const op = OPS_REPLACERS[aws.operation] || aws.operation; + if(GLOBAL_ACTIONS.test(op)) { + arn = `arn:aws:sns:${region}:*:*`; + } + else { + if(!aws.topic_arn) { + logger.warn("Empty topic_arn for SNS op: [%s] on segment (skipping): ", op, segmentDoc); + return; + } + //construct the arn + arn = `arn:aws:sns:${region}:*:${aws.topic_arn}`; + } + actionsMap.addActionToResource(op, arn); +}; +// tslint:enable:jsdoc-format + +export default parseSegment; diff --git a/src/lib/aws-segments/sqs.ts b/src/lib/aws-segments/sqs.ts index de98c65..af2114c 100644 --- a/src/lib/aws-segments/sqs.ts +++ b/src/lib/aws-segments/sqs.ts @@ -81,12 +81,7 @@ const parseSegment: SegmentParseFunc = function(segmentDoc: any, actionsMap: Res const queueName = regexMatch[1]; //construct the arn const arn = `arn:aws:sqs:${region}:*:${queueName}`; - let actions = actionsMap.get(arn); - if(!actions) { - actions = new Set(); - actionsMap.set(arn, actions); - } - actions.add(op); + actionsMap.addActionToResource(op, arn); }; // tslint:enable:jsdoc-format diff --git a/src/lib/xray-trace-fetcher.ts b/src/lib/xray-trace-fetcher.ts index f0d2881..4c373b8 100644 --- a/src/lib/xray-trace-fetcher.ts +++ b/src/lib/xray-trace-fetcher.ts @@ -101,9 +101,22 @@ export async function getXrayTraces(conf?: ScanXrayTracesConf) { } /** - * Maps that maps a resource to set of actions + * Map that maps a resource to set of actions */ -export type ResourceActionMap = Map>; +export class ResourceActionMap extends Map> { + constructor() { + super(); + } + + addActionToResource(actionOp: string, resourceArn: string) { + let actions = super.get(resourceArn); + if(!actions) { + actions = new Set(); + super.set(resourceArn, actions); + } + actions.add(actionOp); + } +} /** * Map which maps a function to the set of ResourceActionMap @@ -122,7 +135,7 @@ function parseSegmentDoc(doc: any, functionMap: FunctionToActionsMap, parentActi logger.debug('Found arn: %s for Segment: %s', arn, doc.id); actionsMap = functionMap.get(arn); if (!actionsMap) { - actionsMap = new Map(); + actionsMap = new ResourceActionMap(); functionMap.set(arn, actionsMap); } } diff --git a/src/test/xray-trace-fetcher.nock.json b/src/test/xray-trace-fetcher.nock.json index 8703487..4e5e35e 100644 --- a/src/test/xray-trace-fetcher.nock.json +++ b/src/test/xray-trace-fetcher.nock.json @@ -8127,5 +8127,330 @@ "Date", "Sun, 04 Feb 2018 23:28:36 GMT" ] + }, + { + "scope": "https://xray.us-east-1.amazonaws.com:443", + "method": "POST", + "path": "/TraceSummaries", + "body": { + "StartTime": 1520850322, + "EndTime": 1520857522 + }, + "status": 200, + "response": { + "ApproximateTime": 1520856000, + "NextToken": "fnsaFIsTE9sgVQoYBhJOCsE30MUjU5MpEqZHvDeRvuiLyTgq8/yX++pMcSbqq4Ic", + "TraceSummaries": [], + "TracesProcessedCount": 0 + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:31:57 GMT", + "Content-Type", + "application/json", + "Content-Length", + "154", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5ab46d59-25f1-11e8-a074-1b8de9a52290" + ] + }, + { + "scope": "https://xray.us-east-1.amazonaws.com:443", + "method": "POST", + "path": "/TraceSummaries", + "body": { + "StartTime": 1520850322, + "EndTime": 1520857522, + "NextToken": "fnsaFIsTE9sgVQoYBhJOCsE30MUjU5MpEqZHvDeRvuiLyTgq8/yX++pMcSbqq4Ic" + }, + "status": 200, + "response": { + "ApproximateTime": 1520853715, + "NextToken": "fnsaFIsTE9sgVQoYBhJOCqfkEjAFXaIbUjgs/WtgeMDFBv66mtKmEdJjk+K9ACxE", + "TraceSummaries": [], + "TracesProcessedCount": 0 + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:31:57 GMT", + "Content-Type", + "application/json", + "Content-Length", + "157", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5aff808b-25f1-11e8-b661-d1d6b282a01f" + ] + }, + { + "scope": "https://xray.us-east-1.amazonaws.com:443", + "method": "POST", + "path": "/TraceSummaries", + "body": { + "StartTime": 1520850322, + "EndTime": 1520857522, + "NextToken": "fnsaFIsTE9sgVQoYBhJOCqfkEjAFXaIbUjgs/WtgeMDFBv66mtKmEdJjk+K9ACxE" + }, + "status": 200, + "response": { + "ApproximateTime": 1520852400, + "NextToken": "M96gZwjp1ojwwSEWzOcjMGD0J90JZ8C5lBLN9ixbuMpTbqqH+noDEWcGX3KEiYIY", + "TraceSummaries": [], + "TracesProcessedCount": 0 + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:31:58 GMT", + "Content-Type", + "application/json", + "Content-Length", + "155", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5b43b4b6-25f1-11e8-9bb1-d3ccdb9c0ef1" + ] + }, + { + "scope": "https://xray.us-east-1.amazonaws.com:443", + "method": "POST", + "path": "/TraceSummaries", + "body": { + "StartTime": 1520850322, + "EndTime": 1520857522, + "NextToken": "M96gZwjp1ojwwSEWzOcjMGD0J90JZ8C5lBLN9ixbuMpTbqqH+noDEWcGX3KEiYIY" + }, + "status": 200, + "response": { + "ApproximateTime": 1520850427, + "NextToken": "M96gZwjp1ojwwSEWzOcjMPOgJX3FPBEux98AjUDNESX7Iyu1hw6NRwignb3+b24+", + "TraceSummaries": [ + { + "Annotations": {}, + "Duration": 3.739, + "HasError": false, + "HasFault": false, + "HasThrottle": false, + "Http": { + "ClientIp": null, + "HttpMethod": null, + "HttpStatus": 200, + "HttpURL": null, + "UserAgent": null + }, + "Id": "1-5aa65bde-633e41d8bdd239c8b30729d8", + "IsPartial": null, + "ResponseTime": 3.721, + "ServiceIds": null, + "Users": [] + } + ], + "TracesProcessedCount": 1 + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:31:58 GMT", + "Content-Type", + "application/json", + "Content-Length", + "448", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5b8ddd4f-25f1-11e8-87aa-ed93d05cf526" + ] + }, + { + "scope": "https://xray.us-east-1.amazonaws.com:443", + "method": "POST", + "path": "/TraceSummaries", + "body": { + "StartTime": 1520850322, + "EndTime": 1520857522, + "NextToken": "M96gZwjp1ojwwSEWzOcjMPOgJX3FPBEux98AjUDNESX7Iyu1hw6NRwignb3+b24+" + }, + "status": 200, + "response": { + "ApproximateTime": 1520850322, + "NextToken": null, + "TraceSummaries": [], + "TracesProcessedCount": 0 + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:31:59 GMT", + "Content-Type", + "application/json", + "Content-Length", + "95", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5bd0da08-25f1-11e8-987e-bb99a8bf9aab" + ] + }, + { + "scope": "https://xray.us-east-1.amazonaws.com:443", + "method": "POST", + "path": "/Traces", + "body": { + "TraceIds": [ + "1-5aa65bde-633e41d8bdd239c8b30729d8" + ] + }, + "status": 200, + "response": { + "NextToken": null, + "Traces": [ + { + "Duration": 3.739, + "Id": "1-5aa65bde-633e41d8bdd239c8b30729d8", + "Segments": [ + { + "Document": "{\"id\":\"2aa2bda276cc1afc\",\"name\":\"nodejs-test-dev-hello\",\"start_time\":1.520851938192E9,\"end_time\":1.52085193856E9,\"parent_id\":\"2dc10c775a25bd6e\",\"aws\":{\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:nodejs-test-dev-hello\",\"resource_names\":[\"nodejs-test-dev-hello\"],\"account_id\":\"123456789012\"},\"trace_id\":\"1-5aa65bde-633e41d8bdd239c8b30729d8\",\"origin\":\"AWS::Lambda::Function\",\"subsegments\":[{\"id\":\"67ac154dc7ae09f5\",\"name\":\"SNS\",\"start_time\":1.520851938489E9,\"end_time\":1.520851938558E9,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"Publish\",\"region\":\"us-east-1\",\"request_id\":\"ac7a859d-3da2-5a1f-b6a0-d6bd6380f0b4\",\"retries\":0,\"topic_arn\":\"arn:aws:sns:us-east-1:123456789012:nodejs-test-topic\"},\"namespace\":\"aws\"},{\"id\":\"703e8f29211c0fb4\",\"name\":\"SNS\",\"start_time\":1.520851938421E9,\"end_time\":1.520851938488E9,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"ListSubscriptionsByTopic\",\"region\":\"us-east-1\",\"request_id\":\"fdf76e9b-a07e-5fad-b8d2-3dbbfe5cd5e8\",\"retries\":0,\"topic_arn\":\"arn:aws:sns:us-east-1:123456789012:nodejs-test-topic\"},\"namespace\":\"aws\"},{\"id\":\"24dbc1792b6d1617\",\"name\":\"SNS\",\"start_time\":1.520851938203E9,\"end_time\":1.52085193832E9,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"ListTopics\",\"region\":\"us-east-1\",\"request_id\":\"94088857-a6af-5cde-8675-db77f88d6094\",\"retries\":0},\"namespace\":\"aws\"},{\"id\":\"5b8f72fa317437de\",\"name\":\"Initialization\",\"start_time\":1.520851935373E9,\"end_time\":1.52085193819E9,\"aws\":{\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:nodejs-test-dev-hello\"}},{\"id\":\"6720e0557100c90f\",\"name\":\"SNS\",\"start_time\":1.52085193836E9,\"end_time\":1.520851938414E9,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"ListSubscriptions\",\"region\":\"us-east-1\",\"request_id\":\"0fbf7ca1-dd2b-53fa-b100-2f8183091fa8\",\"retries\":0},\"namespace\":\"aws\"}]}", + "Id": "2aa2bda276cc1afc" + }, + { + "Document": "{\"id\":\"2dc10c775a25bd6e\",\"name\":\"nodejs-test-dev-hello\",\"start_time\":1.520851934821E9,\"end_time\":1.520851938542E9,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"request_id\":\"6ccbf59c-25e3-11e8-96d9-5bf2bb5ad857\"},\"trace_id\":\"1-5aa65bde-633e41d8bdd239c8b30729d8\",\"origin\":\"AWS::Lambda\",\"resource_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:nodejs-test-dev-hello\"}", + "Id": "2dc10c775a25bd6e" + }, + { + "Document": "{\"id\":\"3d145c980ae23561\",\"name\":\"SNS\",\"start_time\":1.520851938489E9,\"end_time\":1.520851938558E9,\"parent_id\":\"67ac154dc7ae09f5\",\"inferred\":true,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"Publish\",\"region\":\"us-east-1\",\"request_id\":\"ac7a859d-3da2-5a1f-b6a0-d6bd6380f0b4\",\"retries\":0,\"topic_arn\":\"arn:aws:sns:us-east-1:123456789012:nodejs-test-topic\"},\"trace_id\":\"1-5aa65bde-633e41d8bdd239c8b30729d8\",\"origin\":\"AWS::SNS\"}", + "Id": "3d145c980ae23561" + }, + { + "Document": "{\"id\":\"19707f8a3e99d01d\",\"name\":\"SNS\",\"start_time\":1.520851938421E9,\"end_time\":1.520851938488E9,\"parent_id\":\"703e8f29211c0fb4\",\"inferred\":true,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"ListSubscriptionsByTopic\",\"region\":\"us-east-1\",\"request_id\":\"fdf76e9b-a07e-5fad-b8d2-3dbbfe5cd5e8\",\"retries\":0,\"topic_arn\":\"arn:aws:sns:us-east-1:123456789012:nodejs-test-topic\"},\"trace_id\":\"1-5aa65bde-633e41d8bdd239c8b30729d8\",\"origin\":\"AWS::SNS\"}", + "Id": "19707f8a3e99d01d" + }, + { + "Document": "{\"id\":\"2f4d2eeb183ee65f\",\"name\":\"SNS\",\"start_time\":1.520851938203E9,\"end_time\":1.52085193832E9,\"parent_id\":\"24dbc1792b6d1617\",\"inferred\":true,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"ListTopics\",\"region\":\"us-east-1\",\"request_id\":\"94088857-a6af-5cde-8675-db77f88d6094\",\"retries\":0},\"trace_id\":\"1-5aa65bde-633e41d8bdd239c8b30729d8\",\"origin\":\"AWS::SNS\"}", + "Id": "2f4d2eeb183ee65f" + }, + { + "Document": "{\"id\":\"3444714631bb125d\",\"name\":\"SNS\",\"start_time\":1.52085193836E9,\"end_time\":1.520851938414E9,\"parent_id\":\"6720e0557100c90f\",\"inferred\":true,\"http\":{\"response\":{\"status\":200}},\"aws\":{\"operation\":\"ListSubscriptions\",\"region\":\"us-east-1\",\"request_id\":\"0fbf7ca1-dd2b-53fa-b100-2f8183091fa8\",\"retries\":0},\"trace_id\":\"1-5aa65bde-633e41d8bdd239c8b30729d8\",\"origin\":\"AWS::SNS\"}", + "Id": "3444714631bb125d" + } + ] + } + ], + "UnprocessedTraceIds": [] + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:31:59 GMT", + "Content-Type", + "application/json", + "Content-Length", + "4630", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5c17ce55-25f1-11e8-a422-3d384a899f66" + ] + }, + { + "scope": "https://lambda.us-east-1.amazonaws.com:443", + "method": "GET", + "path": "/2015-03-31/functions/arn%3Aaws%3Alambda%3Aus-east-1%3A123456789012%3Afunction%3Anodejs-test-dev-hello/configuration", + "body": "", + "status": 200, + "response": { + "CodeSha256": "0RjgITmdQVtKhbyBEt9D7hgeI7wcU9nwkNJFLcQAJsA=", + "CodeSize": 21597011, + "DeadLetterConfig": null, + "Description": "", + "Environment": null, + "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:nodejs-test-dev-hello", + "FunctionName": "nodejs-test-dev-hello", + "Handler": "dist/main/handler.sdkHandler", + "KMSKeyArn": null, + "LastModified": "2018-03-12T10:48:12.019+0000", + "MasterArn": null, + "MemorySize": 1024, + "RevisionId": "e2f6a75f-d48c-4b65-9c3c-905bf3889617", + "Role": "arn:aws:iam::123456789012:role/nodejs-test-dev-us-east-1-lambdaRole", + "Runtime": "nodejs6.10", + "Timeout": 60, + "TracingConfig": { + "Mode": "Active" + }, + "Version": "$LATEST", + "VpcConfig": { + "SecurityGroupIds": [], + "SubnetIds": [], + "VpcId": null + } + }, + "rawHeaders": [ + "Date", + "Mon, 12 Mar 2018 12:32:00 GMT", + "Content-Type", + "application/json", + "Content-Length", + "687", + "Connection", + "keep-alive", + "x-amzn-RequestId", + "5c9f9b5a-25f1-11e8-89aa-8b48fa0c4b16" + ] + }, + { + "scope": "https://iam.amazonaws.com:443", + "method": "POST", + "path": "/", + "body": "Action=ListRolePolicies&RoleName=nodejs-test-dev-us-east-1-lambdaRole&Version=2010-05-08", + "status": 200, + "response": "\n \n false\n \n dev-nodejs-test-lambda\n \n \n \n 5d06c262-25f1-11e8-bad3-8f5e2f2950a8\n \n\n", + "rawHeaders": [ + "x-amzn-RequestId", + "5d06c262-25f1-11e8-bad3-8f5e2f2950a8", + "Content-Type", + "text/xml", + "Content-Length", + "387", + "Date", + "Mon, 12 Mar 2018 12:32:00 GMT" + ] + }, + { + "scope": "https://iam.amazonaws.com:443", + "method": "POST", + "path": "/", + "body": "Action=ListAttachedRolePolicies&RoleName=nodejs-test-dev-us-east-1-lambdaRole&Version=2010-05-08", + "status": 200, + "response": "\n \n false\n \n \n \n 5d078514-25f1-11e8-bc56-4dc6c66bee6e\n \n\n", + "rawHeaders": [ + "x-amzn-RequestId", + "5d078514-25f1-11e8-bc56-4dc6c66bee6e", + "Content-Type", + "text/xml", + "Content-Length", + "360", + "Date", + "Mon, 12 Mar 2018 12:32:00 GMT" + ] + }, + { + "scope": "https://iam.amazonaws.com:443", + "method": "POST", + "path": "/", + "body": "Action=GetRolePolicy&PolicyName=dev-nodejs-test-lambda&RoleName=nodejs-test-dev-us-east-1-lambdaRole&Version=2010-05-08", + "status": 200, + "response": "\n \n %7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Action%22%3A%5B%22logs%3ACreateLogStream%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3Alogs%3Aus-east-1%3A123456789012%3Alog-group%3A%2Faws%2Flambda%2Fnodejs-test-dev-hello%3A%2A%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22logs%3APutLogEvents%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3Alogs%3Aus-east-1%3A123456789012%3Alog-group%3A%2Faws%2Flambda%2Fnodejs-test-dev-hello%3A%2A%3A%2A%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22dynamodb%3A%2A%22%5D%2C%22Resource%22%3A%22arn%3Aaws%3Adynamodb%3Aus-east-1%3A%2A%3Atable%2F%2A%22%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22xray%3APutTelemetryRecords%22%2C%22xray%3APutTraceSegments%22%5D%2C%22Resource%22%3A%22%2A%22%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22kms%3ADecrypt%22%5D%2C%22Resource%22%3A%22arn%3Aaws%3Akms%3Aus-east-1%3A123456789012%3Akey%2Fd4171d0d-ec6a-4b16-bf8c-fa2b086875bb%22%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22s3%3AListAllMyBuckets%22%5D%2C%22Resource%22%3A%22arn%3Aaws%3As3%3A%3A%3A%2A%22%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22s3%3AGetBucketLocation%22%2C%22s3%3AListBucket%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3As3%3A%3A%3Afone-demo-test-bucket%22%2C%22arn%3Aaws%3As3%3A%3A%3Afone-demo-example%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22s3%3A%2A%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3As3%3A%3A%3Afone-demo-test-bucket%2F%2A%22%2C%22arn%3Aaws%3As3%3A%3A%3Afone-demo-example%2F%2A%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22lambda%3AInvokeFunction%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3Alambda%3Aus-east-1%3A%2A%3Afunction%3Ajava-test-dev-hello%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22sqs%3A%2A%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3Asqs%3Aus-east-1%3A%2A%3Atest-msg-queue%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22sns%3A%2A%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3Asns%3Aus-east-1%3A123456789012%3Anodejs-test-topic%22%5D%2C%22Effect%22%3A%22Allow%22%7D%2C%7B%22Action%22%3A%5B%22sns%3AListTopics%22%2C%22sns%3AListSubscriptions%22%5D%2C%22Resource%22%3A%5B%22arn%3Aaws%3Asns%3Aus-east-1%3A%2A%3A%2A%22%5D%2C%22Effect%22%3A%22Allow%22%7D%5D%7D\n nodejs-test-dev-us-east-1-lambdaRole\n dev-nodejs-test-lambda\n \n \n 5d6ef9a9-25f1-11e8-bc56-4dc6c66bee6e\n \n\n", + "rawHeaders": [ + "x-amzn-RequestId", + "5d6ef9a9-25f1-11e8-bc56-4dc6c66bee6e", + "Content-Type", + "text/xml", + "Content-Length", + "2697", + "Vary", + "Accept-Encoding", + "Date", + "Mon, 12 Mar 2018 12:32:01 GMT" + ] } ] \ No newline at end of file diff --git a/src/test/xray-trace-fetcher.test.ts b/src/test/xray-trace-fetcher.test.ts index 0189686..0ad7062 100644 --- a/src/test/xray-trace-fetcher.test.ts +++ b/src/test/xray-trace-fetcher.test.ts @@ -3,6 +3,7 @@ import { getXrayTraces, parseXrayTrace, FunctionToActionsMap, getFunctionActionM ResourceActionMap, createIAMPolicyDoc, ScanXrayTracesConf, scanXray } from '../lib/xray-trace-fetcher'; import * as nock from 'nock'; // import * as fs from 'fs'; +import { isArray } from 'util'; // tslint:disable:max-line-length // tslint:disable-next-line:no-var-requires @@ -13,12 +14,16 @@ describe('xray fetch tests', function() { this.timeout(60000); before(() => { - nock.load('src/test/xray-trace-fetcher.nock.json'); + // nock.recorder.rec({ + // output_objects: true, + // }); + // nock.load('nock1.rec.json'); + nock.load('src/test/xray-trace-fetcher.nock.json'); }); // after(() => { // const nockRec = nock.recorder.play(); - // fs.writeFileSync('nock-xray-trace-fetcher1.rec.json', JSON.stringify(nockRec, undefined, 2)); + // fs.writeFileSync('nock.rec.json', JSON.stringify(nockRec, undefined, 2)); // }); it('getXrayTraces should fetch 6 traces', async function() { @@ -63,7 +68,7 @@ describe('xray fetch tests', function() { }); it('createIAMPolicyDoc creates proper policy action', function() { - const map: ResourceActionMap = new Map(); + const map = new ResourceActionMap(); map.set("arn:aws:s3:::test-bucket/*", new Set(['PutObjectTagging', 'GetObject', 'DeleteObject', 'PutObject'])); map.set("arn:aws:s3:::test-again/*", new Set(['PutObjectTagging', 'GetObject', 'DeleteObject', 'PutObject'])); map.set("arn:aws:dynamodb:us-east-1:*:table/test-it", new Set(['DeleteItem', 'PutItem', 'Scan', 'GetItem'])); @@ -92,4 +97,25 @@ describe('xray fetch tests', function() { // console.log('compare res: ', JSON.stringify(res.ExcessPermissions, undefined, 2)); }); + it('scanXray SNS should return policies and comparison', async function() { + const conf: ScanXrayTracesConf = { + startTime: new Date(1520850322000), + timeRangeMinutes: 120, + compareExistingRole: true, + }; + const res = await scanXray(conf); + assert.equal(res.GeneratedPolicies.length, 1); + assert.equal(res.ExcessPermissions.length, 1); + //verify that excess permission contains sns:* + const snsExcess = res.ExcessPermissions[0].excessPermissions.find((p) => isArray(p.Action) && ((p.Action! as string[]).find((s) => s === 'sns:*') !== undefined)); + assert.isNotEmpty(snsExcess); + //policy should contain 2 statements + assert.equal(res.GeneratedPolicies[0].Policy.Statement!.length, 2); + const statementResourceSpecific = res.GeneratedPolicies[0].Policy.Statement!.find((s) => isArray(s.Resource) && s.Resource[0].endsWith('test-topic')); + assert.isTrue((statementResourceSpecific!.Action! as string[]).find((a) => a.endsWith("ListSubscriptionsByTopic")) !== undefined); + const statementGlobal = res.GeneratedPolicies[0].Policy.Statement!.find((s) => isArray(s.Resource) && s.Resource[0].endsWith(':*')); + assert.isTrue((statementGlobal!.Action! as string[]).find((a) => a.endsWith("ListTopics")) !== undefined); + // console.log('compare res: ', JSON.stringify(res.ExcessPermissions, undefined, 2)); + }); + });