증분 수집 전략
왜 전체 재수집만으로는 부족한가
취약점 데이터베이스를 처음 만들 때는 전체 덤프나 전체 API 페이지를 한 번 훑으면 된다. 문제는 그 다음날부터다. CVE, GHSA, OSV 같은 보안 데이터는 새 레코드가 추가될 뿐 아니라 기존 레코드의 점수, 영향 버전, withdrawn 여부, reference, alias가 계속 바뀐다. "어제 이후 새로 나온 CVE만 가져오기"로 끝내면 이미 저장한 레코드의 수정분을 놓친다.
증분 수집의 목표는 세 가지다.
- 빠짐없이 반영: 새 레코드와 수정된 레코드를 모두 가져온다.
- 중복 없이 저장: 같은 CVE/GHSA/OSV를 여러 소스에서 보더라도 하나의 canonical key로 합친다.
- 재시도 가능: 네트워크 장애, API 제한, 파서 오류가 나도 같은 구간을 다시 실행할 수 있다.
취약점 수집은 단순 크롤러가 아니라 작은 CDC 시스템에 가깝다. 데이터 소스의 modified 또는 updated 시간을 물고, 내부 저장소에는 마지막 성공 지점과 원본 해시를 남겨야 한다.
기본 아키텍처
5~30분 주기 → Source Connector
NVD · GHSA · OSV → Checkpoint
watermark + cursor
원본 JSON + hash → Normalizer
CVE/GHSA/OSV 공통 모델 → Canonical Store
upsert by advisory id
핵심은 원본 수집과 정규화 저장을 분리하는 것이다. 원본 JSON을 보관하지 않으면 나중에 파서 버그를 고쳤을 때 재처리할 수 없다. 반대로 원본만 보관하고 정규화 테이블을 만들지 않으면 패키지 매칭, diff, 알림이 매번 비싸진다.
워터마크는 published가 아니라 modified 기준
취약점 레코드에는 대체로 두 종류의 시간이 있다.
| 시간 필드 | 의미 | 증분 수집에서의 용도 |
|---|---|---|
published | 최초 공개 시점 | 신규 공개 정렬, 리포트 기준 |
modified / lastModified / updated_at | 마지막 수정 시점 | 증분 수집의 주 기준 |
수집기는 published가 아니라 수정 시간을 기준으로 조회해야 한다. 예를 들어 2021년에 공개된 Log4Shell 레코드가 2026년에 reference나 affected range를 수정했다면, published >= 어제 조건으로는 절대 잡히지 않는다.
안전한 워터마크 규칙은 다음과 같다.
- 각 소스별로
last_successful_window_end를 저장한다. - 다음 실행은
start = last_successful_window_end - overlap으로 시작한다. overlap은 5분~24시간 사이에서 소스 특성에 맞춘다. NVD처럼 지연 반영이 있을 수 있는 소스는 더 길게 둔다.- 저장은 advisory id 기준 upsert이므로 overlap 구간의 중복은 문제가 되지 않게 만든다.
- 모든 페이지와 정규화가 성공한 뒤에만 워터마크를 전진시킨다.
즉 워터마크는 "여기까지 요청했다"가 아니라 "여기까지 저장·검증까지 끝났다"여야 한다.
소스별 증분 방식
NVD CVE API
NVD 2.0 CVE API는 https://services.nvd.nist.gov/rest/json/cves/2.0 엔드포인트에서 lastModStartDate, lastModEndDate, startIndex, resultsPerPage를 조합해 수정 시간 기준 페이지네이션을 수행할 수 있다. 응답에는 totalResults, startIndex, resultsPerPage가 포함되므로 offset 기반으로 끝까지 순회한다.
GET /rest/json/cves/2.0
?lastModStartDate=2026-06-01T00:00:00.000+00:00
&lastModEndDate=2026-06-02T00:00:00.000+00:00
&startIndex=0
&resultsPerPage=2000운영 팁은 두 가지다. 첫째, API 키를 쓰면 rate limit 여유가 커지지만 키를 URL query가 아니라 apiKey 헤더로 보내야 한다. 둘째, 큰 기간을 한 번에 조회하지 말고 하루 또는 몇 시간 단위 window로 쪼개야 실패 재시도가 쉬워진다.
NVD에는 CVE Change History API도 있다. 이것은 CVE 레코드가 언제, 왜 변경됐는지 추적하는 용도에 가깝다. 실제 canonical store를 갱신할 때는 CVE API로 현재 스냅샷을 다시 받아 저장하고, change history는 감사 로그나 원인 분석에 보조로 붙이는 구조가 단순하다.
GitHub Advisory Database
GitHub의 global security advisories REST API는 https://api.github.com/advisories에서 sort=updated, direction=desc, per_page 같은 파라미터를 지원한다. 공개 advisory 조회는 인증 없이도 가능하지만, 안정적인 운영에서는 토큰을 써서 rate limit을 관리하는 편이 낫다.
REST API가 updated since 필터를 모든 사용 사례에 직접 제공한다고 가정하지 않는 것이 안전하다. 실무적으로는 다음 방식이 견고하다.
sort=updated&direction=desc로 최신 수정 순 페이지를 읽는다.- 각 advisory의
updated_at또는 동등한 갱신 시간을 확인한다. - 로컬 워터마크보다 오래된 레코드가 충분히 연속으로 나오면 중단한다.
- 그래도 하루 1회 정도는 더 넓은 window를 재검사해 누락을 보정한다.
GHSA는 ghsa_id와 cve_id를 동시에 가질 수 있다. 저장할 때는 GHSA-...를 원본 키로 보존하고, aliases 또는 identifiers에서 CVE를 추출해 NVD 레코드와 연결한다. CVE가 없는 GitHub-originated advisory도 있으므로 cve_id가 null인 레코드를 버리면 안 된다.
OSV
OSV API의 /v1/query와 /v1/querybatch는 "내 패키지 버전이 취약한가"를 묻는 lookup API다. querybatch는 한 요청에 최대 1000개 package version 또는 commit 질의를 담을 수 있다. 이것은 인벤토리 스캔에는 훌륭하지만, 전체 데이터베이스를 증분 미러링하는 API는 아니다.
OSV를 내부 저장소로 미러링하려면 공개 GCS bucket(osv-vulnerabilities)이나 ecosystem별 덤프를 주기적으로 가져와 modified 필드와 원본 해시로 변경을 판단하는 쪽이 더 자연스럽다. Dependency-Track도 OSV를 GCS bucket에서 미러링하고, 선택한 ecosystem만 동기화하는 방식을 문서화한다.
OSV의 장점은 패키지 생태계와 영향 버전 범위가 이미 구조화되어 있다는 점이다. 수집 후 정규화 단계에서 affected[].package.ecosystem, affected[].package.name, ranges, versions, aliases를 그대로 보존하면 다음 단계의 패키지 매칭 비용이 크게 줄어든다.
저장 모델: 이벤트 로그와 현재 상태를 같이 둔다
증분 수집에서 가장 흔한 실수는 현재 상태 테이블 하나만 두는 것이다. 그렇게 하면 "무엇이 언제 바뀌었는지"를 추적할 수 없다. 최소한 아래 두 계층은 분리하는 편이 좋다.
| 계층 | 키 | 저장 내용 | 용도 |
|---|---|---|---|
| raw event | source + source_id + fetched_at | 원본 JSON, HTTP metadata, body hash | 재처리, 감사, 파서 회귀 테스트 |
| canonical advisory | canonical id | 정규화 필드, aliases, latest hash, latest modified | 조회, 매칭, 리포트 |
| change event | canonical id + version | 이전/이후 hash, 변경 필드 목록 | 알림, 영향 재계산 |
canonical id는 한 번에 완벽히 정하기 어렵다. 초기에 source:id를 기본 키로 저장하고, alias graph를 별도 테이블로 관리한 뒤, CVE/GHSA/OSV 관계가 확인되면 logical advisory group으로 묶는 방식이 안전하다.
-- 개념 예시
advisory_source_record(source, source_id, modified_at, raw_hash, raw_json)
advisory_alias(source, source_id, alias_type, alias_value)
advisory_canonical(canonical_id, latest_modified_at, normalized_json)
advisory_change(canonical_id, changed_at, old_hash, new_hash, changed_fields)idempotent upsert 설계
overlap window를 쓰면 같은 레코드를 반복해서 보게 된다. 따라서 저장은 반드시 idempotent해야 한다.
좋은 upsert 흐름은 다음과 같다.
- 원본 JSON에서 안정적인
source_id를 뽑는다. NVD는 CVE ID, GitHub는 GHSA ID, OSV는 OSVid가 된다. - 정규화 전 원본 body의 hash를 계산한다.
- 같은
source + source_id의 최신 hash와 같으면 canonical 갱신을 건너뛴다. - hash가 다르면 raw event를 추가하고 정규화한다.
- 정규화 결과를 canonical store에 upsert한다.
- 영향받는 패키지 매칭 결과를 재계산한다.
이 구조에서는 네트워크 재시도, 스케줄러 중복 실행, overlap 재조회가 모두 안전해진다. 같은 입력은 같은 결과를 만들고, 변경된 입력만 downstream 작업을 발생시킨다.
삭제와 withdrawn 처리
취약점 데이터는 완전히 삭제되기보다 withdrawn, rejected, disputed 같은 상태로 바뀌는 경우가 많다. 수집기는 "조회 결과에서 사라졌다"를 곧바로 삭제로 해석하면 안 된다.
- OSV에는
withdrawn필드가 있을 수 있다. - CVE에는 rejected 상태의 레코드가 존재할 수 있다.
- GitHub Advisory는 withdrawn 또는 reviewed/unreviewed 상태 변화가 있을 수 있다.
운영 저장소에서는 물리 삭제보다 status를 바꾸고 기존 매칭 결과를 무효화하는 방식이 안전하다. 이미 사용자에게 알림을 보낸 취약점이 나중에 withdrawn되면, "사라짐"이 아니라 "철회됨" 이벤트를 남겨야 감사가 가능하다.
스케줄링과 백오프
증분 수집은 빠를수록 좋은 작업이 아니다. 외부 보안 데이터 제공자의 rate limit과 갱신 주기를 존중해야 한다.
권장 패턴은 다음과 같다.
- NVD: API 키 사용, 짧은 window, rate limit 초과 시 exponential backoff.
- GHSA:
ETag/conditional request를 활용할 수 있으면 사용, 토큰별 rate limit 모니터링. - OSV dump: ecosystem 단위로 나눠 병렬도를 제한하고, 파일 hash가 같으면 재처리 생략.
- 모든 소스: 실패한 window를 dead-letter queue나 retry table에 남긴다.
재시도는 "전체 하루를 다시"보다 "실패한 source/window/page만 다시"가 낫다. 그래야 한 소스 장애가 전체 파이프라인을 막지 않는다.
체크리스트
실제 구현 전에 아래 질문에 답할 수 있어야 한다.
- 소스별 워터마크 필드는 무엇인가? (
lastModified,updated_at,modified) - API 페이지네이션 방식은 offset인가 cursor인가?
- overlap window는 몇 분/몇 시간으로 둘 것인가?
- 같은 CVE가 NVD와 GHSA와 OSV에 동시에 있을 때 어떤 필드를 canonical로 삼을 것인가?
- withdrawn/rejected 상태가 기존 알림과 매칭 결과를 어떻게 바꾸는가?
- raw JSON을 재처리할 수 있는가?
- rate limit 초과와 부분 실패를 어디에 기록하는가?
Open Questions
- GitHub Advisory API의 세부 필드와 필터는 API 버전에 따라 달라질 수 있으므로, 운영 코드에서는 응답 schema snapshot 테스트를 두는 것이 좋다.
- OSV 전체 미러링은 GCS dump 구조와 생태계 선택 정책에 따라 비용이 크게 달라진다. 실제 운영에서는 필요한 ecosystem만 먼저 동기화하는 것이 안전하다.
References
- https://nvd.nist.gov/developers/vulnerabilities
- https://nvd.nist.gov/General/News/api-20-announcements
- https://services.nvd.nist.gov/rest/json/cves/2.0
- https://docs.github.com/en/rest/security-advisories/global-advisories
- https://github.com/advisories
- https://osv.dev/docs/osv_service_v1.swagger.json
- https://google.github.io/osv.dev/api/
- https://docs.dependencytrack.org/datasources/osv
- https://docs.dependencytrack.org/datasources/github-advisories/