Skip to content

Commit

Permalink
Add Secure Static Analysis to Jenkins Pipeline #39
Browse files Browse the repository at this point in the history
  • Loading branch information
brianherrera authored Nov 2, 2023
2 parents 43138ff + e35e695 commit 2873905
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 20 deletions.
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

0 comments on commit 2873905

Please sign in to comment.