/**
* CVE/NVD API 연동 모듈
*
* NVD (National Vulnerability Database) API를 통해 CVE 정보를 조회합니다.
* Rate limiting과 캐싱을 적용하여 효율적으로 동작합니다.
*
* @author zerry
*/
import axios, { AxiosError } from 'axios';
import NodeCache from 'node-cache';
/**
* CVE 상세 정보
*/
export interface CVEInfo {
id: string;
description: string;
publishedDate: string;
lastModifiedDate: string;
cvssV3Score?: number;
cvssV3Severity?: string;
cvssV2Score?: number;
cvssV2Severity?: string;
references: string[];
cweIds: string[];
}
/**
* NVD API 응답 타입
*/
interface NVDResponse {
vulnerabilities: Array<{
cve: {
id: string;
descriptions: Array<{
lang: string;
value: string;
}>;
published: string;
lastModified: string;
metrics?: {
cvssMetricV31?: Array<{
cvssData: {
baseScore: number;
baseSeverity: string;
};
}>;
cvssMetricV2?: Array<{
cvssData: {
baseScore: number;
};
baseSeverity: string;
}>;
};
references?: Array<{
url: string;
}>;
weaknesses?: Array<{
description: Array<{
lang: string;
value: string;
}>;
}>;
};
}>;
}
/**
* CVE 조회 클라이언트
*/
export class CVELookupClient {
private static readonly NVD_API_BASE = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
private static readonly CACHE_TTL = 3600 * 24; // 24시간
private static readonly RATE_LIMIT_DELAY = 6000; // 6초 (NVD API rate limit: 5 requests per 30 seconds for non-API key users)
private cache: NodeCache;
private lastRequestTime: number = 0;
constructor() {
this.cache = new NodeCache({
stdTTL: CVELookupClient.CACHE_TTL,
checkperiod: 600,
});
}
/**
* CVE ID로 상세 정보 조회
*/
async lookupCVE(cveId: string): Promise<CVEInfo | null> {
// 캐시 체크
const cached = this.cache.get<CVEInfo>(cveId);
if (cached) {
return cached;
}
// Rate limiting
await this.respectRateLimit();
try {
const response = await axios.get<NVDResponse>(CVELookupClient.NVD_API_BASE, {
params: {
cveId: cveId,
},
timeout: 10000,
});
const vulnerabilities = response.data.vulnerabilities;
if (!vulnerabilities || vulnerabilities.length === 0) {
return null;
}
const cve = vulnerabilities[0].cve;
const cveInfo = this.parseCVEData(cve);
// 캐시 저장
this.cache.set(cveId, cveInfo);
return cveInfo;
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
if (axiosError.response?.status === 404) {
// CVE가 존재하지 않음
return null;
}
console.error(`CVE 조회 실패 (${cveId}): ${axiosError.message}`);
} else {
console.error(`CVE 조회 실패 (${cveId}):`, error);
}
return null;
}
}
/**
* 여러 CVE를 일괄 조회
*/
async lookupMultipleCVEs(cveIds: string[]): Promise<Map<string, CVEInfo>> {
const results = new Map<string, CVEInfo>();
for (const cveId of cveIds) {
const info = await this.lookupCVE(cveId);
if (info) {
results.set(cveId, info);
}
}
return results;
}
/**
* Rate limit 준수
*/
private async respectRateLimit(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < CVELookupClient.RATE_LIMIT_DELAY) {
const waitTime = CVELookupClient.RATE_LIMIT_DELAY - timeSinceLastRequest;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.lastRequestTime = Date.now();
}
/**
* NVD API 응답을 CVEInfo로 파싱
*/
private parseCVEData(cve: any): CVEInfo {
// Description (영어 우선)
const enDescription = cve.descriptions?.find((d: any) => d.lang === 'en');
const description = enDescription?.value || cve.descriptions?.[0]?.value || 'No description available';
// CVSS Scores
const cvssV3 = cve.metrics?.cvssMetricV31?.[0];
const cvssV2 = cve.metrics?.cvssMetricV2?.[0];
// References
const references = cve.references?.map((r: any) => r.url) || [];
// CWE IDs
const cweIds: string[] = [];
if (cve.weaknesses) {
for (const weakness of cve.weaknesses) {
for (const desc of weakness.description) {
if (desc.value && desc.value.startsWith('CWE-')) {
cweIds.push(desc.value);
}
}
}
}
return {
id: cve.id,
description,
publishedDate: cve.published,
lastModifiedDate: cve.lastModified,
cvssV3Score: cvssV3?.cvssData?.baseScore,
cvssV3Severity: cvssV3?.cvssData?.baseSeverity,
cvssV2Score: cvssV2?.cvssData?.baseScore,
cvssV2Severity: cvssV2?.baseSeverity,
references,
cweIds,
};
}
/**
* 캐시 통계
*/
getCacheStats() {
return {
keys: this.cache.keys().length,
stats: this.cache.getStats(),
};
}
/**
* 캐시 초기화
*/
clearCache() {
this.cache.flushAll();
}
}
/**
* 싱글톤 인스턴스
*/
export const cveLookupClient = new CVELookupClient();