#pragma once // Step 190: Vulnerability knowledge base (OSV) #include #include #include #include #include #include #include #include #include #include #include #include struct VulnerabilityRecord { std::string ecosystem; std::string package; std::string cveId; std::string summary; std::string severity; std::vector affectedVersions; std::vector fixedVersions; std::vector references; struct VersionRange { std::string introduced; std::string fixed; }; std::vector ranges; }; class VulnerabilityDatabase { public: using Fetcher = std::function; void setCacheDirectory(const std::string& path) { std::scoped_lock lock(mu_); cacheDir_ = path; } void setTTLHours(int hours) { std::scoped_lock lock(mu_); ttlHours_ = std::max(1, hours); } void setOfflineMode(bool offline) { std::scoped_lock lock(mu_); offlineMode_ = offline; } void setFetcher(Fetcher fetcher) { std::scoped_lock lock(mu_); fetcher_ = std::move(fetcher); } void trackPackage(const std::string& ecosystem, const std::string& package) { std::scoped_lock lock(mu_); tracked_.push_back(makeKey(ecosystem, package)); } void startBackgroundRefresh() { std::thread([this]() { refreshTrackedPackages(); }).detach(); } std::vector query(const std::string& ecosystem, const std::string& package, const std::string& version) { ensureLoaded(ecosystem, package); const std::string key = makeKey(ecosystem, package); std::scoped_lock lock(mu_); auto it = cache_.find(key); if (it == cache_.end()) return {}; if (version.empty()) return it->second; std::vector out; for (const auto& record : it->second) { if (isAffected(record, version)) out.push_back(record); } return out; } private: std::string cacheDir_ = defaultCacheDir(); int ttlHours_ = 24; bool offlineMode_ = false; Fetcher fetcher_; std::vector tracked_; std::unordered_map> cache_; std::mutex mu_; static std::string defaultCacheDir() { const char* home = std::getenv("USERPROFILE"); if (!home) home = std::getenv("HOME"); std::filesystem::path base = home ? home : "."; return (base / ".whetstone" / "vuln_cache").string(); } static std::string makeKey(const std::string& ecosystem, const std::string& package) { return ecosystem + ":" + package; } static std::string sanitizeFileName(const std::string& name) { std::string out; out.reserve(name.size()); for (char c : name) { if (std::isalnum(static_cast(c)) || c == '_' || c == '-') { out.push_back(c); } else if (c == ':' || c == '/' || c == '\\') { out.push_back('_'); } else { out.push_back('_'); } } return out; } std::filesystem::path cachePathFor(const std::string& ecosystem, const std::string& package) const { std::filesystem::path dir(cacheDir_); std::string file = sanitizeFileName(makeKey(ecosystem, package)) + ".json"; return dir / file; } bool cacheIsStale(const std::filesystem::path& path) const { if (!std::filesystem::exists(path)) return true; auto last = std::filesystem::last_write_time(path); auto now = std::filesystem::file_time_type::clock::now(); auto age = now - last; auto hours = std::chrono::duration_cast(age).count(); return hours >= ttlHours_; } void ensureLoaded(const std::string& ecosystem, const std::string& package) { const std::string key = makeKey(ecosystem, package); { std::scoped_lock lock(mu_); if (cache_.find(key) != cache_.end()) return; } refreshPackage(ecosystem, package, false); } void refreshTrackedPackages() { std::vector tracked; { std::scoped_lock lock(mu_); tracked = tracked_; } for (const auto& key : tracked) { auto pos = key.find(':'); if (pos == std::string::npos) continue; refreshPackage(key.substr(0, pos), key.substr(pos + 1), true); } } void refreshPackage(const std::string& ecosystem, const std::string& package, bool allowFetch) { std::filesystem::path path = cachePathFor(ecosystem, package); if (!allowFetch && !cacheIsStale(path)) { loadCache(path, ecosystem, package); return; } if (allowFetch && !offlineMode_ && fetcher_) { std::string payload = fetcher_(ecosystem, package); if (!payload.empty()) { std::filesystem::create_directories(path.parent_path()); std::ofstream out(path); if (out.is_open()) { out << payload; } loadCache(path, ecosystem, package); return; } } loadCache(path, ecosystem, package); } void loadCache(const std::filesystem::path& path, const std::string& ecosystem, const std::string& package) { std::ifstream in(path); if (!in.is_open()) return; nlohmann::json doc; try { in >> doc; } catch (...) { return; } auto records = parseOSV(doc); std::scoped_lock lock(mu_); cache_[makeKey(ecosystem, package)] = std::move(records); } static std::vector parseOSV(const nlohmann::json& doc) { std::vector records; if (!doc.is_object()) return records; nlohmann::json vulns; if (doc.contains("vulnerabilities")) { vulns = doc.at("vulnerabilities"); } else if (doc.contains("id")) { vulns = nlohmann::json::array({doc}); } else { return records; } for (const auto& vuln : vulns) { if (!vuln.is_object()) continue; VulnerabilityRecord record; record.summary = vuln.value("summary", ""); record.cveId = pickCVE(vuln); record.severity = deriveSeverity(vuln); record.references = parseReferences(vuln); if (!vuln.contains("affected") || !vuln["affected"].is_array()) continue; for (const auto& affected : vuln["affected"]) { if (!affected.is_object()) continue; if (!affected.contains("package")) continue; const auto& pkg = affected["package"]; record.ecosystem = pkg.value("ecosystem", ""); record.package = pkg.value("name", ""); record.affectedVersions = parseVersions(affected); record.fixedVersions = parseFixedVersions(affected); record.ranges = parseRanges(affected); records.push_back(record); } } return records; } static std::vector parseReferences(const nlohmann::json& vuln) { std::vector refs; if (!vuln.contains("references")) return refs; for (const auto& ref : vuln["references"]) { if (!ref.is_object()) continue; std::string url = ref.value("url", ""); if (!url.empty()) refs.push_back(url); } return refs; } static std::string pickCVE(const nlohmann::json& vuln) { if (vuln.contains("aliases")) { for (const auto& alias : vuln["aliases"]) { if (alias.is_string()) { std::string val = alias.get(); if (val.rfind("CVE-", 0) == 0) return val; } } } return vuln.value("id", ""); } static std::string deriveSeverity(const nlohmann::json& vuln) { if (vuln.contains("severity") && vuln["severity"].is_array()) { for (const auto& sev : vuln["severity"]) { std::string score = sev.value("score", ""); double val = 0.0; try { val = std::stod(score); } catch (...) { val = 0.0; } if (val >= 9.0) return "Critical"; if (val >= 7.0) return "High"; if (val >= 4.0) return "Medium"; if (val > 0.0) return "Low"; } } if (vuln.contains("database_specific")) { const auto& db = vuln["database_specific"]; if (db.contains("severity")) return db["severity"].get(); } return "Unknown"; } static std::vector parseVersions(const nlohmann::json& affected) { std::vector versions; if (affected.contains("versions") && affected["versions"].is_array()) { for (const auto& v : affected["versions"]) { if (v.is_string()) versions.push_back(v.get()); } } return versions; } static std::vector parseFixedVersions(const nlohmann::json& affected) { std::vector versions; if (!affected.contains("ranges") || !affected["ranges"].is_array()) return versions; for (const auto& range : affected["ranges"]) { if (!range.contains("events")) continue; for (const auto& event : range["events"]) { if (event.contains("fixed")) { versions.push_back(event["fixed"].get()); } } } return versions; } static std::vector parseRanges(const nlohmann::json& affected) { std::vector ranges; if (!affected.contains("ranges") || !affected["ranges"].is_array()) return ranges; for (const auto& range : affected["ranges"]) { if (!range.contains("events")) continue; VulnerabilityRecord::VersionRange current; bool hasOpen = false; for (const auto& event : range["events"]) { if (event.contains("introduced")) { current.introduced = event["introduced"].get(); current.fixed.clear(); hasOpen = true; } else if (event.contains("fixed")) { if (!hasOpen) current.introduced = "0"; current.fixed = event["fixed"].get(); ranges.push_back(current); hasOpen = false; } else if (event.contains("last_affected")) { if (!hasOpen) current.introduced = "0"; current.fixed = event["last_affected"].get(); ranges.push_back(current); hasOpen = false; } } if (hasOpen) { ranges.push_back(current); } } return ranges; } static bool isAffected(const VulnerabilityRecord& record, const std::string& version) { for (const auto& v : record.affectedVersions) { if (v == version) return true; } for (const auto& range : record.ranges) { if (versionInRange(version, range.introduced, range.fixed)) return true; } return false; } static int compareVersions(const std::string& a, const std::string& b) { auto split = [](const std::string& v) { std::vector parts; std::string cur; for (char c : v) { if (std::isalnum(static_cast(c)) || c == '.') { cur.push_back(c); } else if (!cur.empty()) { parts.push_back(cur); cur.clear(); } } if (!cur.empty()) parts.push_back(cur); return parts; }; auto pa = split(a); auto pb = split(b); size_t count = std::max(pa.size(), pb.size()); for (size_t i = 0; i < count; ++i) { std::string sa = i < pa.size() ? pa[i] : "0"; std::string sb = i < pb.size() ? pb[i] : "0"; if (isNumber(sa) && isNumber(sb)) { long long ia = std::stoll(sa); long long ib = std::stoll(sb); if (ia < ib) return -1; if (ia > ib) return 1; } else { if (sa < sb) return -1; if (sa > sb) return 1; } } return 0; } static bool isNumber(const std::string& s) { if (s.empty()) return false; return std::all_of(s.begin(), s.end(), [](unsigned char c) { return std::isdigit(c); }); } static bool versionInRange(const std::string& version, const std::string& introduced, const std::string& fixed) { if (!introduced.empty() && compareVersions(version, introduced) < 0) return false; if (!fixed.empty() && compareVersions(version, fixed) >= 0) return false; return true; } };