Criação de uma pipeline segura com o Jenkins
Olá pessoal, tudo bem?
Resolvi escrever esse texto, pois muitas pessoas têm dúvida de como criar uma pipeline do zero. Eu já escrevi um artigo assim, utilizando o GitLab e o Digital Ocean, conforme link abaixo. No entanto, hoje eu resolvi trazer um projeto 100% open source e que você pode criar esse mesmo laboratório na sua própria máquina, sem nuvem.
Eu utilizei a mesma aplicação vulnerável, mas criei um outro repositório, pois eu queria deixar a pipeline um pouco mais realística e com isso, adicionei um teste automatizado de qualidade (BDD- Behaviour Driven Development) na aplicação também. O projeto, vocês podem encontrá-lo aqui abaixo. Apenas como curiosidade, utilizei o Cypress + Cucumber para essa etapa.
Agora, iremos para os testes de segurança:
- SCA (Software Composition Analysis) — Ele serve para analisar as bibliotecas utilizadas no projeto para ver se há alguma dependência vulnerável. Isso se deve ao fato que as bibliotecas podem ser uma porta de entrada para explorar uma aplicação.
Na imagem abaixo, utilizei uma ferramenta da OWASP conhecida como Dependency Check para realizar essa análise.
Caso queiram ler mais sobre esses dois itens, deixo aqui 2 outros artigos que escrevi. Um artigo sobre Spring4shell e outro, explicando como integrar o Jenkins ao Dependency Check.
- SAST (Static Application Security Testing) — Ele serve para analisar o código da aplicação. Nesse caso, existem ferramentas com regras internas que detectam de forma bem mais assertivas e para diversas linguagens. No entanto, essas ferramentas são pagas. Nesse nosso caso, eu utilizei o Bandit que é uma ferramenta open source que analisa a segurança apenas para código desenvolvido na linguagem Python.
Nesse ponto, o que eu quero destacar é que nas ferramentas de CI/CD você pode criar seu próprio Security Gate para você determinar o que pode ou não passar pela pipeline. Assim, você pode criar uma trava, caso isso faça sentido para a empresa em que você trabalha, ou então, você pode apenas alertar sobre a existência da severidade de vulnerabilidades ou o tipo de vulnerabilidade encontrada.
Deixei o meu exemplo de step no Jenkins, mas que você modificar de acordo com a política de implementação de desenvolvimento seguro escolhido. Reparem na função de status code, você consegue definir se irá bloquear ou passar na pipeline por meio do resultado vindo do Bandit.
stage('SAST') {
steps { script {
sh 'cd .. && docker run --rm -v $(pwd):/data cytopia/bandit -r app.py -f txt -o bandit-output.txt'
sh 'cat bandit-output.txt'
def statusCode = sh(script: '''
medium=`cat bandit-output.txt | grep "Severity: Medium"| wc -l`
high=`cat bandit-output.txt | grep "Severity: High"| wc -l`
if [[ $medium -eq 0 || $high -eq 0 ]]; then
exit 2
else
exit 1
fi
''',returnStatus:true)
if (statusCode == 2) {
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
error 'STAGE UNSTABLE'
}
} else if(statusCode == 1) {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
error 'STAGE FAILURE'
}
}
}
}
}
- DAST (Dynamic Application Security Testing) — Ele serve para analisar a interface da aplicação. Nesse caso, eu utilizei outra ferramenta da OWASP chamada ZAP API. Essa ferramenta executará uma série de chamadas nos endpoints, utilizando diversos tipos de payload, que chamamos de teste Fuzzing.
# Fase de Deploy
Sei que muitas pessoas ficam com dificuldade de simular essa etapa. Primeiramente, iremos utilizar o SSH na máquina pessoal, onde simularemos a conexão com o servidor (que no caso, é a sua própria máquina).
No macos é necessário:
Então, criar uma chave pública e privada. A chave privada será adicionada ao Jenkins depois.
cd /root/.ssh/
ssh-keygen
ls
id_rsa id_rsa.pub
cat id_rsa // valor da chave privada que será usada no Jenkins
Após isso, na aba de credenciais, você irá adicionar a chave privada.
Por fim, adicione o plugin no Jenkins:
Pronto, concluímos nossa pipeline! 🚀
Imagem do Jenkins:
Jenkinsfile:
pipeline {
agent any
tools{
'org.jenkinsci.plugins.docker.commons.tools.DockerTool' 'docker'
nodejs "NodeJs"
}
stages {
stage('Git') {
steps {
git branch: 'main', url: 'https://github.com/michelleamesquita/python-bdd.git'
}
}
stage('Build') {
steps {
withCredentials(([string(credentialsId: 'CI_REGISTRY_PASSWORD', variable: 'PASSWORD')])){
sh 'docker build -t michelleamesquita/python-cypress:latest .'
sh 'docker login -u michelleamesquita -p $PASSWORD'
sh 'docker push michelleamesquita/python-cypress:latest'}
}
}
stage('BDD') {
steps {
sh 'docker run michelleamesquita/cypress-aula'
}
// steps {
// sh 'cd bdd && npm install && npm run test'
// }
}
stage('SCA') {
steps {
dependencyCheck additionalArguments: '--format HTML --format XML', odcInstallation: 'Dependency Check'
dependencyCheckPublisher pattern: '**/dependency-check-report.xml'
}
}
stage('SAST') {
steps { script {
sh 'cd .. && docker run --rm -v $(pwd):/data cytopia/bandit -r app.py -f txt -o bandit-output.txt'
sh 'cat bandit-output.txt'
def statusCode = sh(script: '''
medium=`cat bandit-output.txt | grep "Severity: Medium"| wc -l`
high=`cat bandit-output.txt | grep "Severity: High"| wc -l`
if [[ $medium -eq 0 || $high -eq 0 ]]; then
exit 2
else
exit 1
fi
''',returnStatus:true)
if (statusCode == 2) {
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
error 'STAGE UNSTABLE'
}
} else if(statusCode == 1) {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
error 'STAGE FAILURE'
}
}
}
}
}
stage('DAST') {
steps {
script {
def statusCode = sh(script: 'docker run -v $(pwd):/zap/wrk --user root -t owasp/zap2docker-stable zap-baseline.py -t http://192.168.0.14:8888 -r report.html',
returnStatus:true)
if (statusCode == 2 || statusCode == 1) {
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
error 'STAGE UNSTABLE'
}
}
}
}
}
stage('Deploy') {
steps {
sshagent(credentials:['Login_Server']){
sh 'ssh -o StrictHostKeyChecking=no mac@127.0.0.1 "/usr/local/bin/docker stop app0 && /usr/local/bin/docker run --rm -d --name app0 -p 8888:8888 michelleamesquita/python-cypress:latest"'
}}
}
stage('Telegram') {
steps {
withCredentials(([string(credentialsId: 'telegramChatId', variable: 'CHAT_ID')])) {
telegramSend(message: 'done 🚀', chatId: "$CHAT_ID")
}
}
}
}
}
## Para instalar o Telegram, basta adicionar o plugin e criar a credencial do tipo texto
Mais informações aqui:
https://gist.github.com/chliwei199/5fb74c789feb44d26e99368ff36feecc
Espero que tenham gostado 💜👩💻