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

Add Secure Static Analysis to Jenkins Pipeline #39

Merged
merged 10 commits into from
Nov 2, 2023
30 changes: 29 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,38 @@ jobs:
- name: Set up Python 3
uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run Pytest
run: |
python -m pytest -v
cdk_nag:
runs-on: ubuntu-latest
defaults:
run:
working-directory: cdk
steps:
- uses: actions/checkout@v3
- name: Set up Python 3
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
npm install -g aws-cdk
pip install -r requirements.txt
- name: Setup test environment
run: |
export CDK_DEFAULT_ACCOUNT=012345678912
export CDK_DEFAULT_REGION=us-west-2
cp tests/cdk.context.json .
- name: Synth pipeline and jenkins server stacks
run: |
cdk synth \
--context codestar-connection=arn:aws:codestar-connections/connection_id \
--context repo=org/repo \
--context branch=branch \
--context cert-arn=arn:aws:acm:certificate/certificate_id
cdk synth --app "python jenkins_server/app.py" --context cert-arn=arn:aws:acm:certificate/certificate_id --no-lookups
2 changes: 2 additions & 0 deletions cdk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import sys
import aws_cdk as cdk

from cdk_nag import AwsSolutionsChecks
from pipeline import JenkinsPipeline, MissingContextError

# Account and region set by the default AWS profile or one specified with --profile
ACCOUNT = os.environ.get('CDK_DEFAULT_ACCOUNT')
REGION = os.environ.get('CDK_DEFAULT_REGION')

app = cdk.App()
cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True))

try:
JenkinsPipeline(app, 'JenkinsPipelineStack', env=cdk.Environment(account=ACCOUNT, region=REGION))
Expand Down
2 changes: 2 additions & 0 deletions cdk/jenkins_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import sys
import aws_cdk as cdk

from cdk_nag import AwsSolutionsChecks
from jenkins_server import JenkinsServerStack

# Account and region set by the default AWS profile or one specified with --profile
ACCOUNT = os.environ.get('CDK_DEFAULT_ACCOUNT')
REGION = os.environ.get('CDK_DEFAULT_REGION')

app = cdk.App()
cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True))

try:
JenkinsServerStack(app, 'JenkinsServerStack',
Expand Down
35 changes: 30 additions & 5 deletions cdk/jenkins_server/jenkins_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import aws_cdk.aws_efs as efs
import aws_cdk.aws_elasticloadbalancingv2 as elb
import aws_cdk.aws_iam as iam
import aws_cdk.aws_kms as kms
import aws_cdk.aws_logs as logs
import aws_cdk.aws_sns as sns
import aws_cdk.aws_s3 as s3

from os import path
from aws_cdk import Stack
from cdk_nag import NagSuppressions
from constructs import Construct


Expand All @@ -43,11 +45,13 @@ def __init__(self, scope: Construct, id: str, **kwargs) -> None:
self.cert_arn = self._load_cert_arn()

self.vpc = self._create_vpc()
self.build_topic = sns.Topic(self, 'BuildTopic')
self.build_topic = sns.Topic(self, 'BuildTopic', master_key=kms.Key(self, "SNSKey", enable_key_rotation=True))
self.log_group = logs.LogGroup(self, 'LogGroup')
self.file_system, self.access_point = self._create_efs()
self.fargate_service = self._create_ecs()
self._create_alb()
self._add_nag_suppressions()


def _load_stack_config(self, config_file):
"""Load stack config. The config file is expected to be in the same directory."""
Expand All @@ -69,12 +73,14 @@ def _load_cert_arn(self):

def _create_vpc(self):
"""Create a new VPC or use an existing one if a VPC ID is provided."""
if self.stack_tags['vpc-id'] == 'None': # Context values will be converted to string and cannot be empty during synth
return ec2.Vpc(self, 'VPC',
if self.stack_tags.get('vpc-id', 'None') == 'None': # Tag values from pipeline will be converted to string and cannot be empty during synth
vpc = ec2.Vpc(self, 'VPC',
cidr=self.stack_config['vpc']['cidr'],
nat_gateways=self.stack_config['vpc']['nat_gateways']
)
return ec2.Vpc.from_lookup(self, 'VPC', vpc_id=self.stack_tags['vpc-id'])
vpc.add_flow_log("JenkinsVPCFlowLog")
return vpc
return ec2.Vpc.from_lookup(self, 'VPC', vpc_id=self.stack_tags.get('vpc-id'))

def _create_efs(self):
"""Create a file system with an access point for the jenkins home directory."""
Expand Down Expand Up @@ -216,7 +222,8 @@ def _create_alb(self):
alb.log_access_logs(
s3.Bucket(self, 'AccessLogsBucket',
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
encryption=s3.BucketEncryption.S3_MANAGED
encryption=s3.BucketEncryption.S3_MANAGED,
enforce_ssl=True
)
)

Expand All @@ -225,3 +232,21 @@ def _create_alb(self):

if alb_config['public'] is True:
alb.connections.allow_from_any_ipv4(ec2.Port.tcp(443))

def _add_nag_suppressions(self):
'''Add cdk-nag suppressions for the Jenkins server stack.'''
suppression_list = [
{
'id': 'AwsSolutions-IAM5',
'reason': 'Wildcard permissions required to allow pulling user added Jenkins configs from parameter store.'
},
{
'id': 'AwsSolutions-EC23',
'reason': 'Allows enabling public access to server. Additional IP based security will be setup through WAF.'
},
{
'id': 'AwsSolutions-S1',
'reason': 'Access logs bucket does not need access logging enabled.'
}
]
NagSuppressions.add_stack_suppressions(self, suppression_list)
29 changes: 29 additions & 0 deletions cdk/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import aws_cdk.pipelines as pipelines

from aws_cdk import Environment, Stack, Stage
from cdk_nag import NagSuppressions
from constructs import Construct
from jenkins_server.jenkins_server import JenkinsServerStack

Expand Down Expand Up @@ -50,6 +51,7 @@ def __init__(self, scope: Construct, id: str, **kwargs) -> None:
self.source = pipelines.CodePipelineSource.connection(self.repo, self.branch, connection_arn=self.codestar_connection)

self._create_pipeline()
self._add_nag_suppressions()

def _get_required_context(self, context_name):
"""Get context value and raise an exception if it does not exist."""
Expand Down Expand Up @@ -119,3 +121,30 @@ def _create_pipeline(self):
pipelines.ManualApprovalStep('ReleaseToProd')
]
)

def _add_nag_suppressions(self):
'''Add cdk-nag suppressions for the pipeline stack.

The CDK Pipeline library generates internal constructs that are not defined here but may be generate rule violations
for cdk-nag. Adding suppressions at stack level to avoid errors.

'''
suppression_list = [
{
'id': 'AwsSolutions-S1',
'reason': 'CDK Pipeline generates its own S3 buckets for pipeline assets.'
},
{
'id': 'AwsSolutions-IAM5',
'reason': 'CDK Pipeline generates its own IAM permissions.'
},
{
'id': 'AwsSolutions-CB3',
'reason': 'CDK Pipeline requires privileged mode for its CodeBuild project to build docker images.'
},
{
'id': 'AwsSolutions-CB4',
'reason': 'CDK Pipeline generates its own CodeBuild project for pipeline assets.'
}
]
NagSuppressions.add_stack_suppressions(self, suppression_list)
29 changes: 16 additions & 13 deletions cdk/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
attrs==22.1.0
aws-cdk-lib==2.95.1
cattrs==22.1.0
constructs==10.1.84
exceptiongroup==1.0.0rc8
iniconfig==1.1.1
jsii==1.88.0
packaging==21.3
pluggy==1.0.0
attrs==23.1.0
aws-cdk-lib==2.103.1
aws-cdk.asset-awscli-v1==2.2.201
aws-cdk.asset-kubectl-v20==2.1.2
aws-cdk.asset-node-proxy-agent-v6==2.0.1
cattrs==23.1.2
cdk-nag==2.27.179
constructs==10.3.0
exceptiongroup==1.1.3
importlib-resources==6.1.0
iniconfig==2.0.0
jsii==1.91.0
packaging==23.2
pluggy==1.3.0
publication==0.0.3
py==1.11.0
pyparsing==3.0.9
pytest==7.1.2
pytest==7.4.3
python-dateutil==2.8.2
six==1.16.0
tomli==2.0.1
typeguard==2.13.3
typing_extensions==4.3.0
typing_extensions==4.8.0
8 changes: 8 additions & 0 deletions cdk/tests/cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"availability-zones:account=012345678912:region=us-west-2": [
"us-west-2a",
"us-west-2b",
"us-west-2c",
"us-west-2d"
]
}
2 changes: 1 addition & 1 deletion cdk/tests/test_jenkins_server_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_stack_context_values(template):
def test_required_resources(template):
template.resource_count_is("AWS::EC2::VPC", 1)
template.resource_count_is("AWS::SNS::Topic", 1)
template.resource_count_is("AWS::Logs::LogGroup", 1)
template.resource_count_is("AWS::Logs::LogGroup", 2)
template.resource_count_is("AWS::EFS::FileSystem", 1)
template.resource_count_is("AWS::EFS::AccessPoint", 1)
template.resource_count_is("AWS::ECS::Cluster", 1)
Expand Down