Files
PHPGuard/backups/ubuntu-cve-20261022-152152/VulnerabilityService.swift
T

181 lines
6.3 KiB
Swift

import Foundation
enum VulnerabilityService {
enum ServiceError: LocalizedError {
case invalidResponse
case httpError(Int)
var errorDescription: String? {
switch self {
case .invalidResponse:
return "Invalid response from the vulnerability database."
case .httpError(let code):
return "Vulnerability database returned HTTP \(code)."
}
}
}
private struct NVDResponse: Decodable {
let vulnerabilities: [Entry]
}
private struct Entry: Decodable {
let cve: CVE
}
private struct CVE: Decodable {
let id: String
let published: String
let descriptions: [Description]
let metrics: Metrics?
}
private struct Description: Decodable {
let lang: String
let value: String
}
private struct Metrics: Decodable {
let cvssMetricV31: [Metric]?
let cvssMetricV30: [Metric]?
let cvssMetricV2: [Metric]?
}
private struct Metric: Decodable {
let cvssData: CVSSData
}
private struct CVSSData: Decodable {
let baseScore: Double?
let baseSeverity: String?
}
static func discover() async throws -> [CVERecord] {
var components = URLComponents(
string: "https://services.nvd.nist.gov/rest/json/cves/2.0"
)!
components.queryItems = [
URLQueryItem(name: "keywordSearch", value: "PHP"),
URLQueryItem(name: "resultsPerPage", value: "100")
]
guard let url = components.url else {
throw ServiceError.invalidResponse
}
var request = URLRequest(url: url)
request.timeoutInterval = 30
request.setValue(
"application/json",
forHTTPHeaderField: "Accept"
)
request.setValue(
"PHPGuard/1.4",
forHTTPHeaderField: "User-Agent"
)
let (data, response) = try await URLSession.shared.data(
for: request
)
guard let http = response as? HTTPURLResponse else {
throw ServiceError.invalidResponse
}
guard (200...299).contains(http.statusCode) else {
throw ServiceError.httpError(http.statusCode)
}
let decoded = try JSONDecoder().decode(
NVDResponse.self,
from: data
)
return decoded.vulnerabilities.map { entry in
let cve = entry.cve
let description = cve.descriptions.first {
$0.lang == "en"
}?.value ?? "No description available."
let metric =
cve.metrics?.cvssMetricV31?.first ??
cve.metrics?.cvssMetricV30?.first ??
cve.metrics?.cvssMetricV2?.first
return CVERecord(
id: cve.id,
description: description,
published: cve.published,
severity: metric?.cvssData.baseSeverity ?? "UNKNOWN",
score: metric?.cvssData.baseScore
)
}
.sorted { $0.published > $1.published }
}
}
extension VulnerabilityService {
// Returns upstream PHP CVE candidates, not confirmed Ubuntu/Debian exposures.
static func assessPHPVersion(_ version: String, runtime: String) async throws -> [CVEServerAssessment] {
let parts = version.split(separator: ".")
guard parts.count >= 3,
parts.prefix(3).allSatisfy({ Int($0) != nil }) else { return [] }
let canonical = parts.prefix(3).joined(separator: ".")
var components = URLComponents(string: "https://services.nvd.nist.gov/rest/json/cves/2.0")!
components.queryItems = [
URLQueryItem(name: "cpeName", value: "cpe:2.3:a:php:php:\(canonical):*:*:*:*:*:*:*"),
URLQueryItem(name: "resultsPerPage", value: "2000")
]
guard let url = components.url else { throw ServiceError.invalidResponse }
var request = URLRequest(url: url)
request.timeoutInterval = 45
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("PHPGuard/1.5", forHTTPHeaderField: "User-Agent")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else { throw ServiceError.invalidResponse }
guard (200...299).contains(http.statusCode) else { throw ServiceError.httpError(http.statusCode) }
let payload = try JSONDecoder().decode(VersionResponse.self, from: data)
// An incomplete NVD response must not be presented as full coverage.
guard payload.totalResults <= payload.vulnerabilities.count else {
throw ServiceError.invalidResponse
}
return payload.vulnerabilities.map { entry in
let cve = entry.cve
let metric = cve.metrics?.cvssMetricV31?.first ?? cve.metrics?.cvssMetricV30?.first ?? cve.metrics?.cvssMetricV2?.first
return CVEServerAssessment(
cveID: cve.id, runtime: runtime,
severity: metric?.cvssData.baseSeverity ?? "UNKNOWN",
score: metric?.cvssData.baseScore,
summary: cve.descriptions.first(where: { $0.lang == "en" })?.value ?? "No English description",
status: "Potentially affected",
evidence: "NVD PHP product CPE matches upstream PHP \(canonical). Distribution package revisions, backported patches, and container image fixes have NOT been verified."
)
}
}
private struct VersionResponse: Decodable {
let totalResults: Int
let vulnerabilities: [VersionEntry]
}
private struct VersionEntry: Decodable { let cve: VersionCVE }
private struct VersionCVE: Decodable {
let id: String
let descriptions: [VersionDescription]
let metrics: VersionMetrics?
}
private struct VersionDescription: Decodable { let lang: String; let value: String }
private struct VersionMetrics: Decodable {
let cvssMetricV31: [VersionMetric]?
let cvssMetricV30: [VersionMetric]?
let cvssMetricV2: [VersionMetric]?
}
private struct VersionMetric: Decodable { let cvssData: VersionCVSS }
private struct VersionCVSS: Decodable { let baseScore: Double?; let baseSeverity: String? }
}