Created
March 10, 2025 08:25
-
-
Save quinn-dougherty/5f92b50e017bfbc3c44c42d9cf0f6cdf to your computer and use it in GitHub Desktop.
Count CVEs pertaining to docker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Docker CVE Counter - Counts Docker-related CVEs by year | |
| This script queries the National Vulnerability Database (NVD) API to find | |
| Docker-related vulnerabilities and provides a yearly count breakdown. | |
| """ | |
| import requests | |
| import json | |
| import time | |
| from collections import Counter, defaultdict | |
| import re | |
| from datetime import datetime | |
| import argparse | |
| class DockerCVECounter: | |
| def __init__(self, api_key=None): | |
| self.api_key = api_key | |
| self.base_url = "https://services.nvd.nist.gov/rest/json/cves/2.0" | |
| self.headers = {"User-Agent": "DockerCVECounter/1.0"} | |
| if api_key: | |
| self.headers["apiKey"] = api_key | |
| def search_cves(self, keyword, start_index=0): | |
| """Search for CVEs with the given keyword.""" | |
| params = { | |
| "keywordSearch": keyword, | |
| "startIndex": start_index, | |
| "resultsPerPage": 2000, # Maximum allowed value | |
| } | |
| try: | |
| response = requests.get(self.base_url, params=params, headers=self.headers) | |
| response.raise_for_status() | |
| return response.json() | |
| except requests.exceptions.RequestException as e: | |
| print(f"Error fetching CVEs: {e}") | |
| return None | |
| def get_all_docker_cves(self): | |
| """Retrieve all Docker-related CVEs from the NVD database.""" | |
| all_cves = [] | |
| start_index = 0 | |
| while True: | |
| print(f"Fetching CVEs starting from index {start_index}...") | |
| result = self.search_cves("docker", start_index) | |
| if ( | |
| not result | |
| or "vulnerabilities" not in result | |
| or not result["vulnerabilities"] | |
| ): | |
| break | |
| vulns = result["vulnerabilities"] | |
| all_cves.extend(vulns) | |
| if len(vulns) < 2000: | |
| break | |
| start_index += 2000 | |
| # Respect rate limits | |
| print("Waiting to respect API rate limits...") | |
| time.sleep( | |
| 6 | |
| ) # NVD recommends at least 6 seconds between requests without API key | |
| return all_cves | |
| def filter_relevant_cves(self, cves): | |
| """ | |
| Filter CVEs to ensure they're truly Docker-related. | |
| This helps exclude false positives where 'docker' is mentioned but not the main subject. | |
| """ | |
| docker_cves = [] | |
| docker_patterns = [ | |
| re.compile(r"\bdocker\b", re.IGNORECASE), | |
| re.compile( | |
| r"\bmoby\b", re.IGNORECASE | |
| ), # Docker's open-source engine project | |
| re.compile(r"\bdocker engine\b", re.IGNORECASE), | |
| re.compile(r"\bdocker daemon\b", re.IGNORECASE), | |
| re.compile(r"\bdockerd\b", re.IGNORECASE), | |
| ] | |
| for cve_entry in cves: | |
| cve_item = cve_entry.get("cve", {}) | |
| cve_id = cve_item.get("id", "Unknown") | |
| descriptions = cve_item.get("descriptions", []) | |
| # Get the English description | |
| description = "" | |
| for desc in descriptions: | |
| if desc.get("lang") == "en": | |
| description = desc.get("value", "") | |
| break | |
| # Check if the description indicates this is primarily about Docker | |
| if any(pattern.search(description) for pattern in docker_patterns): | |
| # Further filter to reduce false positives | |
| # Skip entries that are primarily about other software that just happens to mention Docker | |
| skip_patterns = [ | |
| re.compile( | |
| r"when using docker", re.IGNORECASE | |
| ), # Often about other software used with Docker | |
| re.compile( | |
| r"in a docker", re.IGNORECASE | |
| ), # Often about software running in Docker, not Docker itself | |
| re.compile( | |
| r"docker image", re.IGNORECASE | |
| ), # Often about vulnerable images, not Docker itself | |
| ] | |
| # Skip if the CVE is primarily about another product | |
| if description and any( | |
| skip_pattern.search(description) for skip_pattern in skip_patterns | |
| ): | |
| # But keep it if it explicitly mentions Docker engine/daemon components | |
| if not description or not re.search( | |
| r"\bdocker engine\b|\bdockerd\b|\bmoby\b", | |
| description, | |
| re.IGNORECASE, | |
| ): | |
| continue | |
| docker_cves.append(cve_entry) | |
| return docker_cves | |
| def categorize_by_year(self, cves): | |
| """Categorize CVEs by the year they were published.""" | |
| yearly_counts = Counter() | |
| cve_details_by_year = defaultdict(list) | |
| for cve_entry in cves: | |
| cve_item = cve_entry.get("cve", {}) | |
| cve_id = cve_item.get("id", "Unknown") | |
| published_date = cve_item.get("published", "") | |
| try: | |
| # Parse the published date (format: 2023-01-01T00:00:00.000) | |
| year = datetime.fromisoformat( | |
| published_date.replace("Z", "+00:00") | |
| ).year | |
| yearly_counts[year] += 1 | |
| # Store CVE details for later reference | |
| cve_details_by_year[year].append( | |
| { | |
| "id": cve_id, | |
| "published": published_date, | |
| "base_score": self.get_base_score(cve_item), | |
| } | |
| ) | |
| except (ValueError, TypeError): | |
| print(f"Could not parse published date for {cve_id}: {published_date}") | |
| return yearly_counts, cve_details_by_year | |
| def get_base_score(self, cve_item): | |
| """Extract the CVSS base score from a CVE item.""" | |
| metrics = cve_item.get("metrics", {}) | |
| # Try to get CVSS 3.x score first | |
| cvss_v3 = metrics.get("cvssMetricV31", []) or metrics.get("cvssMetricV30", []) | |
| if cvss_v3: | |
| return cvss_v3[0].get("cvssData", {}).get("baseScore", "N/A") | |
| # Fall back to CVSS 2.0 | |
| cvss_v2 = metrics.get("cvssMetricV2", []) | |
| if cvss_v2: | |
| return cvss_v2[0].get("cvssData", {}).get("baseScore", "N/A") | |
| return "N/A" | |
| def display_results(self, yearly_counts, cve_details): | |
| """Display the results in a readable format.""" | |
| print("\n===== Docker-related CVEs by Year =====") | |
| # Sort years in descending order (newest first) | |
| for year, count in sorted(yearly_counts.items(), reverse=True): | |
| print(f"{year}: {count} CVEs") | |
| print(f"\nTotal Docker-related CVEs found: {sum(yearly_counts.values())}") | |
| # Calculate average per year | |
| if yearly_counts: | |
| avg_per_year = sum(yearly_counts.values()) / len(yearly_counts) | |
| print(f"Average CVEs per year: {avg_per_year:.2f}") | |
| # Detailed analysis option | |
| detailed = input( | |
| "\nShow detailed breakdown of CVEs for a specific year? (Enter year or 'n' to skip): " | |
| ) | |
| if detailed.lower() != "n" and detailed.isdigit(): | |
| year_to_show = int(detailed) | |
| if year_to_show in cve_details: | |
| print(f"\n===== Detailed Docker CVEs for {year_to_show} =====") | |
| for cve in cve_details[year_to_show]: | |
| print( | |
| f"{cve['id']} - CVSS Score: {cve['base_score']} - Published: {cve['published']}" | |
| ) | |
| else: | |
| print(f"No data available for {year_to_show}") | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Count Docker-related CVEs by year") | |
| parser.add_argument( | |
| "--api-key", help="NVD API key (optional, allows higher rate limits)" | |
| ) | |
| args = parser.parse_args() | |
| print( | |
| "Docker CVE Counter - Fetching and analyzing Docker-related vulnerabilities..." | |
| ) | |
| counter = DockerCVECounter(api_key=args.api_key) | |
| all_cves = counter.get_all_docker_cves() | |
| if not all_cves: | |
| print("No CVEs found or error connecting to the NVD database.") | |
| return | |
| print( | |
| f"Found {len(all_cves)} potential CVEs mentioning Docker. Filtering for relevance..." | |
| ) | |
| docker_cves = counter.filter_relevant_cves(all_cves) | |
| print(f"After filtering: {len(docker_cves)} Docker-related CVEs.") | |
| yearly_counts, cve_details = counter.categorize_by_year(docker_cves) | |
| counter.display_results(yearly_counts, cve_details) | |
| if __name__ == "__main__": | |
| main() |
Author
quinn-dougherty
commented
Mar 10, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment