Building an End-to-End CI/CD Pipeline for Node.js Applications - Part 3: Jenkins with Trivy Security Scanning

The third part of our Node.js CI/CD pipeline series, focusing on implementing advanced security scanning with Trivy in a Jenkins pipeline.

Building an End-to-End CI/CD Pipeline for Node.js Applications - Part 3: Jenkins with Trivy Security Scanning

Table of Contents

Introduction

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.

Code Repository

For this tutorial, we will be using the To-Do App repository: To-Do App GitHub Repository.

Prerequisites

Before starting this project, make sure you have the following prerequisites:

  1. Jenkins: Ensure Jenkins is installed and running on a server (as covered in Part 1).
  2. Docker: Install Docker on your server.
  3. Node.js: Ensure Node.js is installed.
  4. SonarQube: SonarQube server for code quality and security scanning.
  5. Trivy: Install Trivy for container vulnerability scanning.
  6. OWASP Dependency Check: For scanning application dependencies.
  7. EC2 Instance: AWS EC2 instance of type t2.large for sufficient resources.

Setting Up Jenkins

If you haven’t already set up Jenkins from Part 1, follow these steps:

  1. SSH into the EC2 instance and install Java:
sudo apt update -y
sudo apt install openjdk-17-jdk -y
java -version
  1. Download and install Jenkins:
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.

Installing Required Jenkins Plugins

For our enhanced pipeline, install the following plugins:

  1. NodeJS Plugin: For Node.js build environment
  2. SonarQube Scanner: For code quality and security analysis
  3. Docker Pipeline: For Docker integration
  4. OWASP Dependency-Check: For vulnerability scanning of dependencies

To install these plugins:

  1. Go to Manage Jenkins → Manage Plugins → Available
  2. Search for and select each plugin
  3. Click “Install without restart”
  4. Restart Jenkins after all plugins are installed

Configuring Jenkins Tools

Configure the required tools in Jenkins:

  1. Go to Manage Jenkins → Global Tool Configuration

  2. Node.js:

    • Add NodeJS → Name it as “node18” → Install automatically → Select version 18.x
  3. SonarQube Scanner:

    • Add SonarQube Scanner → Name it as “sonarqube scanner” → Install automatically
  4. OWASP Dependency-Check:

    • Add Dependency-Check → Name it as “DP” → Install automatically

Setting Up Trivy

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

Setting Up SonarQube

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:

  1. Log in to SonarQube
  2. Go to Administration → Security → Users
  3. Click on the update token option for the admin user
  4. Generate a token named “jenkins”
  5. Save the token for Jenkins configuration

Add the token to Jenkins:

  1. Go to Manage Jenkins → Manage Credentials → Global
  2. Add Credentials
  3. Choose “Secret text” as the kind
  4. In the Secret field, paste your SonarQube token
  5. Set ID as “sonar” and description as “sonar”
  6. Click OK

Configure SonarQube in Jenkins:

  1. Go to Manage Jenkins → Configure System
  2. Find the SonarQube servers section
  3. Add a SonarQube installation:
    • Name: “SonarQube”
    • Server URL: http://:9000
    • Server authentication token: Select your sonar credential
  4. Save the configuration

Creating an Advanced Jenkins Pipeline

Now, let’s create an enhanced pipeline with Trivy scanning:

  1. Go to Dashboard → New Item → Enter “nodejs-advanced-pipeline” as the name → Select Pipeline → OK
  2. Under “General”, check “Discard old builds” and set “Max # of builds to keep” to 2
  3. Under “Pipeline”, select “Pipeline script” and use the following script:
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!'
        }
    }
}

Adding Docker Hub Credentials

Before running the pipeline, add Docker Hub credentials to Jenkins:

  1. Go to Manage Jenkins → Manage Credentials → Global
  2. Click on Add Credentials
  3. Choose Username with password
  4. Enter your Docker Hub username and password
  5. Set ID as “docker-hub-credentials” and description as “Docker Hub”
  6. Click OK

Understanding the Enhanced Pipeline

Let’s break down the key components of our enhanced pipeline:

1. Git Checkout

Fetches the code from the GitHub repository.

2. SonarQube Analysis

Performs static code analysis to identify code quality issues and potential security vulnerabilities.

3. Dependency Installation

Installs all required npm dependencies.

4. OWASP Dependency Check

Scans all dependencies for known vulnerabilities using the OWASP Dependency Check tool.

5. Docker Image Build

Builds a Docker image for the application.

6. Trivy Scan

This is the key enhancement in this pipeline. Trivy scans the built Docker image for vulnerabilities and:

  • Focuses on HIGH and CRITICAL severity vulnerabilities
  • Outputs results in JSON format for processing
  • Can conditionally fail the build if too many serious vulnerabilities are found

7. Docker Image Push

Pushes the Docker image to Docker Hub.

8. Container Deployment

Deploys the application as a Docker container.

9. Post Actions

Archives the scan results and cleans up the workspace after the pipeline completes.

Understanding Trivy Scan Results

Trivy provides detailed vulnerability information, including:

  1. CVE IDs: Unique identifiers for vulnerabilities
  2. Severity: Rating of the vulnerability impact (CRITICAL, HIGH, MEDIUM, LOW)
  3. Package: The vulnerable package
  4. Installed Version: The version containing the vulnerability
  5. Fixed Version: The version that fixes the vulnerability
  6. Title/Description: Details about the vulnerability

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"
          ]
        }
      ]
    }
  ]
}

Strategies for Handling Vulnerabilities

There are several approaches to handling vulnerabilities found by Trivy:

1. Threshold-Based

As shown in our pipeline, set a threshold for the maximum number of HIGH or CRITICAL vulnerabilities allowed.

2. Zero Critical Vulnerability Policy

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"
    }
}

3. Vulnerability Age Consideration

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"
    }
}

4. Exclusion Lists

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"
    }
}

Extending the Pipeline with Additional Security Tools

Beyond Trivy, consider adding these security tools to your pipeline:

1. ESLint for Security Rules

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"]
}

2. npm audit

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"
            }
        }
    }
}

3. Integration with Security Information and Event Management (SIEM) System

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
            """
        }
    }
}

Visualization and Reporting

To improve visibility of security issues:

1. Integrate with Jenkins Warnings Plugin

stage('Process Trivy Results') {
    steps {
        recordIssues enabledForFailure: true, tool: trivy(pattern: 'trivy-results.json')
    }
}

2. Generate HTML Reports

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'
            ])
        }
    }
}

Remediation Workflows

When vulnerabilities are found, implement a remediation workflow:

  1. Automated PRs for Dependency Updates:

    • Use tools like Dependabot to automatically create PRs for updating vulnerable dependencies
  2. Integration with Issue Tracking Systems:

    • Create Jira tickets for vulnerabilities that need manual remediation
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/
                        """
                    }
                }
            }
        }
    }
}

Continuous Security Monitoring

Beyond CI/CD pipeline integration, implement continuous security monitoring:

  1. Scheduled Scans:

    • Configure Jenkins to run security scans on a regular schedule, even if there are no code changes
  2. Runtime Security Monitoring:

    • Implement runtime application security monitoring with tools like Falco or Aqua Security

Conclusion

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:

  1. Identify vulnerabilities early in the development lifecycle
  2. Prevent deploying vulnerable applications to production
  3. Maintain a systematic approach to security in our DevOps practices
  4. Create a tangible security posture measurement through trend analysis

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.

Table of Contents