システム開発におけるセキュリティシフトレフトの実践 ~SASTパイプラインの作成~
近年、システム開発においてセキュリティの重要性がますます高まっています。特に「開発・運用プロセス全体にセキュリティを組み込む」というDevSecOpsの考え方が普及し、セキュリティを開発初期(シフトレフト)で実装する取り組みが重視されています。
本記事では、その中でもSAST(静的アプリケーションセキュリティテスト)にフォーカスし、実際にパイプラインに組み込んでみた内容について紹介します。
セキュリティテストの種類
セキュリティテストは主に以下の3つに分類されます。
-
SCA(Software Composition Analysis)
→ ライブラリやOSSの脆弱性検査 -
SAST(Static Application Security Testing)
→ ソースコードの静的解析による脆弱性検査 -
DAST(Dynamic Application Security Testing)
→ アプリケーションの実行時に外部から脆弱性を検査
今回はこの中からSASTにフォーカスし、SonarQubeを使ったパイプライン連携に挑戦しました。
SonarQubeとは
SonarQubeはオープンソースの静的コード解析ツールです。コードの品質問題、セキュリティ脆弱性、バグを自動的に検出し、開発者に報告する機能を備えています。無料版(Community Edition)が提供されており、個人開発や小規模プロジェクトでも気軽に導入できるのが大きなメリットのため、このツールを使うことにしました。
やってみたこと
EC2でSonarQube環境を構築
構築する環境は、EC2にSonarQubeをインストールして立ち上げ、ソースコードの場所をSonarQubeサーバーに渡すことによって、静的コードチェックを行い、SonarQube内に診断レポートを作成する環境になります。この環境より開発チームは常に最新のコード品質状況を把握できるようになります。
EC2インスタンス作成
- マシンイメージはAmazonLinux2023
- インスタンスタイプは「t3.medium」以上推奨
- キーペアはなし(SSM接続のポリシーがIAMユーザーorIAMロールにアタッチされている必要がある)
- ネットワーク設定はSonarQubeを稼働させたいVPC,サブネットを指定
- セキュリティグループは新規作成
上記設定でインスタンスを作成
EC2にSSMからアクセスして、SonarQube立ち上げ
-
Dockerインストール
sudo yum install -y docker sudo systemctl start docker sudo systemctl enable docker
-
Docker composeインストール
sudo curl -L "https://github.com/docker/compose/releases/download/v2.34.0/docker- compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
-
sonarqube用のDockerfile保存ディレクトリ作成
mkdir ~/sonarqube cd ~/sonarqube
-
Docker composeファイル作成
vi docker-compose.yml
ファイル内容
services:
sonarqube:
image: sonarqube:lts
container_name: sonarqube
environment:
- SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/*****
- SONARQUBE_JDBC_USERNAME=*********
- SONARQUBE_JDBC_PASSWORD=*********
ports:
- "9000:9000"
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
networks:
- sonarnetwork
db:
image: postgres:13
container_name: sonar_postgres
environment:
- POSTGRES_USER=******
- POSTGRES_PASSWORD=*********
- POSTGRES_DB=**********
volumes:
- sonar_postgres_data:/var/lib/postgresql/data
networks:
- sonarnetwork
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonar_postgres_data:
networks:
sonarnetwork:
- Docker compose実行
sudo docker-compose up -d
- アクセス確認
http://[EC2パブリックIP]:9000 //パブリックIPは今アクセスしているEC2インスタンスのIPv4パブリックIP
SonarQubeでプロジェクト作成
- アクセスして初回ログイン(初期ユーザー名、パスワードは両方とも「admin」)
- パスワード変更(初回アクセス時のみ)
- プロジェクト作成
- ローカルでスキャン実行するためのコマンドをインストール
- 下記リンクにアクセスしてSonarScannerコマンドをインストール
https://docs.sonarsource.com/sonarqube-server/9.9/analyzing-source-code/scanners/sonarscanner/
※ 前提としてJavaがインストールされていること(インストールがまだの人はOpenJDKをインストールしてください)
https://openjdk.org/
- 下記リンクにアクセスしてSonarScannerコマンドをインストール
ローカルからスキャン実行
ローカル環境でSonarQubeをセットアップし、セットアップ中に表示されたコマンドで静的コード解析を試しました。
sonar-scanner \
-Dsonar.projectKey=sampleProject \
-Dsonar.sources=. \
-Dsonar.host.url=http://xx.xx.xx.xx:9000 \
-Dsonar.login=sqp_9999999999999999
スキャン結果
脆弱性や、CodeSmellなどが検知できていることを確認
AWS上で実行(実装はIaC)
次に、AWS上に環境を構築し、パイプラインに組み込んだ形で実行してみました。
パイプラインに日時で実行(実装はIaC)
毎日(8:30)にトリガー起動をして、自動的にSASTが走る仕組みを構築しました。
Slackで結果通知(実装はIaC)
さらに、解析結果をSlackに通知し、チーム全体で結果を素早く把握できるようにしました。
IaCによる環境構築
今回の取り組みでは、Infrastructure as Code(IaC) を活用してSonarQube環境を整備しました。
今回はAWS CDKで実装しました。
-
EC2+SonarQube構築
EC2上にSonarQubeサーバーを構築。
VPCとサブネット、セキュリティグループの作成
const vpc = new ec2.Vpc(this, 'VPC_NAME(※ここには任意の名前を入れてください)', {
// 今回はSonarQube用のEC2しか立てないので、cidrを28にしています(LBとか置きたい場合は変更してください)
// IPも今回は10.9.0.0を使っていますが、使われていたら変えてください
cidr: '10.9.0.0/28',
maxAzs: 1,
natGateways: 0,
subnetConfiguration: [
{
name: 'VPC_NAME(ここには任意の名前を入れてください)',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 28,
},
],
});
// セキュリティグループ作成(port9000許可)
const securityGroup = new ec2.SecurityGroup(this, 'SG_NAME(ここには任意の名前を入れてください)', {
vpc,
description: 'Allow Http port9000 access',
allowAllOutbound: true,
});
// SonarQubeはデフォルトポートが9000なのでport9000を開けています
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(9000), 'Allow HTTP access port 9000');
SonarQubeEC2・SSMポリシー付与IAMロール作成
// IAMロールの作成(SSMに必要なポリシーを付与)
const role = new iam.Role(this, 'SSMROLE_NAME()', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), // SSM用のポリシー
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'), // S3へのフルアクセス
],
});
// Amazon Linux 2023 AMI
const ami = ec2.MachineImage.latestAmazonLinux2023();
// EC2インスタンス作成
const ec2instance = new ec2.Instance(this, 'EC2_NAME(※ここには任意の名前を入れてください)', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
machineImage: ami,
securityGroup,
role: role,
});
// EC2インスタンス起動時にSonarQubeをDockerで起動するためのUserDataを追加
ec2instance.addUserData(
'sudo yum update -y',
'sudo yum install -y docker',
'sudo systemctl start docker',
'sudo systemctl enable docker',
'sudo curl -L "https://github.com/docker/compose/releases/download/v2.34.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose',
'sudo chmod +x /usr/local/bin/docker-compose',
`echo "version: '3'
services:
sonarqube:
image: sonarqube:lts
container_name: sonarqube
environment:
- SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/*****
- SONARQUBE_JDBC_USERNAME=*********
- SONARQUBE_JDBC_PASSWORD=*********
ports:
- '9000:9000'
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
networks:
- sonarnetwork
db:
image: postgres:13
container_name: sonar_postgres
environment:
- POSTGRES_USER=******
- POSTGRES_PASSWORD=**********
- POSTGRES_DB=********
volumes:
- sonar_postgres_data:/var/lib/postgresql/data
networks:
- sonarnetwork
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonar_postgres_data:
networks:
sonarnetwork:" > /home/ec2-user/docker-compose.yml`,
'sudo docker-compose -f /home/ec2-user/docker-compose.yml up -d',
)
SonarQubeの設定をコードで管理するためにシェルスクリプトを作成(APIを使った設定)
-
管理者パスワード変更
セキュリティ向上のため、初期パスワードから変更。# [初回のみ]password変更 // SonarQubeのあるEC2パブリックURLを指定 curl -X POST "$EC2_SONARQUBE_URL/api/users/change_password" \ -u "admin:admin" \ -d "login=admin&previousPassword=admin&password=$SONAR_PASSWORD"
-
プロジェクト作成
解析対象プロジェクトを自動作成。# プロジェクト作成 curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/projects/create" \ -d "name=$PROJECT_NAME&project=$PROJECT_KEY"
-
Quality Gate設定
解析結果の合否基準(Quality Gate)も設定し、自動判定できるようにしました。
重要度がBlocker,Critical,Majorのものが出た時に検知するように設定# Quality Gate 作成 curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create" \ -d "name=$QUALITY_GATE_NAME" # 条件追加 curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create_condition" \ -d "gateName=$QUALITY_GATE_NAME&metric=blocker_violations&op=GT&error=0&type=INT" curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create_condition" \ -d "gateName=$QUALITY_GATE_NAME&metric=critical_violations&op=GT&error=0&type=INT" curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create_condition" \ -d "gateName=$QUALITY_GATE_NAME&metric=major_violations&op=GT&error=0&type=INT" # 作成した Quality Gate をデフォルトに設定 curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/set_as_default" \ -d "name=$QUALITY_GATE_NAME"
パイプライン構成
パイプラインは次のような流れで構成しました。
-
EventBridgeで時間起動
定期的な実行(毎日8:30)に対応// EventBridge による日次スケジュール(JST 8:30実行) const eventbridge = new events.Rule(this, 'EVENTBRIDGE_NAME(※ここには任意の名前を入れてください)', { schedule: events.Schedule.cron({ minute: '30', hour: '23', weekDay: 'MON-FRI' }), // UTC targets: [new targets.CodePipeline(pipline)], });
-
CodeCommitからコード取得
AWS CodeCommitから最新ソースを取得// CodeCommit リポジトリ(既存のものを参照) const repo = codecommit.Repository.fromRepositoryName(this, 'Repo', process.env.REPO_NAME!); // CodePipeline のアーティファクト const sourceOutput = new codepipeline.Artifact();
-
CodeBuildでスキャン実行
SonarQube解析用のジョブを実行// CodeBuild プロジェクト(SonarQube スキャンを実行) const project = new codebuild.PipelineProject(this, 'PROJECT_NAME(※ここには任意の名前を入れてください)', { buildSpec: codebuild.BuildSpec.fromObject({ version: '0.2', phases: { install: { 'runtime-versions': { java: 'corretto11', }, commands: [ 'export SONAR_SCANNER_VERSION=4.7.0.2747', 'export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux', 'curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip', 'unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/', 'export PATH=$SONAR_SCANNER_HOME/bin:$PATH', 'export SONAR_SCANNER_OPTS="-server"', ], }, build: { commands: [ '# SonarQube スキャンの実行', 'export PATH=$PATH:$(pwd)/sonar-scanner/bin', `sonar-scanner -Dsonar.projectKey=${process.env.PROJECT_KEY} -Dsonar.projectName=${process.env.PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${process.env.EC2_SONARQUBE_URL} -Dsonar.login=$SONAR_TOKEN -Dsonar.exclusions=${process.env.EXCLUSION_PATH} -Dsonar.inclusions=${process.env.INCLUSION_PATH} -Dsonar.log.=DEBUG -Dsonar.verbose=true`, ], }, }, }), environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, environmentVariables: { SONAR_TOKEN: { value: process.env.SONAR_TOKEN, }, SNS_TOPIC_ARN: { value: topic.topicArn }, }, }, });
-
Pipeline作成
// CodePipeline 定義 const pipline = new codepipeline.Pipeline(this, 'PIPELINE_NAME(※ここには任意の名前を入れてください)', { stages: [ { stageName: 'Source', actions: [ new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit', repository: repo, branch: process.env.BRANCH_NAME!, output: sourceOutput, }), ], }, { stageName: 'BuildAndScan', actions: [ new codepipeline_actions.CodeBuildAction({ actionName: 'SonarScan', project, input: sourceOutput, }), ], }, ], });
-
SNSで結果送信
スキャン結果(合否や問題件数)をSNS経由で通知- SNSトピック作成
// SNS トピックの作成 const topic = new sns.Topic(this, 'ALERTTOPIC_NAME(※ここには任意の名前を入れてください)', { displayName: 'SonarQube Quality Gate Alerts', }); // SNS トピックにメール通知を追加 topic.addSubscription( new sns_subs.EmailSubscription(process.env.EMAIL_ADDRESS!) ); // SNSトピックに対する publish 権限をCodeBuildに付与 topic.grantPublish(project.role!);
- CodeBuildでSNSを起動するコマンドを追記
build: { commands: [ '# SonarQube スキャンの実行', 'export PATH=$PATH:$(pwd)/sonar-scanner/bin', `sonar-scanner -Dsonar.projectKey=${process.env.PROJECT_KEY} -Dsonar.projectName=${process.env.PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${process.env.EC2_SONARQUBE_URL} -Dsonar.login=$SONAR_TOKEN -Dsonar.exclusions=${process.env.EXCLUSION_PATH} -Dsonar.inclusions=${process.env.INCLUSION_PATH} -Dsonar.log.=DEBUG -Dsonar.verbose=true`, '# Quality Gate 結果の取得', `RESPONSE=$(curl -s -u "$SONAR_TOKEN:" "${process.env.EC2_SONARQUBE_URL}/api/qualitygates/project_status?projectKey=${process.env.PROJECT_KEY}")`, 'STATUS=$(echo "$RESPONSE" | jq -r \'.projectStatus.status\')', `SUMMARY=$(echo "$RESPONSE" | jq -r ' .projectStatus.conditions[] | "- \\( if .status == "OK" then "✅[OK]" else "❌[NG]" end ) \\( if .metricKey == "new_coverage" then "カバレッジ" elif .metricKey == "new_duplicated_lines_density" then "重複行(%)" elif .metricKey == "new_maintainability_rating" then "保守性" elif .metricKey == "new_reliability_rating" then "信頼性" elif .metricKey == "new_security_rating" then "セキュリティ" elif .metricKey == "new_security_hotspots_reviewed" then "セキュリティホットスポットのレビュー率" elif .metricKey == "blocker_violations" then "ブロッカー違反" elif .metricKey == "critical_violations" then "クリティカル違反" elif .metricKey == "major_violations" then "メジャー違反" else .metricKey end ): 値=\\(.actualValue)"' | paste -sd '\\n' - )`, // メール本文作成 `FAILED_MESSAGE="Project $PROJECT_KEY failed the Quality Gate.\nViolations Summary:\n$SUMMARY\nSonarQube URL:\n${process.env.EC2_SONARQUBE_URL}/dashboard?id=${process.env.PROJECT_KEY}"`, 'echo "Quality Gate Status: $STATUS"', '# ステータスが ERROR の場合、SNS に通知', 'if [ "$STATUS" = "ERROR" ]; then echo "Sending alert to SNS..."; aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[❌FAILED] SonarQube Quality Gate Failed" --message "$FAILED_MESSAGE"; else aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[✅SUCCEED] SonarQube Quality Gate succeed" --message "SonarQube Quality Gate succeed"; fi', ], },
- SNSトピック作成
- AWS CDKコード全文(EC2)
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as dotenv from 'dotenv';
dotenv.config();
export class InsurtechLabssSonarqube extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC作成(パブリックサブネット付き)
const vpc = new ec2.Vpc(this, 'VPC_NAME(※ここには任意の名前を入れてください)', {
cidr: '10.9.0.0/28',
maxAzs: 1,
natGateways: 0,
subnetConfiguration: [
{
name: 'insurtech-labss-sonarqube-subnet-public',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 28,
},
],
});
// セキュリティグループ作成(port9000許可)
const securityGroup = new ec2.SecurityGroup(this, 'SG_NAME(※ここには任意の名前を入れてください)', {
vpc,
description: 'Allow Http port9000 access',
allowAllOutbound: true,
});
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(9000), 'Allow HTTP access port 9000');
// IAMロールの作成(SSMに必要なポリシーを付与)
const role = new iam.Role(this, 'SSMROLE_NAME(※ここには任意の名前を入れてください)', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), // SSM用のポリシー
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'), // S3へのフルアクセス
],
});
// Amazon Linux 2023 AMI
const ami = ec2.MachineImage.latestAmazonLinux2023();
// EC2インスタンス作成
const ec2instance = new ec2.Instance(this, 'EC2INSTANCE_NAME(※ここには任意の名前を入れてください)', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
machineImage: ami,
securityGroup,
role: role,
});
// EC2インスタンス起動時にSonarQubeをDockerで起動するためのUserDataを追加
ec2instance.addUserData(
'sudo yum update -y',
'sudo yum install -y docker',
'sudo systemctl start docker',
'sudo systemctl enable docker',
'sudo curl -L "https://github.com/docker/compose/releases/download/v2.34.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose',
'sudo chmod +x /usr/local/bin/docker-compose',
`echo "version: '3'
services:
sonarqube:
image: sonarqube:lts
container_name: sonarqube
environment:
- SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/******
- SONARQUBE_JDBC_USERNAME=*********
- SONARQUBE_JDBC_PASSWORD=*********
ports:
- '9000:9000'
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
networks:
- sonarnetwork
db:
image: postgres:13
container_name: sonar_postgres
environment:
- POSTGRES_USER=*********
- POSTGRES_PASSWORD=*********
- POSTGRES_DB=********
volumes:
- sonar_postgres_data:/var/lib/postgresql/data
networks:
- sonarnetwork
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonar_postgres_data:
networks:
sonarnetwork:" > /home/ec2-user/docker-compose.yml`,
'sudo docker-compose -f /home/ec2-user/docker-compose.yml up -d',
)
}
}
- AWS CDKコード全文(Pipeline)
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as sns_subs from 'aws-cdk-lib/aws-sns-subscriptions';
import * as dotenv from 'dotenv';
dotenv.config();
export class SonarqubePipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// CodeCommit リポジトリ(既存のものを参照)
const repo = codecommit.Repository.fromRepositoryName(this, 'Repo', process.env.REPO_NAME!);
// SNS トピックの作成
const topic = new sns.Topic(this, 'ALERTTOPIC_NAME(※ここには任意の名前を入れてください)', {
displayName: 'SonarQube Quality Gate Alerts',
});
// CodePipeline のアーティファクト
const sourceOutput = new codepipeline.Artifact();
// CodeBuild プロジェクト(SonarQube スキャンを実行)
const project = new codebuild.PipelineProject(this, 'PROJECT_NAME(※ここには任意の名前を入れてください)', {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
install: {
'runtime-versions': {
java: 'corretto11',
},
commands: [
'export SONAR_SCANNER_VERSION=4.7.0.2747',
'export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux',
'curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip',
'unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/',
'export PATH=$SONAR_SCANNER_HOME/bin:$PATH',
'export SONAR_SCANNER_OPTS="-server"',
],
},
build: {
commands: [
'# SonarQube スキャンの実行',
'export PATH=$PATH:$(pwd)/sonar-scanner/bin',
`sonar-scanner -Dsonar.projectKey=${process.env.PROJECT_KEY} -Dsonar.projectName=${process.env.PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${process.env.EC2_SONARQUBE_URL} -Dsonar.login=$SONAR_TOKEN -Dsonar.exclusions=${process.env.EXCLUSION_PATH} -Dsonar.inclusions=${process.env.INCLUSION_PATH} -Dsonar.log.=DEBUG -Dsonar.verbose=true`,
'# Quality Gate 結果の取得',
`RESPONSE=$(curl -s -u "$SONAR_TOKEN:" "${process.env.EC2_SONARQUBE_URL}/api/qualitygates/project_status?projectKey=${process.env.PROJECT_KEY}")`,
'STATUS=$(echo "$RESPONSE" | jq -r \'.projectStatus.status\')',
`SUMMARY=$(echo "$RESPONSE" | jq -r '
.projectStatus.conditions[] |
"- \\(
if .status == "OK" then "✅[OK]" else "❌[NG]" end
) \\(
if .metricKey == "new_coverage" then "カバレッジ"
elif .metricKey == "new_duplicated_lines_density" then "重複行(%)"
elif .metricKey == "new_maintainability_rating" then "保守性"
elif .metricKey == "new_reliability_rating" then "信頼性"
elif .metricKey == "new_security_rating" then "セキュリティ"
elif .metricKey == "new_security_hotspots_reviewed" then "セキュリティホットスポットのレビュー率"
elif .metricKey == "blocker_violations" then "ブロッカー違反"
elif .metricKey == "critical_violations" then "クリティカル違反"
elif .metricKey == "major_violations" then "メジャー違反"
else .metricKey
end
): 値=\\(.actualValue)"' | paste -sd '\\n' -
)`,
// メール本文作成
`FAILED_MESSAGE="Project $PROJECT_KEY failed the Quality Gate.\nViolations Summary:\n$SUMMARY\nSonarQube URL:\n${process.env.EC2_SONARQUBE_URL}/dashboard?id=${process.env.PROJECT_KEY}"`,
'echo "Quality Gate Status: $STATUS"',
'# ステータスが ERROR の場合、SNS に通知',
'if [ "$STATUS" = "ERROR" ]; then echo "Sending alert to SNS..."; aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[❌FAILED] SonarQube Quality Gate Failed" --message "$FAILED_MESSAGE"; else aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[✅SUCCEED] SonarQube Quality Gate succeed" --message "SonarQube Quality Gate succeed"; fi',
],
},
},
}),
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
environmentVariables: {
SONAR_TOKEN: {
value: process.env.SONAR_TOKEN,
},
SNS_TOPIC_ARN: { value: topic.topicArn },
},
},
});
// CodePipeline 定義
const pipline = new codepipeline.Pipeline(this, 'PIPELINE_NAME(※ここには任意の名前を入れてください)', {
stages: [
{
stageName: 'Source',
actions: [
new codepipeline_actions.CodeCommitSourceAction({
actionName: 'CodeCommit',
repository: repo,
branch: process.env.BRANCH_NAME!,
output: sourceOutput,
}),
],
},
{
stageName: 'BuildAndScan',
actions: [
new codepipeline_actions.CodeBuildAction({
actionName: 'SonarScan',
project,
input: sourceOutput,
}),
],
},
],
});
// EventBridge による日次スケジュール(JST 8:30実行)
const eventbridge = new events.Rule(this, 'EVENTBRIDGE_NAME(※ここには任意の名前を入れてください)', {
schedule: events.Schedule.cron({ minute: '30', hour: '23', weekDay: 'MON-FRI' }), // UTC
targets: [new targets.CodePipeline(pipline)],
});
// SNS トピックにメール通知を追加
topic.addSubscription(
new sns_subs.EmailSubscription(process.env.EMAIL_ADDRESS!)
);
// SNSトピックに対する publish 権限をCodeBuildに付与
topic.grantPublish(project.role!);
// 生成リソースにタグを付与
cdk.Tags.of(project).add('Dept', 'InsurTech');
cdk.Tags.of(project).add('SystemName', process.env.SYSTEM_NAME!);
cdk.Tags.of(pipline).add('Dept', 'InsurTech');
cdk.Tags.of(pipline).add('SystemName', process.env.SYSTEM_NAME!);
cdk.Tags.of(eventbridge).add('Dept', 'InsurTech');
cdk.Tags.of(eventbridge).add('SystemName', process.env.SYSTEM_NAME!);
cdk.Tags.of(topic).add('Dept', 'InsurTech');
cdk.Tags.of(topic).add('SystemName', process.env.SYSTEM_NAME!);
}
}
結果
-
毎回確実にチェックできる仕組みが完成!
定期実行により、自動でコード品質を確認できるようになりました。 -
Slack通知による確認の簡略化
AWS環境やSonarQubeにアクセスせずにざっくりとしたエラーがどうかを確認できるようになりました。
まとめ
今回の取り組みにより、パイプライン上での静的解析が自動化され、セキュリティリスクを早期発見できるようになりました。
特に、定期実行に対応したことで、継続的にコード品質を保つ体制が整いました。
次回は、さらに範囲を広げてDASTについても取り組み、実行結果の共有方法まで深堀りしていきたいと思います。