취약점 데이터 (Trivy DB)
v0.10.0부터 TRUSCA는 단일 임베디드 엔진 Trivy(Aqua Security)로 SBOM을 CVE에 대조합니다. 워커 이미지에 trivy 바이너리가 포함되어 있고, Trivy DB(NVD + OSV + GHSA + EPSS + KEV 통합 번들)는 첫 부팅 시 다운로드되어 주간 단위로 갱신됩니다. 본 페이지는 그 라이프사이클을 운영·감사하는 방법입니다.
배포를 운영하는 super_admin. docker-compose와 기본 셸 친숙도 가정. 설치본에서 마이그레이션 중이라면 v0.10.0 릴리스 노트의 DT 제거 절차를 먼저 확인하세요.
v0.10.0은 Dependency-Track(DT)과 /admin/dt 커넥터 페이지를 제거했습니다. Trivy DB가 같은 역할(SBOM ↔ CVE 매칭)을 4GB JVM + H2 DB 대신 ~500MB 풋프린트로 수행합니다.
라이프사이클 동작
워커 부팅 ──► trivy --download-db-only ──► /var/lib/trivy/db
│
↓
Celery beat (주간) ─► trivy --download-db-only (refresh)
│
↓
trivy sbom <sbom.json> ──► vulnerability_findings
구성 요소는 세 가지입니다.
- 부팅 시 다운로드. 워커는 Celery
worker_ready시그널 핸들러를 등록해 워커가 큐를 소비하기 시작하면 백그라운드 스레드에서trivy --download-db-only를 호출합니다. 첫 다운로드는 약 500MB로, 일반 네트워크에서 1~3분이 소요됩니다. 다운로드는 백그라운드로 진행되므로 Celery는 즉시 태스크를 받기 시작합니다 — 부트스트랩과 경합하는 스캔은 Trivy 자체 파일 락에 잠깐 블로킹(분 단위 아님)된 뒤 새 manifest를 읽습니다. 캐시 볼륨이 이미 채워진 워커 재시작에서는 다운로드가 no-op입니다(Trivy가 manifest를 stat해 0으로 종료). - 주간 갱신. Celery Beat 태스크(
trustedoss.trivy_db_refresh, 일요일 03:00 UTC)가 동일trivy --download-db-only를 실행합니다. Trivy의 manifest 계약은 부분 다운로드를 원자적으로 교체합니다 — 갱신 실패는 기존 DB를 그대로 둡니다. 취약점 재매칭 beat(trustedoss.vulnerability_rematch_enqueue, 6시간 주기)가 다음 tick에 새 어드바이저리를 픽업해 새 critical을 Slack / Teams 알림으로 노출합니다. - 스캔별 사용. 각 source-scan 파이프라인은
trivy sbom <cyclonedx.json>을 호출해 컴포넌트를 로컬 DB에 대조합니다. 스캔당 네트워크 왕복은 없습니다.
갱신 실패(타임아웃, 네트워크 미도달, 미러 인증 오류)는 /notifications에 설정된 Slack·Teams 채널로 TRIVY-DB-REFRESH-FAILED / TRIVY-DB-REFRESH-TIMEOUT 알림을 발사합니다 — 운 영자는 웹훅 이벤트로 staleness를 인지하며 조용히 누락되지 않습니다.
업스트림 DB 재빌드 주기는 약 6시간입니다. 주간 갱신은 신선도와 egress 바이트를 트레이드오프합니다. 더 빠른 주기가 필요하면 TRIVY_DB_REFRESH_HOURS를 낮추고(admin 패널 + next_refresh_at에 노출되는 안내값) apps/backend/tasks/celery_app.py의 Beat 스케줄을 일간 cron으로 수정하세요.
환경 변수
| 키 | 기본값 | 설명 |
|---|---|---|
TRIVY_DB_REPOSITORY | ghcr.io/aquasecurity/trivy-db | DB를 받아오는 OCI 저장소. air-gapped 미러로 오버라이드. |
TRIVY_DB_REFRESH_HOURS | 168 (주간) | admin/health Trivy DB 패널에 next_refresh_at = last_update + 이 값으로 노출되는 안내 주기. 실제 beat 스케줄은 crontab(minute=0, hour=3, day_of_week='sun')이고, 더 빠른 주기는 apps/backend/tasks/celery_app.py에서 직접 수정. BomLens 엔진 스캔에서 데이터베이스가 얼마나 오래돼도 되는지도 이 값으로 정합니다(일 단위로 올림한 값의 두 배에 이틀을 더하고 30일을 넘지 않음, Trivy 데이터베이스의 나이 참고). 그래서 이 값을 낮출 때는 스케줄도 함께 바꾸십시오. 주간 스케줄을 그대로 두고 값만 낮추면 매주 며칠씩 BomLens 스캔이 실패합니다. |
TRIVY_CACHE_DIR | /var/lib/trivy | DB가 풀리는 디렉터리. 두 compose 파일 모두 공유 trivy-cache named volume을 여기 마운트 — 워커(rw, 다운로드·갱신)와 backend(ro, 관리자 health/disk 패널). |
TRIVY_DB_BOOTSTRAP_ON_START | true | 워커의 worker_ready 핸들러가 부팅 시 trivy --download-db-only를 실행할지. 별도 프로세스가 캐시 볼륨에 DB를 미러링하는 air-gapped 클러스터에선 false로 설정 — 워커는 네트워크 pull을 시도하지 않음. |
TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS | 900 | 부팅 시 다운로드 subprocess의 wall-clock 한도. 느린 회사 프록시에선 상향. 범위 [30, 3600]. |
TRIVY_DB_REFRESH_TIMEOUT_SECONDS | 900 | 주간 beat refresh subprocess의 wall-clock 한도. 범위 [30, 3600]. |
TRIVY_TIMEOUT_SECONDS | 300 | trivy sbom의 스캔별 타임아웃. 대규모 모놀리스에선 상향. |
모든 키는 apps/backend/core/config.py가 런타임에 읽습니다. 워커·beat 컨테이너를 다시 만들면 반영되며(docker-compose -f docker-compose.yml up -d worker-scan worker-default beat), 리빌드는 필요 없습니다.
동작 확인
설치 직후(또는 TRIVY_* 키 수정 후):
# 1. 워커에 DB가 있는지 확인
docker-compose -f docker-compose.yml exec worker-scan \
ls -lh /var/lib/trivy/db/
# 2. DB 메타데이터(UpdatedAt·NextUpdate·DownloadedAt) 확인
docker-compose -f docker-compose.yml exec worker-scan trivy version
# 3. 작은 스캔으로 동작 확인
docker-compose -f docker-compose.yml exec worker-scan \
trivy sbom --format json /tmp/sample-cyclonedx.json | jq '.Results | length'
정상 설치는 db/ 하위 파일 수가 0이 아니고, UpdatedAt 타임스탬프가 최근(≤ 7일)이며, 동작 확인 명령이 최소 1개의 Results 배열을 반환합니다.
/admin/health 하위의 Trivy DB 카드가 마지막 갱신 시각, 취약점 수, 신선도(fresh / stale / very_stale / unknown), 설정된 주기, 캐시 디렉터리, 미러 저장소를 노출합니다. 패널은 디스크의 metadata.json을 직접 읽으므로(라이브 네트워크 프로브 없음) air-gapped 클러스터에서도 유용합니다.
KEV 피드 패널
/admin/health의 Trivy DB 카드 옆에는 CISA KEV 카탈로그 일일 동기화 상태를 보여주는 KEV 피드 패널이 있습니다. KEV(Known Exploited Vulnerabilities, 알려진 악용 취약점)는 미국 사이버안보·인프라 보안청(CISA, Cybersecurity and Infrastructure Security Agency)이 실제 악용을 확인한 CVE 목록입니다. 이 동기화가 결과 테이블의 KEV 배지를 최신으로 유지합니다. 배지가 사용자에게 무엇을 뜻하는지는 KEV — 알려진 악용 취약점을 참고하십시오.
패널은 두 가지를 읽습니다. trustedoss.kev_catalog_refresh beat 태스크가 매 실행 끝에 UPSERT하는 단일 kev_sync_state 상태 행과, 취약점 카탈로그에서 현재 KEV로 표시된 CVE의 실시간 개수입니다. Trivy DB 카드와 달리 확인할 디스크 메타데이터 파일이 없으므로(동기화가 카탈로그에 바로 기록합니다)이 상태 행이 패널의 유일한 영속 소스입니다.
네 가지 지표
| 지표 | 의미 |
|---|---|
| 마지막 동기화 | 마지막으로 성공한 동기화 시각. 건너뛴 실행은 이 값을 갱신하지 않습니다 — 패널이 마지막 시도 시각을 따로 보여주므로 "마지막 시도는 오늘, 마지막 동기화는 3일 전"은 "피드가 3일째 실패 중"으로 읽힙니다. |
| KEV 등재 수 | 카탈로그에서 현재 KEV로 표시된 CVE의 실시간 개수 — 마지막 동기화 결과의 복사본이 아니라 페이지를 열 때 셉니다. 동기화된 배포라면 약 1,600건입니다. |
| 등재 / 해제 | 마지막 성공 실행에서 새로 등재(또는 날짜 보정)된 행 수와 해제된 행 수. CISA는 항목을 한두 건씩 넣고 빼므로 둘 다 보통 한 자릿수입니다. |
| 다음 동기화 | 다음 beat 실행 시각 — 매일 01:45 UTC. |
상태 값
| 상태 | 의미 |
|---|---|
| 정상 | 마지막 실행이 성공적으로 동기화했습니다(last_result: synced). |
| 건너뜀 + 사유 | 마지막 실행이 돌았지만 아무것도 기록하지 않았고, 기존 KEV 플래그는 보존되었습니다. 사유는 아래를 참고하십시오. |
| 비활성 | KEV_REFRESH_ENABLED=false — air-gapped 배포에서 기대되는 상태이며 장애가 아닙니다. |
| 미실행 | 설치 후 아직 한 번도 실행되지 않았습니다. 첫 실행은 beat 컨테이너 기동 후 24시간 안에 돕니다. |
건너뜀 사유는 다음과 같습니다.
feed_unavailable— 피드 다운로드가 실패했습니다(네트워크, HTTP 오류, 과대·손상 본문). 기존 플래그는 그대로 두고 다음 일일 실행이 재시도하므로, CISA의 일시 장애가 카탈로그를 대량 해제하는 일은 없습니다.feed_below_sanity_floor— 피드가 유효한 JSON으로 파싱되었지만 항목이 500건 미만입니다. 실제 카탈로그는 약 1,600건이고 지금까지 줄어든 적 없이 커져 왔는데, 속이 빈 문서(빈vulnerabilities배열, 잘린 본문, 오동작하는 미러)는 "유효"하게 파싱되면서 해제 단계에는 "전부 빠졌다"로 읽힙니다 — 방치하면 실행 한 번이 카탈로그의 모든 KEV 플래그를 지웁니다. 하한선은 이런 피드를 장애와 똑같이 다룹니다. 경고하고, 건너뛰고, 기존 플래그를 보존합니다. 정식 CISA URL에서 이 사유가 나오면 포털보다 피드(또는 미러)를 먼저 점검하십시오.unexpected:<예외명>— 예상한 실패 모드 밖의 오류입니다. beat / worker 로그에서kev_catalog_refresh_unexpected_error를 확인하십시오.
KEV_REFRESH_ENABLED=false이면 패널은 경보가 아니라 비활성 상태로 표시됩니다 — 동기 화가 예정되지 않았으므로 신선도를 따질 대상도 없습니다. 이 모드에서는 KEV 데이터가 로드되지 않습니다. 결과 화면에서 무엇이 사라지는지는 사용자 가이드의 안내를 참고하십시오. 대안으로 KEV_FEED_URL이 CISA JSON의 사내 미러를 가리킬 수도 있습니다 — TRIVY_DB_REPOSITORY와 같은 패턴입니다. 환경 변수 — KEV 카탈로그를 참고하십시오.
Air-gapped 운영
워커 호스트가 ghcr.io에 접근할 수 없다면 두 가지 지원 경로 중 운영 모델에 맞는 쪽을 선택하세요.
- 경로 A — 사내 OCI 미러.
trivy-db사본을 네트워크가 접근 가능한 사내 OCI 레지스트리에 유지. 워커는 여전히trivy --download-db-only(부트스트랩 + 주간 갱신)를 실행하되 미러에서 pull. 대부분의 엔터프라이즈에서 권장 — Trivy의 manifest 계약이 pull마다 원자적 교체와 무결성 검증을 유지. - 경로 B — 완전 오프라인 캐시 볼륨. 워커 내부 다운로드 경로 전체를 비활성화(
TRIVY_DB_BOOTSTRAP_ON_START=false)하고, 연결된 호스트에서 sneaker-net으로 가져온 tarball로 캐시 볼륨을 사전 채움. 사내 레지스트리조차 air gap 반대편에 있을 때 사용.
경로 A — 사내 OCI 미러
1. DB 미러링
ghcr.io에 접근 가능한 호스트에서 실행:
# oras 설치 (1회)
curl -fsSL https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz \
| tar -xz oras
sudo mv oras /usr/local/bin/
# 업스트림 DB pull
oras pull ghcr.io/aquasecurity/trivy-db:2
# 사내 레지스트리에 re-push
oras push registry.internal.acme.com/trivy/trivy-db:2 \
db.tar.gz:application/vnd.aquasec.trivy.db.layer.v1.tar+gzip
Trivy DB는 멀티 아키 OCI 아티팩트로 :1·:2 태그가 있습니다. 포털은 현재 :2(Trivy v2 스키마)를 사용하므로, 포털 버전이 요구하는 동일 태그로 고정하세요.
2. 워커가 미러를 바라보게 설정
.env:
TRIVY_DB_REPOSITORY=registry.internal.acme.com/trivy/trivy-db
미러가 인증을 요구하면 DB를 내려받는 워커마다 한 번씩 로그인합니다. worker-scan과 worker-default가 모두 해당합니다. 두 워커 모두 기동 시 DB를 내려받고, 주간 갱신은 기본 큐에서 그 작업을 가져간 워커가 실행합니다.
docker-compose -f docker-compose.yml exec -T worker-scan \
trivy registry login --username svc-trivy --password-stdin \
registry.internal.acme.com < /run/secrets/trivy_registry_pw
docker-compose -f docker-compose.yml exec -T worker-default \
trivy registry login --username svc-trivy --password-stdin \
registry.internal.acme.com < /run/secrets/trivy_registry_pw
Trivy는 컨테이너 내부 ~/.docker/config.json에 자격 증명을 저장합니다. 홈 디렉터리(/home/trustedoss)는 워커마다 따로 둔 볼륨에 있으므로 재시작과 docker-compose down 뒤에도 로그인이 유지됩니다. docker-compose down -v를 실행하면 지워집니다.