LLM WikiAccess-protected knowledge portal
🔒 내부용 · 설계 단계(미구현) · raw 수집 PoC까지만 존재

OpenSSF 악성 패키지(MAL) RAW 수집 (2026)

OpenSSF malicious-packages를 독립 RAW 자산으로 수집하는 파이프라인 — 새 팀원 온보딩 상세

0한눈에

1왜 이렇게 하나

악성 패키지 정보는 스캐너/배포로 흘러가는 1차 보안 데이터다. 그런데 악성 패키지는 레지스트리에서 빠르게 takedown되고, advisory도 사후 정정된다. 그래서 (1) 원본을 무손실로 먼저 확보하고(독립 수집), (2) 정정·삭제는 물리 삭제가 아니라 상태 전이로 다뤄 배포 동기화가 깨지지 않게 하는 것이 핵심이다.

OS 배포판(Ubuntu/RHEL 등)은 0건이다. OpenSSF malicious-packages는 언어 레지스트리(npm/PyPI 등) 전용이고, OS 배포판 보안 피드는 멀웨어가 아니라 취약점(CVE)이라 포함되지 않는다.

2데이터 형태 — 먼저 알아야 할 3가지

affected vs database_specific. affected는 정규화된 결론(매핑에 사용), database_specific.malicious-packages-origins출처·감사 추적이다. 여러 피드가 같은 패키지를 신고해도 affected는 1개로 머지되고 근거는 origins에 N개로 남는다.

ranges vs versions. 범위 표현과 명시적 버전 목록이 둘 다 쓰인다.

형태건수비율
ranges만 (대개 introduced:0=전 버전)203,35689.6%
versions만 (특정 버전)16,7047.4%
둘 다7,0003.1%

③ 상한 있는 케이스는 희소하지만 중요. fixed 33건, last_affected 128건. "정상 패키지가 특정 버전만 탈취됐다 수정된" 고가치 케이스(예: pyphetools [0.9.120,0.9.121))다.

주의: versions는 불연속이 많다(예: easyascii 2.4.1, 2.4.5, 3.2.1…). 절대 min~max 단일 구간으로 합치지 말 것 — 정상 버전을 악성으로 오탐하게 된다.

3RAW 수집 흐름

  1. 최초 git clone / 이후 git pull. repo 자체가 raw 저장소(osv/malicious/<eco>/<pkg>/MAL-*.json).
  2. data/manifest.jsonhead_commit·생태계별 카운트 기록.
  3. (설계) 파싱 → RANGE 변환 → PRODUCT_KEY 산출(npm=사내 해시, 그 외 평문) → 적재.

현재 PoC: ~/labrador/tool/openssf-malicious/collect.py (raw 확보까지, DB 미연동).

RANGE 변환 규칙VULN_RANGE| 구분 토큰(구간/단일버전)의 합집합.

입력변환
introduced:0(,)
introduced:X[X,)
introduced:A + fixed:B[A,B)
introduced:A + last_affected:B[A,B]
versions:[…] (불연속)v1|v2|v3 (합치기 금지)
range 여러 개 (한 product, 747건)union 단순화 ((,) 있으면 (,))

ecosystem 정규화 — OSV→내부 대문자 코드: npm→NPM, PyPI→PYPI, Packagist→COMPOSER, crates.io→CARGO, Go→GOLANG, Maven→MAVEN 등. 케이싱 불일치(pypi/PYPI, Cargo/CARGO)는 조인 시 UPPER()로 흡수. vscode/git은 매핑 없음 → 스킵+로그.

4증분 — 핵심: 폐기는 삭제가 아니다

advisory는 사후 수정된다. 예: MAL-xxxx-xx가 처음엔 패키지 A를 악성으로 지목했다가, 재분석 후 패키지 B로 정정될 수 있다.

  1. git diff로 직전 head_commit 대비 변경된 MAL 파일만 추출.
  2. 변경 advisory 재파싱 → 현재 (PRODUCT_KEY, REPOSITORY, RANGE) 행 집합 계산.
  3. 같은 VULN_ID 기존 행과 비교: 새 행 → INSERT(DEPRECATED=0) / 사라진 행(A) → DEPRECATED=1.
  4. advisory 파일 자체 삭제 → 해당 VULN_ID 행 전부 DEPRECATED=1.
  5. head_commit 저장 → 멱등(같은 commit 재처리 시 무변화).
왜 물리 삭제 금지: 수집 DB 데이터는 배포(distribution) DB로 동기화되어 흘러간다. 행을 지우면 sync에서 누락·정합 문제가 난다. 그래서 A 행은 지우지 않고 DEPRECATED=1상태만 전이한다.

기준점(prev) 복구. prev는 최초 NULL이거나 upstream history rewrite/shallow로 사라질 수 있다. 유효성은 "존재"가 아니라 HEAD의 조상인지(git merge-base --is-ancestor)로 판정한다. 무효 시 stats의 commit_time 이하(≤)의 가장 최근 commit을 base로 잡는다(반드시 내림 — 올려잡으면 데이터 유실). 둘 다 실패하면 full reconcile.

full reconcile (안전망): "활성이어야 하는 것"의 진짜 기준은 diff가 아니라 현재 HEAD 트리다. repo 전체 MAL 집합 vs DB 활성 행(DEPRECATED=0)을 통째로 대조(repo만 있음→INSERT / DB만 있음→DEPRECATED=1). 22.7만 건이라 감당 가능, 주기 실행 시 드리프트 self-heal. 상태는 TB_CRAWLER_STATUS TYPE='OPENSSF_MAL'{commit_id, commit_time}(committer date, TZ ISO8601).

5DEPRECATED 컬럼 + 활성 뷰

DEPRECATED TINYINT0 = 활성, 1 = 폐기. 소비자/배포는 원본 테이블이 아니라 활성 뷰만 조회한다.

CREATE VIEW VIEW_COMP_LIB_MAL AS
SELECT VULN_ID, REPOSITORY, NAME, PRODUCT_KEY, VULN_RANGE, SOURCE_URL,
       RECORD_CREATED, RECORD_UPDATED
FROM TB_COMP_LIB_MAL
WHERE DEPRECATED = 0;

폐기 이력은 원본에 남아 감사·복원이 가능하고, 다운스트림은 "현재 유효한 악성 매핑"만 단순 조회한다.

6아직 안 정해진 것 (Open Questions)

근거: ~/labrador/tool/openssf-malicious · 설계 결정 ai/decisions/2026-06-15-malicious-package-openssf-collection.md · 정본 human/reports/2026-malicious-package-collection.md