AB
The third part of our Node.js CI/CD pipeline series, focusing on implementing advanced security scanning with Trivy in a Jenkins pipeline.
In this final part of our Node.js CI/CD pipeline series, we will enhance our Jenkins pipeline from Part 1 by incorporating Trivy, a comprehensive vulnerability scanner for containers and application dependencies. By integrating security scanning into our CI/CD pipeline, we ensure that security issues are detected early in the development lifecycle, significantly reducing the risk of deploying vulnerable applications to production.
For this tutorial, we will be using the To-Do App repository: To-Do App GitHub Repository.
Before starting this project, make sure you have the following prerequisites:
If you haven’t already set up Jenkins from Part 1, follow these steps:
sudo apt update -y
sudo apt install openjdk-17-jdk -y
java -version
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update
sudo apt-get install jenkins
Don’t forget to open port 8080 in your security group to access Jenkins.
For our enhanced pipeline, install the following plugins:
To install these plugins:
Configure the required tools in Jenkins:
Go to Manage Jenkins → Global Tool Configuration
Node.js:
SonarQube Scanner:
OWASP Dependency-Check:
Trivy is a simple yet comprehensive vulnerability scanner for containers. Install it on your Jenkins server:
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
Verify the installation:
trivy --version
If you haven’t already set up SonarQube from Part 1, run it using Docker:
docker run -d --name sonarqube -p 9000:9000 sonarqube:lts-community
Access SonarQube at http://<your-ec2-ip>:9000
with the default login credentials (admin/admin).
Generate a token for Jenkins integration:
Add the token to Jenkins:
Configure SonarQube in Jenkins:
Now, let’s create an enhanced pipeline with Trivy scanning:
pipeline {
agent any
tools {
nodejs 'node18'
}
environment {
SONARQUBE_URL = tool name: 'sonarqube scanner'
}
stages {
stage('Git Checkout') {
steps {
git url: 'https://github.com/jaiswaladi246/to-do-app', branch: 'main'
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh '''
$SONARQUBE_URL/bin/sonar-scanner \
-Dsonar.projectKey=to-do-app \
-Dsonar.qualitygate.wait=true \
-Dsonar.sources=. \
-Dsonar.projectName=to-do-app \
-Dsonar.projectVersion=1.0
'''
}
}
}
stage('Install Dependencies') {
steps {
sh 'npm install'
}
}
stage('OWASP Dependency Check') {
steps {
dependencyCheck additionalArguments: '--scan ./', odcInstallation: 'DP'
dependencyCheckPublisher pattern: '**/dependency-check-report.xml'
}
}
stage('Build Docker Image') {
steps {
script {
withDockerRegistry([credentialsId: 'docker-hub-credentials', toolName: 'docker']) {
sh 'docker build -t jaiswaladi246/to-do-app:latest .'
}
}
}
}
stage('Trivy Scan') {
steps {
sh 'trivy image --severity HIGH,CRITICAL --format json -o trivy-results.json jaiswaladi246/to-do-app:latest'
sh 'cat trivy-results.json'
// Optional: Add threshold for failing the build based on vulnerabilities
script {
def trivyResults = readJSON file: 'trivy-results.json'
def vulnerabilitiesCount = trivyResults.Results.sum { result ->
result.Vulnerabilities ? result.Vulnerabilities.size() : 0
}
echo "Found ${vulnerabilitiesCount} HIGH or CRITICAL vulnerabilities"
// Fail the build if more than 5 high or critical vulnerabilities are found
if (vulnerabilitiesCount > 5) {
error "Build failed due to too many HIGH or CRITICAL vulnerabilities: ${vulnerabilitiesCount}"
}
}
}
}
stage('Push Docker Image') {
steps {
script {
withDockerRegistry([credentialsId: 'docker-hub-credentials', toolName: 'docker']) {
sh 'docker push jaiswaladi246/to-do-app:latest'
}
}
}
}
stage('Deploy Container') {
steps {
script {
withDockerRegistry([credentialsId: 'docker-hub-credentials', toolName: 'docker']) {
sh 'docker stop to-do-app || true'
sh 'docker rm to-do-app || true'
sh 'docker run -d --name to-do-app -p 80:80 jaiswaladi246/to-do-app:latest'
}
}
}
}
}
post {
always {
// Archive the Trivy scan results
archiveArtifacts artifacts: 'trivy-results.json', fingerprint: true
// Clean up workspace
cleanWs()
}
success {
echo 'Pipeline completed successfully!'
}
failure {
echo 'Pipeline failed!'
}
}
}
Before running the pipeline, add Docker Hub credentials to Jenkins:
Let’s break down the key components of our enhanced pipeline:
Fetches the code from the GitHub repository.
Performs static code analysis to identify code quality issues and potential security vulnerabilities.
Installs all required npm dependencies.
Scans all dependencies for known vulnerabilities using the OWASP Dependency Check tool.
Builds a Docker image for the application.
This is the key enhancement in this pipeline. Trivy scans the built Docker image for vulnerabilities and:
Pushes the Docker image to Docker Hub.
Deploys the application as a Docker container.
Archives the scan results and cleans up the workspace after the pipeline completes.
Trivy provides detailed vulnerability information, including:
Sample Trivy output:
{
"Results": [
{
"Target": "jaiswaladi246/to-do-app:latest (node)",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2021-43138",
"PkgName": "json-schema",
"InstalledVersion": "0.2.3",
"FixedVersion": "0.4.0",
"Severity": "HIGH",
"Description": "json-schema is vulnerable to Prototype Pollution...",
"References": [
"https://nvd.nist.gov/vuln/detail/CVE-2021-43138",
"https://github.com/advisories/GHSA-896r-f27r-55mw"
]
}
]
}
]
}
There are several approaches to handling vulnerabilities found by Trivy:
As shown in our pipeline, set a threshold for the maximum number of HIGH or CRITICAL vulnerabilities allowed.
Fail the build if any CRITICAL vulnerabilities are found:
script {
def trivyResults = readJSON file: 'trivy-results.json'
def criticalVulnerabilities = trivyResults.Results.sum { result ->
result.Vulnerabilities ? result.Vulnerabilities.findAll { it.Severity == "CRITICAL" }.size() : 0
}
if (criticalVulnerabilities > 0) {
error "Build failed due to CRITICAL vulnerabilities found"
}
}
Allow vulnerabilities without fixes or those discovered very recently:
script {
def trivyResults = readJSON file: 'trivy-results.json'
def currentDate = new Date()
def recentVulnerabilities = trivyResults.Results.sum { result ->
result.Vulnerabilities ? result.Vulnerabilities.findAll { vuln ->
// Convert publication date to a Date object
def publishedDate = Date.parse("yyyy-MM-dd", vuln.PublishedDate.substring(0, 10))
// Calculate days since publication
def daysSincePublication = (currentDate.time - publishedDate.time) / (1000 * 60 * 60 * 24)
// Allow vulnerabilities less than 30 days old
return vuln.Severity == "CRITICAL" && daysSincePublication > 30
}.size() : 0
}
if (recentVulnerabilities > 0) {
error "Build failed due to non-recent CRITICAL vulnerabilities"
}
}
Maintain an exclusion list for vulnerabilities that can’t be immediately fixed:
script {
def trivyResults = readJSON file: 'trivy-results.json'
def excludedVulnerabilities = ["CVE-2021-43138", "CVE-2022-12345"]
def criticalVulnerabilities = trivyResults.Results.sum { result ->
result.Vulnerabilities ? result.Vulnerabilities.findAll { vuln ->
return vuln.Severity == "CRITICAL" && !excludedVulnerabilities.contains(vuln.VulnerabilityID)
}.size() : 0
}
if (criticalVulnerabilities > 0) {
error "Build failed due to non-excluded CRITICAL vulnerabilities"
}
}
Beyond Trivy, consider adding these security tools to your pipeline:
Add a step for ESLint with security plugins:
stage('ESLint Security Scan') {
steps {
sh 'npm install [email protected]'
sh 'npm install eslint-plugin-security'
sh 'npx eslint . --config .eslintrc-security.json'
}
}
Create a .eslintrc-security.json
file:
{
"plugins": ["security"],
"extends": ["plugin:security/recommended"]
}
Integrate npm’s built-in security auditing:
stage('npm Audit') {
steps {
sh 'npm audit --json > npm-audit.json || true'
script {
def auditReport = readJSON file: 'npm-audit.json'
if (auditReport.metadata.vulnerabilities.high > 0 || auditReport.metadata.vulnerabilities.critical > 0) {
echo "WARNING: npm audit found ${auditReport.metadata.vulnerabilities.high} high and ${auditReport.metadata.vulnerabilities.critical} critical vulnerabilities"
}
}
}
}
Send security results to a SIEM system:
stage('Report to SIEM') {
steps {
script {
def trivyResults = readJSON file: 'trivy-results.json'
def vulnerabilitiesCount = trivyResults.Results.sum { result ->
result.Vulnerabilities ? result.Vulnerabilities.size() : 0
}
// Example: Send to a webhook
sh """
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"build": "${env.BUILD_NUMBER}", "vulnerabilities": ${vulnerabilitiesCount}}' \
https://your-siem-webhook-url
"""
}
}
}
To improve visibility of security issues:
stage('Process Trivy Results') {
steps {
recordIssues enabledForFailure: true, tool: trivy(pattern: 'trivy-results.json')
}
}
stage('Generate Security Report') {
steps {
script {
def trivyResults = readJSON file: 'trivy-results.json'
// Create HTML report
def htmlReport = """
<html>
<head>
<title>Security Scan Report</title>
<style>
body { font-family: Arial, sans-serif; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
th { background-color: #f2f2f2; }
.critical { background-color: #ffcccc; }
.high { background-color: #ffffcc; }
</style>
</head>
<body>
<h1>Security Scan Report</h1>
<h2>Trivy Scan Results</h2>
<table>
<tr>
<th>Vulnerability ID</th>
<th>Package</th>
<th>Installed Version</th>
<th>Fixed Version</th>
<th>Severity</th>
</tr>
"""
trivyResults.Results.each { result ->
if (result.Vulnerabilities) {
result.Vulnerabilities.each { vuln ->
def rowClass = vuln.Severity == "CRITICAL" ? "critical" : vuln.Severity == "HIGH" ? "high" : ""
htmlReport += """
<tr class="${rowClass}">
<td>${vuln.VulnerabilityID}</td>
<td>${vuln.PkgName}</td>
<td>${vuln.InstalledVersion}</td>
<td>${vuln.FixedVersion ?: 'Not Available'}</td>
<td>${vuln.Severity}</td>
</tr>
"""
}
}
}
htmlReport += """
</table>
</body>
</html>
"""
writeFile file: 'security-report.html', text: htmlReport
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'security-report.html',
reportName: 'Security Scan Report'
])
}
}
}
When vulnerabilities are found, implement a remediation workflow:
Automated PRs for Dependency Updates:
Integration with Issue Tracking Systems:
stage('Create Remediation Tickets') {
steps {
script {
def trivyResults = readJSON file: 'trivy-results.json'
trivyResults.Results.each { result ->
if (result.Vulnerabilities) {
result.Vulnerabilities.findAll { it.Severity == "CRITICAL" }.each { vuln ->
// Example: Create Jira ticket
sh """
curl -X POST \
-H 'Content-Type: application/json' \
-u '${JIRA_USERNAME}:${JIRA_API_TOKEN}' \
-d '{
"fields": {
"project": { "key": "SEC" },
"summary": "Security Vulnerability: ${vuln.VulnerabilityID}",
"description": "Package: ${vuln.PkgName}\\nSeverity: ${vuln.Severity}\\nDescription: ${vuln.Description}",
"issuetype": { "name": "Bug" },
"priority": { "name": "High" }
}
}' \
https://your-jira-instance/rest/api/2/issue/
"""
}
}
}
}
}
}
Beyond CI/CD pipeline integration, implement continuous security monitoring:
Scheduled Scans:
Runtime Security Monitoring:
In this final part of our Node.js CI/CD pipeline series, we’ve enhanced our Jenkins pipeline with comprehensive security scanning using Trivy. By integrating security into our CI/CD process, we can:
This advanced pipeline provides a solid foundation for building secure Node.js applications with automated security testing and deployment. By implementing the strategies and tools covered in this tutorial, you can significantly reduce the risk of security issues in your applications while maintaining development velocity.
Together with Parts 1 and 2 of this series, you now have a comprehensive understanding of different approaches to implementing CI/CD pipelines for Node.js applications, with options ranging from Jenkins to CircleCI, and from basic pipelines to security-enhanced advanced implementations.