TT Lab
블로그 시작하기
배우기 러닝패스 코스

스캐너는 빨간 줄 마흔 개를 줬다

스캐너가 쏟아낸 빨간 줄에서 진짜를 가린다

TT Lab 에서 이어서 보기

목표

스캐너(Trivy)가 낸 리포트에서 출발해, 심각도만이 아니라 도달 가능성으로 우선순위를 정하고, 베이스 이미지를 올려 다시 스캔했을 때 숫자가 어떻게 줄어드는지 보고, 근거 있는 예외(OpenVEX)만 통과시키는 관문을 코드로 만듭니다.

왜 중요한가

스캐너는 "빨간 줄 수십 개" 를 쏟아 냅니다. 그것을 전부 심각도 순으로 고치려 하면 영영 끝나지 않고, 정작 급한 것이 묻힙니다. 현장의 질문은 둘입니다 — 이 취약점이 우리 코드에서 실제로 닿는가(도달성), 배포 산출물에 실리는가(범위). CRITICAL 이라도 그 취약 함수를 아무도 부르지 않으면 오늘의 최우선이 아닙니다. 반대로 MEDIUM 이라도 외부 입력이 닿는 경로에 있으면 급합니다.

고친 뒤에는 반드시 다시 잽니다. 베이스 이미지를 올리면 OS 취약점 여러 개가 한꺼번에 사라지는데, 재스캔으로 확인하지 않으면 무엇이 남았는지 모릅니다. 그리고 "지금은 안 고친다" 는 결정은 말이 아니라 기계가 읽을 수 있는 근거(OpenVEX)로 남겨, 다음 스캔이 같은 줄을 다시 띄우지 않게 합니다.

이 실습은 리포트에서 출발합니다

실습 파드에는 스캐너도 그 취약점 DB(Trivy DB 약 136MB, 매일 갱신)도 없어서, Trivy 가 낸 것과 같은 형식의 리포트가 /opt/fixtures/vulnmgmt/ 에 준비돼 있습니다. 실제 스캐너를 돌리는 명령은 읽기 자료에 있습니다.

단계

  1. 리포트를 심각도·클래스별 표로 /root/vuln/01-summary.json 에 만듭니다.
  2. 앱 취약점에 도달성·범위를 붙여 /root/vuln/02-enriched.json 에 저장합니다.
  3. fix 가 있는 것만 /root/vuln/03-fixable.json 에 모읍니다.
  4. now·later·watch 로 우선순위를 /root/vuln/04-priority.json 에 나눕니다.
  5. 베이스를 올린 리포트와 견주어 /root/vuln/05-basediff.json 에 OS 취약점 감소를 적습니다.
  6. 도달 불가 취약점에 OpenVEX 문서를 /root/vuln/06-vex.json 에 씁니다.
  7. 관문 /root/vuln/gate.py 를 만들어 /root/vuln/07-verdict.json 을 냅니다.

참고

리포트를 사람이 읽을 표로

/opt/fixtures/vulnmgmt/scan-old.json(Trivy 형식)을 읽어 심각도별 개수와 전체 개수를 /root/vuln/01-summary.json 에 저장하세요. total 과 by_severity(CRITICAL·HIGH·MEDIUM·LOW 키), 그리고 by_class(os-pkgs·lang-pkgs 개수)를 담습니다.

Trivy JSON 은 Results[].Vulnerabilities[] 구조입니다. 각 Result 의 Class 가 os-pkgs/lang-pkgs 를 가릅니다. 개수를 세는 것부터 시작해야 무엇이 급한지 가늠이 됩니다.

도달성과 범위를 붙인다

앱 취약점(lang-pkgs)에 도달 가능성과 범위를 붙여 /root/vuln/02-enriched.json 에 배열로 저장하세요. 각 항목은 id·pkg·severity·fixed(있으면 버전, 없으면 null)·scope(runtime/dev, /opt/fixtures/vulnmgmt/deps.json)·reachable(/opt/fixtures/vulnmgmt/callgraph.json 의 vulnerable_symbol)입니다. id 사전순.

두 픽스처를 합칩니다. deps.json 이 패키지의 scope 를, callgraph.json 의 vulnerable_symbol 이 CVE 별 reachable 을 알려 줍니다. 스캐너 리포트만으로는 이 둘을 알 수 없어서, 현장에서는 따로 붙입니다.

오늘 고칠 수 있는 것만

앱 취약점 중 fix 가 있는 것만 골라 /root/vuln/03-fixable.json 에 id 사전순 배열로 저장하세요. 각 항목은 id·pkg·from(설치 버전)·to(fixed)입니다. fix 가 없는 것은 뺍니다.

리포트의 InstalledVersion 과 FixedVersion 을 씁니다. FixedVersion 이 비어 있으면 오늘 올릴 판이 없는 것이라 계획에서 뺍니다 — 심각도와는 다른 축입니다.

진짜 급한 것을 가린다

/root/vuln/04-priority.json 에 우선순위를 now·later·watch 세 칸으로 나눠 저장하세요(각각 id 사전순 배열). 규칙: 배포에 실리고(runtime) 도달 가능하며 심각도가 HIGH/CRITICAL 이고 fix 가 있으면 now; 도달 불가거나 dev 전용이면 watch; 나머지(도달 가능하지만 MEDIUM 이하이거나 fix 없음)는 later.

02 단계의 enriched 를 그대로 분류하면 됩니다. CRITICAL 이라도 도달 불가면 now 가 아니라 watch 로 갑니다 — 이것이 '심각도 ≠ 우선순위' 의 핵심입니다. 세 칸의 합은 앱 취약점 전체와 같아야 합니다.

베이스를 올리고 다시 잰다

베이스 이미지를 올린 뒤의 리포트 /opt/fixtures/vulnmgmt/scan-newbase.json 와 처음 리포트를 견주어, OS 취약점이 얼마나 줄었는지를 /root/vuln/05-basediff.json 에 저장하세요. os_before·os_after(개수), removed(사라진 OS CVE id 사전순), remaining(남은 OS CVE id 와 그 이유 — fix 가 없으면 "no-fix")를 담습니다.

두 리포트의 os-pkgs 결과를 비교합니다. fix 가 있던 OS 취약점은 새 베이스에서 사라지고, fix 가 없던 것(expat)은 남습니다. 베이스 교체가 OS 패치를 한꺼번에 해결하는 흔한 수단임을 보여 주는 단계입니다.

근거 있는 예외를 쓴다

도달 불가로 판정한 취약점에 대해 OpenVEX 문서를 /root/vuln/06-vex.json 에 쓰세요. OpenVEX 형식(@context·@id·author·timestamp·statements[])을 따르고, 각 statement 는 vulnerability.name·products·status·justification·impact_statement 를 가집니다. pyyaml CVE 를 not_affected / vulnerable_code_not_in_execute_path 로 적습니다.

OpenVEX 는 '이 취약점이 우리 제품에 실제로 영향을 주는가' 를 기계가 읽을 수 있게 적는 문서입니다(https://github.com/openvex/spec). status 가 not_affected 이면 justification 이 반드시 있어야 합니다 — 근거 없는 예외는 관문이 거부합니다.

관문을 코드로 쓴다

/root/vuln/gate.py 를 쓰세요. python3 /root/vuln/gate.py <scan.json> <vex.json> 로 부르면, 배포에 실리고(runtime) 도달 가능하고 심각도 HIGH/CRITICAL 인 앱 취약점 가운데 근거 있는 VEX(not_affected + justification)로 덮이지 않은 것을 blocking 으로 모아 JSON 으로 내고, 막을 것이 있으면 1 로 끝납니다. 그다음 /opt/fixtures/vulnmgmt/scan-old.json 과 /root/vuln/06-vex.json 으로 돌린 출력을 /root/vuln/07-verdict.json 에 저장하세요.

관문은 02·04 의 판단을 프로그램으로 옮긴 것입니다. VEX 가 not_affected 로 덮되 justification 이 있어야 인정합니다 — justification 없는 not_affected 는 무시해 근거 없는 예외가 통하지 않게 합니다. 도달성과 scope 는 deps.json·callgraph.json 에서 읽습니다.