├── requirements-dev.txt
├── requirements.txt
├── .gitignore
├── package.json
├── backend
├── runtime
│ ├── requirements.txt
│ ├── app.py
│ └── Dockerfile
└── infrastructure.py
├── vulnerable_code
└── hard_coded_secret.py
├── CODE_OF_CONDUCT.md
├── deployment.py
├── scripts
├── install_dependencies.sh
├── run_zap.sh
└── run_tests.sh
├── shared
├── shared_infra.py
└── network
│ └── infrastructure.py
├── app.py
├── LICENSE
├── constants.py
├── cdk.json
├── securityhub
├── securityhub_report_step.py
└── runtime
│ └── invoke_action_function.py
├── CONTRIBUTING.md
├── sectools
└── infrastructure.py
├── pipeline.py
└── README.md
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest==6.2.5
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aws-cdk-lib==2.50.0
2 | constructs>=10.0.0,<11.0.0
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | package-lock.json
3 | __pycache__
4 | .pytest_cache
5 | .venv
6 | .env
7 | *.egg-info
8 |
9 | # CDK asset staging directory
10 | .cdk.staging
11 | cdk.out
12 | node_modules
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "aws-cdk": "2.50.0"
4 | },
5 | "scripts": {
6 | "cdk": "cdk"
7 | },
8 | "dependencies": {
9 | "requirements": "^1.4.1"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/backend/runtime/requirements.txt:
--------------------------------------------------------------------------------
1 | click==8.1.3
2 | Flask==2.3.2
3 | importlib-metadata==5.0.0
4 | itsdangerous==2.1.2
5 | Jinja2==3.1.2
6 | MarkupSafe==2.1.1
7 | typing_extensions==4.4.0
8 | Werkzeug==2.2.3
9 | zipp==3.10.0
10 | # cryptography==2.9.1
11 |
--------------------------------------------------------------------------------
/vulnerable_code/hard_coded_secret.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | from cryptography.fernet import Fernet
6 |
7 | def encrypt():
8 | key = Fernet.generate_key()
9 | f = Fernet(key)
10 | f.encrypt(b"a secret message")
11 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/backend/runtime/app.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | from flask import Flask
6 | from flask import request
7 |
8 | app = Flask(__name__)
9 |
10 | @app.route('/')
11 | def index():
12 | return '
Hello from Flask & Docker'
13 |
14 | if __name__ == "__main__":
15 | app.run()
--------------------------------------------------------------------------------
/backend/runtime/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | FROM public.ecr.aws/docker/library/python:3.9.15-slim
4 |
5 | WORKDIR /api
6 |
7 | COPY requirements.txt requirements.txt
8 | RUN pip3 install -r requirements.txt
9 |
10 | COPY . .
11 |
12 | ENV FLASK_APP app/app.py
13 |
14 | CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]
--------------------------------------------------------------------------------
/deployment.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import aws_cdk as cdk
6 |
7 | from backend.infrastructure import Backend
8 |
9 | class SampleApplicationBackend(cdk.Stage):
10 | def __init__(self, scope, id_, stage_name: str, **kwargs):
11 | super().__init__(scope, id_, **kwargs)
12 |
13 | stateless = cdk.Stack(self, "Stateless")
14 |
15 | self.backend = Backend(stateless, 'Backend', stage_name)
16 |
--------------------------------------------------------------------------------
/scripts/install_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ##Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | ##SPDX-License-Identifier: MIT-0
5 |
6 | set -o errexit
7 | set -o verbose
8 |
9 | wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.7.0.2747-linux.zip
10 | unzip sonar-scanner-cli-4.7.0.2747-linux.zip
11 |
12 | wget https://github.com/jeremylong/DependencyCheck/releases/download/v7.3.0/dependency-check-7.3.0-release.zip
13 | unzip dependency-check-7.3.0-release.zip
--------------------------------------------------------------------------------
/shared/shared_infra.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import aws_cdk as cdk
6 | from constructs import Construct
7 |
8 | from shared.network.infrastructure import Network
9 |
10 | class SharedInfraStack(cdk.Stack):
11 | def __init__(self, scope: Construct, id_: str, **kwargs) -> None:
12 | super().__init__(scope, id_, **kwargs)
13 |
14 | self.network = Network(self, "Network")
15 |
16 | cdk.CfnOutput(self, 'CoreVPC', value=self.network.vpc.vpc_id, export_name='CoreVPCId')
17 |
--------------------------------------------------------------------------------
/scripts/run_zap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ##Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | ##SPDX-License-Identifier: MIT-0
5 |
6 | set -o errexit
7 | set -o verbose
8 |
9 | mkdir $SECURITY_SCANNING_OUTPUT_DIR
10 |
11 | ################
12 | # OWASP ZAP
13 | ################
14 |
15 | echo $APPLICATION_URL_OUTPUT_KEY
16 | application_url=$(aws cloudformation list-exports --query "Exports[?Name=='$APPLICATION_URL_OUTPUT_KEY'].Value" --output text)
17 | docker run -v $(pwd):/zap/wrk/:rw --user root public.ecr.aws/deepfactor/zap2docker-stable:2.10.0-df zap-baseline.py -t $application_url -J $OWASP_ZAP_OUTPUT_FILE || true
18 | echo "OWASP ZAP analysis status is completed...";
--------------------------------------------------------------------------------
/shared/network/infrastructure.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import aws_cdk.aws_ec2 as ec2
6 | import aws_cdk.aws_ssm as ssm
7 | from constructs import Construct
8 |
9 | import constants
10 |
11 | class Network(Construct):
12 | def __init__(self, scope: Construct, id_: str) -> None:
13 | super().__init__(scope, id_)
14 |
15 | self.vpc = ec2.Vpc(self, 'VPC',
16 | ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16")
17 | )
18 |
19 | ssm.StringParameter(self, 'VPCID',
20 | parameter_name=constants.CORE_VPC_PARAMETER_NAME,
21 | string_value=self.vpc.vpc_id
22 | )
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5 | SPDX-License-Identifier: MIT-0
6 | """
7 | import aws_cdk as cdk
8 |
9 | from pipeline import DevSecOpsPipelineStack
10 | from shared.shared_infra import SharedInfraStack
11 | from sectools.infrastructure import SecTools
12 |
13 | import constants
14 |
15 | app = cdk.App()
16 |
17 | shared_infra = SharedInfraStack(
18 | app,
19 | 'SharedInfraStack',
20 | env=constants.DEV_ENV
21 | )
22 |
23 | DevSecOpsPipelineStack(
24 | app,
25 | "DevSecOpsPipelineStack",
26 | env=constants.DEV_ENV
27 | )
28 |
29 | SecTools(
30 | app,
31 | "SecToolsStack",
32 | env=constants.DEV_ENV
33 | )
34 | app.synth()
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
16 |
--------------------------------------------------------------------------------
/constants.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import aws_cdk as cdk
6 |
7 | CDK_APP_NAME = "SampleDevSecOps-CICD-Pipeline"
8 |
9 | CDK_APP_PYTHON_VERSION = "3.7"
10 |
11 | DEV_ACCOUNT_ID = ""
12 | REGION = ""
13 |
14 | DEV_ENV = cdk.Environment(account=DEV_ACCOUNT_ID, region=REGION)
15 |
16 | CODECOMMIT_REPOSITORY_NAME = "cdk-devsecops-cicd-pipeline"
17 |
18 | CORE_VPC_PARAMETER_NAME = "/SampleDevSecOpsCICDPipeline/CoreVPCID"
19 |
20 | SONARQUBE_SECRET_ARN_EXPORT_NAME = "SonarQubeSecretArn"
21 |
22 | SONARQUBE_RESULT_REPORT_OUTPUT_NAME = "sonar_result"
23 | SECURITY_SCANNING_RESULT_DIR = "security_scanning_output"
24 | SONARQUBE_SCAN_RESULT_OUTPUT_FILE = f"{SECURITY_SCANNING_RESULT_DIR}/sonarscan_result.txt"
25 | SONARQUBE_QUALITY_STATUS_OUTPUT_FILE = f"{SECURITY_SCANNING_RESULT_DIR}/sonar_quality_status.json"
26 | SONARQUBE_ISSUES_OUTPUT_FILE = f"{SECURITY_SCANNING_RESULT_DIR}/sonar_issues.json"
27 | OWASP_DEPENDENCY_CHECK_OUTPUT_FILE = f"{SECURITY_SCANNING_RESULT_DIR}/owasp_dependency_check_result.json"
28 | OWASP_ZAP_OUTPUT_FILE = f"{SECURITY_SCANNING_RESULT_DIR}/owasp_zap_result.json"
29 |
30 | SECURITY_HUB_PRODUCT_ARN = "arn:aws:securityhub:{0}:{1}:product/{1}/default".format(REGION, DEV_ACCOUNT_ID)
31 |
--------------------------------------------------------------------------------
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "python3 app.py",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "requirements*.txt",
11 | "source.bat",
12 | "**/__init__.py",
13 | "python/__pycache__",
14 | "tests"
15 | ]
16 | },
17 | "context": {
18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
19 | "@aws-cdk/core:stackRelativeExports": true,
20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
23 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26 | "@aws-cdk/core:checkSecretUsage": true,
27 | "@aws-cdk/aws-iam:minimizePolicies": true,
28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
30 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
31 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
32 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
33 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
34 | "@aws-cdk/core:enablePartitionLiterals": true,
35 | "@aws-cdk:enableDiffNoFail": true,
36 | "@aws-cdk/core:target-partitions": [
37 | "aws",
38 | "aws-cn"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/backend/infrastructure.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import pathlib
6 |
7 | from constructs import Construct
8 | from aws_cdk.aws_ecr_assets import DockerImageAsset
9 |
10 | import aws_cdk as cdk
11 | import aws_cdk.aws_ecs as ecs
12 | import aws_cdk.aws_ecs_patterns as ecsp
13 | import aws_cdk.aws_ec2 as ec2
14 | import aws_cdk.aws_ssm as ssm
15 |
16 | import constants
17 |
18 | class Backend(Construct):
19 | def __init__(self, scope: Construct, id_: str, stage_name: str) -> None:
20 | super().__init__(scope, id_)
21 |
22 | runtime_asset = str(pathlib.Path(__file__).parent.joinpath("runtime").resolve())
23 | docker_image_asset = DockerImageAsset(self, "SampleApp-API",
24 | directory=runtime_asset
25 | )
26 |
27 | vpc = ec2.Vpc.from_lookup(self, "VPC",
28 | vpc_id=ssm.StringParameter.value_from_lookup(self, constants.CORE_VPC_PARAMETER_NAME)
29 | )
30 |
31 | backend_ecs = ecsp.ApplicationLoadBalancedFargateService(self, "BackendAPI",
32 | task_image_options=ecsp.ApplicationLoadBalancedTaskImageOptions(
33 | image=ecs.ContainerImage.from_ecr_repository(
34 | repository=docker_image_asset.repository,
35 | tag=docker_image_asset.image_tag),
36 | container_port=5000
37 | ),
38 | public_load_balancer=True,
39 | vpc=vpc
40 | )
41 |
42 | self.application_url_output_key = f'{constants.CDK_APP_NAME}-{stage_name}'
43 |
44 | cdk.CfnOutput(self, 'SonarQubeSecretArnOutput',
45 | value=f"http://{backend_ecs.load_balancer.load_balancer_dns_name}",
46 | export_name=self.application_url_output_key
47 | )
48 |
49 |
--------------------------------------------------------------------------------
/scripts/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ##Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | ##SPDX-License-Identifier: MIT-0
5 |
6 | set -o errexit
7 | set -o verbose
8 |
9 | mkdir $SECURITY_SCANNING_OUTPUT_DIR
10 |
11 | ################
12 | # SonarQube
13 | ################
14 |
15 | ./sonar-scanner-4.7.0.2747-linux/bin/sonar-scanner -Dsonar.host.url=$HOST -Dsonar.login=$LOGIN -Dsonar.projectKey=$PROJECT -Dsonar.sources=backend/runtime/ > $SONAR_SCAN_OUTPUT_FILE
16 | sonar_task_id=$(cat $SONAR_SCAN_OUTPUT_FILE | egrep -o "task\?id=[^ ]+" | cut -d'=' -f2)
17 | stat="PENDING";
18 | while [ "$stat" != "SUCCESS" ]; do
19 | if [ $stat = "FAILED" ] || [ $stat = "CANCELLED" ]; then
20 | echo "SonarQube task $sonar_task_id failed";
21 | exit 1;
22 | fi
23 | stat=$(curl -u $LOGIN: $HOST/api/ce/task\?id=$sonar_task_id | jq -r '.task.status');
24 | sleep 5;
25 | done
26 |
27 | sonar_analysis_id=$(curl -u $LOGIN: $HOST/api/ce/task\?id=$sonar_task_id | jq -r '.task.analysisId')
28 | curl -o $SONAR_QUALITY_STATUS_OUTPUT_FILE -u $LOGIN: $HOST/api/qualitygates/project_status\?analysisId=$sonar_analysis_id
29 | quality_status=$(cat $SONAR_QUALITY_STATUS_OUTPUT_FILE | jq -r '.projectStatus.status')
30 | curl -o $SONAR_ISSUES_OUTPUT_FILE -u $LOGIN: $HOST/api/issues/search?createdAfter=2022-11-10&componentKeys=$PROJECT&severities=CRITICAL,BLOCKER&types=VULNERABILITY&onComponentOnly=true
31 |
32 | if [ $FAIL_BUILD_FOR_SONAR_QUALITY_STATUS = true ] ; then
33 | if [ $quality_status = "ERROR" ] || [ $quality_status = "WARN" ]; then
34 | echo "in quality_status ERROR or WARN condition"
35 | exit 1;
36 | elif [ $quality_status = "OK" ]; then
37 | echo "in quality_status OK condition"
38 | else
39 | echo "in quality_status unexpected condition"
40 | exit 1;
41 | fi
42 | fi
43 |
44 | ########################
45 | # OWASP Dependency-Check
46 | ########################
47 | ./dependency-check/bin/dependency-check.sh --project $PROJECT --format JSON --prettyPrint --enableExperimental --scan backend/runtime --out $OWASP_DEPENDENCY_CHECK_OUTPUT_FILE
48 | echo "OWASP dependency check analysis status is completed...";
49 |
--------------------------------------------------------------------------------
/securityhub/securityhub_report_step.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 |
6 | import jsii
7 | import pathlib
8 |
9 | from aws_cdk import aws_codepipeline_actions as cpa
10 | from aws_cdk import aws_codepipeline as codepipeline
11 | from aws_cdk import pipelines
12 | from aws_cdk import aws_lambda as lambda_
13 | from aws_cdk import aws_iam as iam
14 |
15 | from constructs import Construct
16 |
17 | import constants
18 |
19 | @jsii.implements(pipelines.ICodePipelineActionFactory)
20 | class SecurityHubReportStep(pipelines.Step):
21 | def __init__(self, id_: str, inputs: pipelines.FileSet, **kwargs) -> None:
22 | super().__init__(id_)
23 |
24 | self.inputs = inputs
25 |
26 | @jsii.member(jsii_name="produceAction")
27 | def produce_action(
28 | self,
29 | stage: codepipeline.IStage,
30 | options: pipelines.ProduceActionOptions,
31 | ) -> pipelines.CodePipelineActionFactoryResult:
32 |
33 | runtime_asset = str(pathlib.Path(__file__).parent.joinpath("runtime").resolve())
34 | function = lambda_.Function(options.scope, "InvokeActionFunction",
35 | runtime=lambda_.Runtime.PYTHON_3_9,
36 | handler="invoke_action_function.handler",
37 | code=lambda_.Code.from_asset(runtime_asset),
38 | environment={
39 | "SECURITY_HUB_PRODUCT_ARN": constants.SECURITY_HUB_PRODUCT_ARN
40 | }
41 | )
42 | function.add_to_role_policy(
43 | statement=iam.PolicyStatement(
44 | actions=[
45 | 'securityhub:BatchImportFindings'
46 | ],
47 | resources=[constants.SECURITY_HUB_PRODUCT_ARN],
48 | )
49 | )
50 |
51 | stage.add_action(
52 | cpa.LambdaInvokeAction(
53 | action_name="SecurityHubReport",
54 | inputs= [options.artifacts.to_code_pipeline(input) for input in self.inputs],
55 | lambda_=function,
56 | )
57 | )
58 |
59 | return pipelines.CodePipelineActionFactoryResult(run_orders_consumed=1)
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/sectools/infrastructure.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import os
6 | from constructs import Construct
7 | import aws_cdk as cdk
8 |
9 | from aws_cdk import aws_ec2 as ec2
10 | from aws_cdk import aws_iam as iam
11 | from aws_cdk import aws_ecs as ecs
12 | from aws_cdk import aws_rds as rds
13 | from aws_cdk import aws_secretsmanager as secretsmanager
14 | from aws_cdk import aws_ssm as ssm
15 |
16 | import aws_cdk.aws_ecs_patterns as ecsp
17 |
18 | import constants
19 |
20 | #########################################
21 | # In this section we need to create new #
22 | # infra resrouces used to deploy tools #
23 | #########################################
24 |
25 | # Class defined to deploy new ECS infra and Security Tools
26 | class SecTools(cdk.Stack):
27 |
28 | def __init__(self, scope: Construct, id: str, **kwargs) -> None:
29 | super().__init__(scope, id, **kwargs)
30 |
31 | #########################################
32 | # Shared components #
33 | #########################################
34 |
35 | vpc = ec2.Vpc.from_lookup(self, "VPC",
36 | vpc_id=ssm.StringParameter.value_from_lookup(
37 | self, constants.CORE_VPC_PARAMETER_NAME)
38 | )
39 |
40 | # Create IAM Role using AWS managed policies with permissions to deploy ECS Tasks
41 | ecs_task_role = iam.Role(
42 | self,
43 | id="ECSTaskRole",
44 | role_name="ECSTaskRole",
45 | assumed_by=iam.ServicePrincipal(service="ecs-tasks.amazonaws.com"),
46 | managed_policies=[
47 | iam.ManagedPolicy.from_aws_managed_policy_name(
48 | "service-role/AmazonECSTaskExecutionRolePolicy")
49 | ]
50 | )
51 |
52 | cluster = ecs.Cluster(self, "SecurityToolsECSCluster",
53 | capacity=ecs.AddCapacityOptions(
54 | instance_type=ec2.InstanceType('m5.large')),
55 | vpc=vpc
56 | )
57 |
58 | asg = cluster.autoscaling_group
59 | asg.add_user_data(
60 | 'sudo sysctl -w vm.max_map_count=524288',
61 | 'sudo sysctl -w fs.file-max=131072',
62 | 'sudo ulimit -n 131072',
63 | 'sudo ulimit -u 8192'
64 | 'sudo echo "vm.max_map_count=524288" >> /etc/sysctl.conf',
65 | 'sudo sysctl -p'
66 | )
67 |
68 | #########################################
69 | # SonarQube #
70 | #########################################
71 |
72 | # Create SG for RDS PostGres to be used by Sonarqube allowing all outbound traffic
73 | self.sg = ec2.SecurityGroup(
74 | self, 'RDSSecurityGroup',
75 | vpc=vpc,
76 | allow_all_outbound=True,
77 | description="RDS Instance Security Group"
78 | )
79 |
80 | # Create Sonarqube Postgres DB for Sonarqube service connect
81 | self.database = rds.DatabaseInstance(self, 'SonarqubeDB',
82 | engine=rds.DatabaseInstanceEngine.postgres(
83 | version=rds.PostgresEngineVersion.VER_14_2),
84 | database_name="sonarqube",
85 | credentials=rds.Credentials.from_generated_secret(
86 | "sonar_creds"),
87 | instance_type=ec2.InstanceType.of(
88 | ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM),
89 | vpc=vpc,
90 | multi_az=True,
91 | vpc_subnets=ec2.SubnetSelection(
92 | subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS,
93 | ),
94 | publicly_accessible=False,
95 | security_groups=[self.sg]
96 | )
97 |
98 | rds_url = 'jdbc:postgresql://{}/sonarqube'.format(
99 | self.database.db_instance_endpoint_address)
100 |
101 | # Addins SG rule allowing access to DB from VPC
102 | self.sg.add_ingress_rule(
103 | ec2.Peer.ipv4('10.0.0.0/16'), ec2.Port.tcp(5432), "AccessToDB"
104 | )
105 |
106 | sonarqube_ecs_service = ecsp.ApplicationLoadBalancedEc2Service(self, "SonarquebEcsTask",
107 | task_image_options=ecsp.ApplicationLoadBalancedTaskImageOptions(
108 | environment={
109 | 'sonar.jdbc.url': rds_url,
110 | },
111 | image=ecs.ContainerImage.from_registry(
112 | "public.ecr.aws/docker/library/sonarqube:latest"),
113 | container_port=9000,
114 | secrets={
115 | "sonar.jdbc.username": ecs.Secret.from_secrets_manager(self.database.secret, field="username"),
116 | "sonar.jdbc.password": ecs.Secret.from_secrets_manager(self.database.secret, field="password")
117 | },
118 | task_role=ecs_task_role,
119 | ),
120 | public_load_balancer=True,
121 | cluster=cluster,
122 | cpu=512,
123 | memory_limit_mib=2048,
124 | )
125 |
126 | sonarqube_secret = secretsmanager.Secret(self, "SonarQubeSecret",
127 | secret_object_value={
128 | "host": cdk.SecretValue.unsafe_plain_text(f'http://{sonarqube_ecs_service.load_balancer.load_balancer_dns_name}')
129 | }
130 | )
131 |
132 | #########################################
133 | # Outputs #
134 | #########################################
135 |
136 | cdk.CfnOutput(self, 'SonarQubeSecretArnOutput',
137 | value=sonarqube_secret.secret_full_arn,
138 | export_name=constants.SONARQUBE_SECRET_ARN_EXPORT_NAME
139 | )
140 |
--------------------------------------------------------------------------------
/pipeline.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | SPDX-License-Identifier: MIT-0
4 | """
5 | import aws_cdk as cdk
6 | from aws_cdk import aws_codecommit as codecommit
7 | from aws_cdk import aws_codebuild as codebuild
8 | from aws_cdk import pipelines
9 | from aws_cdk import aws_secretsmanager as secretsmanager
10 | from aws_cdk import aws_iam as iam
11 | from constructs import Construct
12 |
13 | from deployment import SampleApplicationBackend
14 | from securityhub.securityhub_report_step import SecurityHubReportStep
15 |
16 | import constants
17 |
18 | class DevSecOpsPipelineStack(cdk.Stack):
19 |
20 | def __init__(self, scope: Construct, id_: str, **kwargs) -> None:
21 | super().__init__(scope, id_, **kwargs)
22 |
23 | repository = codecommit.Repository.from_repository_name(self,
24 | 'CodeCommitRepo',
25 | constants.CODECOMMIT_REPOSITORY_NAME
26 | )
27 | source = pipelines.CodePipelineSource.code_commit(repository, branch='main')
28 |
29 | sonarqube_secret_arn = cdk.Fn.import_value(constants.SONARQUBE_SECRET_ARN_EXPORT_NAME)
30 | sonar_secret = secretsmanager.Secret.from_secret_complete_arn(self, 'SonarQubeSecret',
31 | sonarqube_secret_arn
32 | )
33 |
34 | build_spec = codebuild.BuildSpec.from_object(
35 | {
36 | "env": {
37 | "secrets-manager": {
38 | "LOGIN": f'{sonar_secret.secret_full_arn}:access_token',
39 | "HOST": f'{sonar_secret.secret_full_arn}:host',
40 | "PROJECT": f'{sonar_secret.secret_full_arn}:project'
41 | },
42 | "variables": {
43 | "SECURITY_SCANNING_OUTPUT_DIR": constants.SECURITY_SCANNING_RESULT_DIR,
44 | "SONAR_SCAN_OUTPUT_FILE": constants.SONARQUBE_SCAN_RESULT_OUTPUT_FILE,
45 | "SONAR_QUALITY_STATUS_OUTPUT_FILE": constants.SONARQUBE_QUALITY_STATUS_OUTPUT_FILE,
46 | "SONAR_ISSUES_OUTPUT_FILE": constants.SONARQUBE_ISSUES_OUTPUT_FILE,
47 | "OWASP_DEPENDENCY_CHECK_OUTPUT_FILE": constants.OWASP_DEPENDENCY_CHECK_OUTPUT_FILE,
48 | "FAIL_BUILD_FOR_SONAR_QUALITY_STATUS": False
49 | }
50 | },
51 | "phases": {
52 | "install": {
53 | "runtime-versions": {
54 | "python": constants.CDK_APP_PYTHON_VERSION
55 | },
56 | "commands": [
57 | "./scripts/install_dependencies.sh",
58 | "npm install",
59 | "pip3 install -r requirements.txt"
60 | ],
61 | },
62 | "build": {
63 | "commands": [
64 | "./scripts/run_tests.sh",
65 | "npx cdk synth"
66 | ]
67 | }
68 | },
69 | "version": "0.2",
70 | }
71 | )
72 |
73 | # Synth CDK application and build artifacts
74 | synth_action = pipelines.CodeBuildStep(
75 | 'Build',
76 | input=source,
77 | partial_build_spec=build_spec,
78 | commands=[],
79 | role_policy_statements=[
80 | iam.PolicyStatement(
81 | actions=[
82 | 'secretsmanager:DescribeSecret',
83 | 'secretsmanager:GetSecretValue'
84 | ],
85 | resources=[sonarqube_secret_arn],
86 | ),
87 | iam.PolicyStatement(
88 | actions=[
89 | 'ssm:GetParameter',
90 | ],
91 | resources=[f'arn:aws:ssm:{constants.REGION}:{constants.DEV_ACCOUNT_ID}:parameter{constants.CORE_VPC_PARAMETER_NAME}'],
92 | ),
93 | ],
94 | build_environment=codebuild.BuildEnvironment(
95 | privileged=True
96 | ),
97 | cache=codebuild.Cache.local(codebuild.LocalCacheMode.DOCKER_LAYER),
98 | )
99 | scan_report_output = synth_action.add_output_directory(
100 | constants.SECURITY_SCANNING_RESULT_DIR
101 | )
102 |
103 | pipeline = pipelines.CodePipeline(self,
104 | 'Pipeline',
105 | docker_enabled_for_synth=True,
106 | synth=synth_action,
107 | )
108 |
109 | # Deploy application in dev
110 | sample_application_backend = SampleApplicationBackend(
111 | self,
112 | f'{constants.CDK_APP_NAME}-Dev',
113 | 'dev',
114 | env=constants.DEV_ENV
115 | )
116 | pipeline.add_stage(
117 | sample_application_backend,
118 | pre=[
119 | pipelines.ManualApprovalStep("PromoteToDev")
120 | ]
121 | )
122 |
123 | # Scan with OWASP ZAP
124 | owasp_zap_build_spec = codebuild.BuildSpec.from_object(
125 | {
126 | "env": {
127 | "variables": {
128 | "SECURITY_SCANNING_OUTPUT_DIR": constants.SECURITY_SCANNING_RESULT_DIR,
129 | "OWASP_ZAP_OUTPUT_FILE": constants.OWASP_ZAP_OUTPUT_FILE,
130 | "APPLICATION_URL_OUTPUT_KEY": sample_application_backend.backend.application_url_output_key
131 | }
132 | },
133 | "phases": {
134 | "build": {
135 | "commands": [
136 | "./scripts/run_zap.sh"
137 | ]
138 | }
139 | },
140 | "version": "0.2",
141 | }
142 | )
143 | owasp_zap_build_step = pipelines.CodeBuildStep(
144 | 'OwaspZap',
145 | input=source,
146 | partial_build_spec=owasp_zap_build_spec,
147 | build_environment=codebuild.BuildEnvironment(
148 | privileged=True
149 | ),
150 | commands=[],
151 | role_policy_statements=[
152 | iam.PolicyStatement(
153 | actions=[
154 | 'cloudformation:ListExports',
155 | ],
156 | resources=['*'],
157 | )
158 | ],
159 | cache=codebuild.Cache.local(codebuild.LocalCacheMode.DOCKER_LAYER),
160 | )
161 | zap_scan_report_output = owasp_zap_build_step.add_output_directory(
162 | constants.SECURITY_SCANNING_RESULT_DIR
163 | )
164 | pipeline.add_wave("OWASP-ZAP").add_pre(owasp_zap_build_step)
165 |
166 | # Send report to Security Hub
167 | pipeline.add_wave("SecurityHub-Report").add_pre(
168 | SecurityHubReportStep('SecurityHubReportStep', inputs=[scan_report_output, zap_scan_report_output])
169 | )
170 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AWS CI/CD Pipeline for DevSecOps
2 |
3 | This project is a reference implementation for a CI/CD pipeline integrated with security vulnerability scanning tools.
4 |
5 | The pipeline is implemented as code using [AWS CDK](https://aws.amazon.com/cdk/) and the [CDK Pipelines construct](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.pipelines-readme.html). The current implementation performs security vulnerability scanning using SonarQube and Dependency-Check. The pipeline sends to [AWS Security Hub](https://aws.amazon.com/security-hub/) the reports for the security scanning executions. It also contains a sample application implementation for testing purposes.
6 |
7 | ## Project structure
8 |
9 | * `backend/infrastructure.py`: definition of the infrastructure components necessary to run the `backend` component of the sample application.
10 | * `backend/runtime/`: the actual code of the sample application
11 | * `shared/`: definition of the core components that the security tools and components of the sample application share.
12 | * `securityhub/`: implementation of the integration with AWS Security Hub
13 | * `sectools/`: definition of all the security tools the pipeline uses for security vulnerability scanning.
14 | * `pipeline.py`: definition of the CI/CD pipeline as code.
15 | * `deployment.py`: definition of the deployment unit of the sample application that the pipeline will deploy.
16 |
17 | ## Create development environment
18 | See [Getting Started With the AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html)
19 | for additional details and prerequisites
20 |
21 | ### Clone the code
22 | ```bash
23 | git clone https://github.com/aws-samples/cdk-devsecops-cicd-pipeline
24 | cd cdk-devsecops-cicd-pipeline
25 | ```
26 |
27 | ### Create Python virtual environment and install the dependencies
28 | ```bash
29 | python3.7 -m venv .venv
30 | source .venv/bin/activate
31 | pip install -r requirements.txt
32 | ```
33 |
34 | ### Deploy to a sandbox environment
35 |
36 | To deploy the stacks, use CDK commands. If you are new to CDK, see [Getting started with the AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html).
37 |
38 | Before proceeding, update the `DEV_ACCOUNT_ID` and `REGION` values on `constants.py` to the AWS account ID of your sandbox environment and your region of choice. After that, run a `cdk ls` command to test if everything is correct with your CDK app. This will list the 4 stacks existing on the CDK app. Note that a file called `cdk.context.json` will also be generated in the root of the project. This is the runtime context file, and it must be commited to your source control. See [Runtime context](https://docs.aws.amazon.com/cdk/v2/guide/context.html) to learn more about that.
39 |
40 | #### Push the code to AWS CodeCommit
41 | For this sample CI/CD pipeline, we are using [AWS CodeCommit](https://aws.amazon.com/codecommit/) as a Git repository. For your own projects, you can update `pipeline.py` to use your Git repository of choice.
42 |
43 | From the terminal, create a new Git repository on your sandbox environment:
44 | ```bash
45 | aws codecommit create-repository --repository-name cdk-devsecops-cicd-pipeline
46 | ```
47 |
48 | After you created the repository, push the code of this sample pipeline to CodeCommit. If you are new to CodeCommit, see [Getting started with Git and AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/getting-started.html).
49 |
50 | #### Bootstrap CDK
51 | ```bash
52 | cdk bootstrap aws://
53 | ```
54 |
55 | #### Deploy shared components
56 | ```bash
57 | cdk deploy SharedInfraStack
58 | ```
59 |
60 | #### Deploy security tools
61 | ```bash
62 | cdk deploy SecToolsStack
63 | ```
64 |
65 | After deploying the security tools, you'll see output values from CDK on your terminal. Find an output named `SecToolsStack.SonarquebEcsTaskServiceURLABCXYZ`, which is the URL from where your [SonarQube](https://www.sonarqube.org/) instance is responding.
66 |
67 | Example output:
68 | ```text
69 | ✅ SecToolsStack
70 |
71 | ✨ Deployment time: 1018.67s
72 |
73 | Outputs:
74 | SecToolsStack.SonarQubeSecretArnOutput = arn:aws:secretsmanager:us-east-1:.elb.amazonaws.com
76 | SecToolsStack.SonarquebEcsTaskServiceURLE4434029 = http://SecTo-Sonar-ABCXYZ..elb.amazonaws.com
77 | ```
78 |
79 | To interact with SonarQube's APIs, you need to generate a user token. See [Generating and Using Tokens](https://docs.sonarqube.org/latest/user-guide/user-token/) on SonarQube's documentation to learn how to create yours. You'll also have to create a project on SonarQube to represent the sample application we are using in this reference implementation. The deployment of DB instance associated with SonarQube service can take between 15 to 20 minutes.
80 |
81 | As part of the `SecToolsStack`, a secret is created on [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). Find on the CDK output on your terminal the ARN of this secret, which must be similar to `SecToolsStack.SonarQubeSecretArnOutput`. Update this secret with the SonarQube access data:
82 | ```bash
83 | aws secretsmanager put-secret-value \
84 | --secret-id \
85 | --secret-string "{\"access_token\":\"\",\"host\":\" 0:
237 | raise Exception("Failed to import finding: {}".format(response['FailedCount']))
238 |
239 | code_pipeline.put_job_success_result(jobId=job_id)
240 |
241 | except Exception as e:
242 | traceback.print_exc()
243 | code_pipeline.put_job_failure_result(
244 | jobId=job_id,
245 | failureDetails={
246 | 'message': 'Function exception: ' + str(e),
247 | 'type': 'JobFailed'
248 | }
249 | )
250 |
251 | return "Complete."
252 |
--------------------------------------------------------------------------------