Skip to main content
Intermediate

Keep CVE Data Current in Air-Gapped Forward Enterprise Instances

  • April 16, 2025
  • 1 reply
  • 29 views

captainpacket
Employee

 

For Forward Enterprise on-prem customers running air-gapped instances, maintaining up-to-date security data can offer challenges. This Python script addresses those challenges by automating the daily update of the CVE (Common Vulnerabilities and Exposures) database, ensuring your instance reflects the latest threat intelligence—even when it’s offline. You can perform these updates in the Forward Enterprise UI, but this script offers an automated solution to ensure you’re running the most up-to-date definitions. In addition to CVEs, you can modify this script to include custom definitions to identify threats and compliance requirements specific to your industry or sector. 

 

Jump to the script

 

Designed for Security and Compliance Teams

 

This script is particularly valuable for:

  • Security Operations Center (SOC) teams monitoring for zero-day vulnerabilities
     
  • Audit and compliance professionals needing accurate, up-to-date security data
     
  • Any on-prem customer managing Forward Enterprise in a secure or regulated environment
     

By removing the need for daily manual updates, it saves time and reduces the risk of human error in high-stakes environments.

 

Automate Download to Upload

 

Here’s how the script works:

  • Connect to Forward’s web platform using your credentials (or ideally, an API key).
     
  • Downloads the latest CVE database as a compressed file.
     
  • Uploads the file to your on-prem instance, using its local credentials and endpoint information.
     
  • Runs on a schedule (via cron or task scheduler), so the update process is hands-off after setup.
     

Typically, this script runs on a bastion host—any machine that can access both the internet and the internal network. It could be your laptop or a dedicated server bridging the two environments

 

Expand Beyond CVEs with Minimal Effort

While the script’s primary use case is daily CVE updates, it’s flexible enough to support other workflows:

  • Upload custom or third-party compliance files
     
  • Sync internal datasets not available via API
     
  • Extend the logic to support multiple destination hosts
     

With only minor tweaks to the source URL and upload target, the same foundation can support a broader set of update tasks.

 

 

 

Running the script requires:

  • A modern Python environment
     
  • The requests library (a common dependency)
     
  • A Forward user account with download access to the CVE database
     
  • On-prem instance credentials (or support for insecure uploads if no certificate is installed)

Python Script

 

#!/usr/bin/env python3

import argparse
import logging
import os
import re
import sys
import requests
from requests.auth import HTTPBasicAuth
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def setup_logging(verbose):
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(levelname)s - %(message)s'
    )

def create_session():
    session = requests.Session()
    retries = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[502, 503, 504],
        allowed_methods=["GET", "PUT"]
    )
    session.mount('https://', HTTPAdapter(max_retries=retries))
    return session

def download_file(session, url, auth):
    logging.info(f"Downloading from {url}...")
    response = session.get(url, auth=auth)
    if response.status_code != 200:
        logging.error(f"Failed to download file. Status code: {response.status_code}")
        sys.exit(1)

    # Extract filename from Content-Disposition
    content_disposition = response.headers.get('Content-Disposition', '')
    filename = "cve-index.bin.gz"  # default
    if 'filename=' in content_disposition:
        filename = content_disposition.split('filename=')[-1].strip('";')

    logging.info(f"Download successful. Filename: {filename}")
    return response.content, filename

def safe_filename(name):
    return re.sub(r'[^\w.-]', '_', name)

def save_file_locally(data, filename='cve-index.bin.gz'):
    safe_name = safe_filename(filename)
    path = os.path.join('/tmp', safe_name)
    logging.debug(f"Saving file to {path} for debugging purposes.")
    with open(path, 'wb') as f:
        f.write(data)
    logging.info(f"File saved as {path}.")

def upload_file(session, data, upload_url, auth, verify=True):
    headers = {'Content-Type': 'application/x-gzip'}
    logging.info(f"Uploading to {upload_url}...")
    response = session.put(upload_url, headers=headers, auth=auth, data=data, verify=verify)
    if response.status_code not in (200, 201, 204):
        logging.error(f"Failed to upload file. Status code: {response.status_code}")
        logging.debug(response.text)
        sys.exit(1)
    logging.info("Upload successful.")

def main():
    parser = argparse.ArgumentParser(description='Download and forward CVE index file.')
    parser.add_argument('--fwd-user', required=True, help='Username for fwd.app')
    parser.add_argument('--fwd-pass', required=True, help='Password for fwd.app')
    parser.add_argument('--target-host', required=True, help='Target host (e.g., portal.forwardnetworks.your.instance.url)')
    parser.add_argument('--target-user', required=True, help='Username for target host')
    parser.add_argument('--target-pass', required=True, help='Password for target host')
    parser.add_argument('--insecure-upload', action='store_true', help='Disable SSL verification on upload')
    parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')

    args = parser.parse_args()
    setup_logging(args.verbose)

    source_url = 'https://fwd.app/api/cve-index'
    target_url = f'https://{args.target_host}/api/cve-index'

    session = create_session()

    file_data, filename = download_file(session, source_url, HTTPBasicAuth(args.fwd_user, args.fwd_pass))
    save_file_locally(file_data, filename)

    upload_file(
        session,
        file_data,
        target_url,
        HTTPBasicAuth(args.target_user, args.target_pass),
        verify=not args.insecure_upload
    )

if __name__ == "__main__":
    main()

 

Did this topic help you find an answer to your question?

Forum|alt.badge.img+1
  • Employee
  • April 16, 2025

Super useful, I might make the following suggestion to use .env file or variables instead of exposing creds on command-line that could be hijacked.

 

 

#!/usr/bin/env python3

import argparse
import logging
import os
import re
import sys
import requests
from requests.auth import HTTPBasicAuth
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from dotenv import load_dotenv

def setup_logging(verbose):
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(levelname)s - %(message)s'
    )

def create_session():
    session = requests.Session()
    retries = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[502, 503, 504],
        allowed_methods=["GET", "PUT"]
    )
    session.mount('https://', HTTPAdapter(max_retries=retries))
    return session

def download_file(session, url, auth):
    logging.info(f"Downloading from {url}...")
    response = session.get(url, auth=auth)
    if response.status_code != 200:
        logging.error(f"Failed to download file. Status code: {response.status_code}")
        sys.exit(1)

    # Extract filename from Content-Disposition
    content_disposition = response.headers.get('Content-Disposition', '')
    filename = "cve-index.bin.gz"  # default
    if 'filename=' in content_disposition:
        filename = content_disposition.split('filename=')[-1].strip('";')

    logging.info(f"Download successful. Filename: {filename}")
    return response.content, filename

def safe_filename(name):
    return re.sub(r'[^\w.-]', '_', name)

def save_file_locally(data, filename='cve-index.bin.gz'):
    safe_name = safe_filename(filename)
    path = os.path.join('/tmp', safe_name)
    logging.debug(f"Saving file to {path} for debugging purposes.")
    with open(path, 'wb') as f:
        f.write(data)
    logging.info(f"File saved as {path}.")

def upload_file(session, data, upload_url, auth, verify=True):
    headers = {'Content-Type': 'application/x-gzip'}
    logging.info(f"Uploading to {upload_url}...")
    response = session.put(upload_url, headers=headers, auth=auth, data=data, verify=verify)
    if response.status_code not in (200, 201, 204):
        logging.error(f"Failed to upload file. Status code: {response.status_code}")
        logging.debug(response.text)
        sys.exit(1)
    logging.info("Upload successful.")

def check_credentials():
    required_vars = ['FWD_USER', 'FWD_PASS', 'TARGET_USER', 'TARGET_PASS']
    missing_vars = []

    # Check if .env file exists and load it
    env_file_exists = os.path.exists('.env')
    if env_file_exists:
        load_dotenv()
        logging.info("Loaded credentials from .env file")

    # Check for missing environment variables
    for var in required_vars:
        if not os.getenv(var):
            missing_vars.append(var)

    if missing_vars:
        error_msg = "Missing required credentials:\n"
        if not env_file_exists:
            error_msg += "- No .env file found\n"
        error_msg += "- Missing environment variables: " + ", ".join(missing_vars) + "\n\n"
        error_msg += "Please either:\n"
        error_msg += "1. Create a .env file with the required variables, or\n"
        error_msg += "2. Set the following environment variables:\n"
        for var in required_vars:
            error_msg += f"   {var}\n"
        logging.error(error_msg)
        sys.exit(1)

    # Log where credentials were loaded from
    for var in required_vars:
        if os.getenv(var):
            source = "environment variable" if var in os.environ else ".env file"
            logging.debug(f"Loaded {var} from {source}")

def main():
    parser = argparse.ArgumentParser(description='Download and forward CVE index file.')
    parser.add_argument('--target-host', required=True, help='Target host (e.g., portal.forwardnetworks.your.instance.url)')
    parser.add_argument('--insecure-upload', action='store_true', help='Disable SSL verification on upload')
    parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')

    args = parser.parse_args()
    setup_logging(args.verbose)

    # Check for credentials before proceeding
    check_credentials()

    # Get credentials from environment variables (which may have come from .env or system env)
    fwd_user = os.getenv('FWD_USER')
    fwd_pass = os.getenv('FWD_PASS')
    target_user = os.getenv('TARGET_USER')
    target_pass = os.getenv('TARGET_PASS')

    source_url = 'https://fwd.app/api/cve-index'
    target_url = f'https://{args.target_host}/api/cve-index'

    session = create_session()

    file_data, filename = download_file(session, source_url, HTTPBasicAuth(fwd_user, fwd_pass))
    save_file_locally(file_data, filename)

    upload_file(
        session,
        file_data,
        target_url,
        HTTPBasicAuth(target_user, target_pass),
        verify=not args.insecure_upload
    )

if __name__ == "__main__":
    main()

 


Reply


Cookie policy

We use cookies to enhance and personalize your experience. If you accept you agree to our full cookie policy. Learn more about our cookies.

 
Cookie settings