08.02.2026

Automating Malware Scanning in CI/CD with VirusTotal

VirusTotal CI/CD Integration

Supply chain attacks are on the rise. Compromised dependencies, malicious build artifacts, and infected container images can slip into production undetected. Adding automated malware scanning to your CI/CD pipeline creates a security gate that catches threats before deployment.

This guide shows how to integrate VirusTotal into your build pipeline using the API and CLI.

Why Scan in CI/CD?

Traditional security scans happen post-deployment or during scheduled audits. By then, malicious code may already be running in production. CI/CD integration provides:

  • Shift-left security - Catch threats before they reach production
  • Automated enforcement - No manual steps to forget
  • Audit trail - Every build has a security scan record
  • Fast feedback - Developers know immediately if something's wrong

Prerequisites

  1. VirusTotal API key - Get one free at virustotal.com
  2. vt-cli installed - See our VirusTotal CLI guide for setup
  3. CI/CD platform - Examples for GitHub Actions, GitLab CI, and Jenkins

API Rate Limits:

Plan Requests/min Requests/day
Free 4 500
Premium 30 5,000+

For active CI/CD, you'll likely need a premium key.

GitHub Actions Integration

Basic File Scan

name: Security Scan

on:
  push:
    branches: [main]
  pull_request:

jobs:
  malware-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build artifact
        run: |
          npm ci
          npm run build
          tar -czf build.tar.gz dist/
      
      - name: Install VirusTotal CLI
        run: |
          curl -LO https://github.com/VirusTotal/vt-cli/releases/download/1.0.0/vt_1.0.0_linux_amd64.deb
          sudo dpkg -i vt_1.0.0_linux_amd64.deb
      
      - name: Scan build artifact
        env:
          VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
        run: |
          vt scan file build.tar.gz --wait
          RESULT=$(vt file "$(sha256sum build.tar.gz | cut -d' ' -f1)" --format json)
          
          MALICIOUS=$(echo "$RESULT" | jq '.data.attributes.last_analysis_stats.malicious')
          if [ "$MALICIOUS" -gt 0 ]; then
            echo "::error::Malware detected! $MALICIOUS engines flagged this file."
            exit 1
          fi
          echo "Scan passed: No malware detected"

Scan Multiple Files

- name: Scan all artifacts
  env:
    VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
  run: |
    for file in dist/*.js dist/*.css; do
      echo "Scanning $file..."
      HASH=$(sha256sum "$file" | cut -d' ' -f1)
      
      # Check if already scanned
      if ! vt file "$HASH" &>/dev/null; then
        vt scan file "$file" --wait
      fi
      
      MALICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.malicious')
      if [ "$MALICIOUS" -gt 0 ]; then
        echo "::error::Malware detected in $file"
        exit 1
      fi
    done

Scan Container Images

- name: Build and scan Docker image
  env:
    VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
  run: |
    docker build -t myapp:${{ github.sha }} .
    docker save myapp:${{ github.sha }} | gzip > image.tar.gz
    
    vt scan file image.tar.gz --wait
    HASH=$(sha256sum image.tar.gz | cut -d' ' -f1)
    
    MALICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.malicious')
    SUSPICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.suspicious')
    
    if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 2 ]; then
      echo "::error::Security issues detected in container image"
      exit 1
    fi

GitLab CI Integration

stages:
  - build
  - security
  - deploy

build:
  stage: build
  script:
    - npm ci && npm run build
    - tar -czf build.tar.gz dist/
  artifacts:
    paths:
      - build.tar.gz

malware_scan:
  stage: security
  image: ubuntu:latest
  before_script:
    - apt-get update && apt-get install -y curl jq
    - curl -LO https://github.com/VirusTotal/vt-cli/releases/download/1.0.0/vt_1.0.0_linux_amd64.deb
    - dpkg -i vt_1.0.0_linux_amd64.deb
  script:
    - export VT_API_KEY=$VIRUSTOTAL_API_KEY
    - vt scan file build.tar.gz --wait
    - |
      HASH=$(sha256sum build.tar.gz | cut -d' ' -f1)
      MALICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.malicious')
      if [ "$MALICIOUS" -gt 0 ]; then
        echo "Malware detected!"
        exit 1
      fi
  dependencies:
    - build

deploy:
  stage: deploy
  script:
    - ./deploy.sh
  only:
    - main
  needs:
    - malware_scan

Jenkins Pipeline

pipeline {
    agent any
    
    environment {
        VT_API_KEY = credentials('virustotal-api-key')
    }
    
    stages {
        stage('Build') {
            steps {
                sh 'npm ci && npm run build'
                sh 'tar -czf build.tar.gz dist/'
            }
        }
        
        stage('Security Scan') {
            steps {
                sh '''
                    curl -LO https://github.com/VirusTotal/vt-cli/releases/download/1.0.0/vt_1.0.0_linux_amd64.deb
                    dpkg -i vt_1.0.0_linux_amd64.deb || apt-get install -f -y
                    
                    vt scan file build.tar.gz --wait
                    
                    HASH=$(sha256sum build.tar.gz | cut -d' ' -f1)
                    MALICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.malicious')
                    
                    if [ "$MALICIOUS" -gt 0 ]; then
                        echo "MALWARE DETECTED"
                        exit 1
                    fi
                '''
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh './deploy.sh'
            }
        }
    }
    
    post {
        failure {
            slackSend channel: '#security-alerts',
                      message: "Security scan failed for ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

Scanning Dependencies

Scan downloaded packages before using them:

- name: Scan npm dependencies
  run: |
    # Create tarball of node_modules
    tar -czf dependencies.tar.gz node_modules/
    
    # Scan the bundle
    vt scan file dependencies.tar.gz --wait
    
    # Or scan individual suspicious packages
    for pkg in node_modules/.package-lock.json; do
      # Extract and scan packages with native bindings
      ...
    done

Best Practices

1. Cache Scan Results

Don't re-scan files you've already checked:

- name: Check cache before scanning
  run: |
    HASH=$(sha256sum build.tar.gz | cut -d' ' -f1)
    
    # Try to get existing report first
    if vt file "$HASH" --format json 2>/dev/null; then
      echo "Using cached scan result"
    else
      echo "Uploading for fresh scan"
      vt scan file build.tar.gz --wait
    fi

2. Set Appropriate Thresholds

Not every detection is malware. Set sensible thresholds:

MALICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.malicious')
SUSPICIOUS=$(vt file "$HASH" --format json | jq '.data.attributes.last_analysis_stats.suspicious')

# Fail on any malicious detection
if [ "$MALICIOUS" -gt 0 ]; then
  exit 1
fi

# Warn but don't fail on suspicious (could be false positive)
if [ "$SUSPICIOUS" -gt 3 ]; then
  echo "::warning::Multiple suspicious detections - review manually"
fi

3. Handle Rate Limits

scan_with_retry() {
  local file=$1
  local retries=3
  
  for i in $(seq 1 $retries); do
    if vt scan file "$file" --wait 2>&1; then
      return 0
    fi
    echo "Rate limited, waiting 60s... (attempt $i/$retries)"
    sleep 60
  done
  return 1
}

4. Store Reports for Audit

- name: Save scan report
  run: |
    HASH=$(sha256sum build.tar.gz | cut -d' ' -f1)
    vt file "$HASH" --format json > scan-report.json
    
- name: Upload scan report
  uses: actions/upload-artifact@v4
  with:
    name: virustotal-report
    path: scan-report.json
    retention-days: 90

Limitations to Consider

Limitation Workaround
File size limit (650MB free) Split large files or use premium
Rate limits Cache results, batch scans
Scan time (up to 5 min) Run scans in parallel jobs
False positives Set thresholds, review manually
No real-time protection Combine with other security tools

Conclusion

Adding VirusTotal to your CI/CD pipeline creates an automated security gate that catches malware before it reaches production. Combined with dependency scanning and container image verification, you build defense in depth against supply chain attacks.

Start with scanning your final build artifacts, then expand to dependencies and container images as your security posture matures.


Want to automate security incident response when threats are detected? Akmatori helps SRE teams build AI-powered runbooks that can automatically respond to security alerts and coordinate remediation.

Automate incident response and prevent on-call burnout with AI-driven agents!