상태: 설계 단계 / 미구현 (2026-06-15). 코드는 raw 수집 PoC(
collect.py)까지만 존재하며, DB 적재·증분·뷰는 본 문서의 설계안이다. 대상 독자: 데이터파트 엔지니어 (수집기 이해·운영).
0. 한눈에
- 대상: OpenSSF
ossf/malicious-packages— 악성 패키지 advisory를 OSV JSON 포맷
(MAL-YYYY-NNNN)으로 게시하는 공개 저장소.
- 방향(방안 1: 독립 수집): 우리 라이브러리 인벤토리(
TB_COMP_LIB_PRODUCT_*)와
즉시 조인하지 않고, 독립 RAW 자산으로 전량 수집한다. 인벤토리 매핑은 이후 레이어로 분리(역방향보다 데이터 유실이 없음).
- 수집 실측(2026-06-15): 227,060건 / 896MB. npm 94%, OS 배포판 생태계는 없음.
- RANGE: OSV
ranges/versions를 기존 사내 구간 표기((,),[a,b)등)로 변환. - 증분:
git pulldiff 기반. 바뀐 패키지/버전은 **물리 삭제하지 않고DEPRECATED
플래그로 소프트삭제하며, 소비/배포는 활성 뷰**만 본다.
1. 소스와 데이터 형태
- 저장소:
ossf/malicious-packages(기본 브랜치main). - 경로 규칙:
osv/malicious/<ecosystem>/<package>/MAL-YYYY-NNNN.json. - 한 advisory = OSV JSON 1개. repo 자체가 사실상 raw 데이터셋이다.
1.1 생태계 분포 (수집 실측)
| ecosystem | 건수 | ecosystem | 건수 | |
|---|---|---|---|---|
| npm | 213,926 | go | 18 | |
| pypi | 11,340 | vscode(+open-vsx) | 20 | |
| rubygems | 970 | crates.io | 9 | |
| nuget | 773 | maven | 2 | |
| packagist / git | 1 / 1 |
→ npm이 절대다수(94%). Ubuntu/RHEL/Debian 등 OS 배포판은 0건 — OpenSSF malicious-packages는 언어 레지스트리 전용이고, OS 배포판 보안 피드는 멀웨어가 아니라 취약점(CVE)이라 여기 포함되지 않는다.
1.2 affected vs database_specific (헷갈리기 쉬움)
| 영역 | 정체 | 용도 |
|---|---|---|
affected[] | 정규화된 결론 (OSV 표준) | 매핑/적재는 이걸 사용 |
database_specific.malicious-packages-origins | 출처·감사 추적 (OpenSSF 전용) | 신뢰도(다중 출처)·중복제거·증분 |
affected[].database_specific | 해당 패키지 분류 정보 | cwes(대개 CWE-506), 원본 URL |
여러 피드(ghsa-malware, amazon-inspector, osv 등)가 같은 패키지를 신고해도 affected는 하나로 머지되고, 근거는 origins에 N개로 남는다. (예: MAL-2026-887 pyrefly — affected 1개, origins 2개)
1.3 버전 표현: ranges vs versions (수집 실측)
| 형태 | 건수 | 비율 |
|---|---|---|
ranges만 (대부분 introduced:0 = 전 버전) | 203,356 | 89.6% |
versions만 (특정 버전 목록) | 16,704 | 7.4% |
| 둘 다 | 7,000 | 3.1% |
- 둘 다인 경우:
ranges는 규칙,versions는 그 규칙을 실제 출시 버전에 풀어 쓴
목록 → 충돌 아님, 일관됨.
- 상한 있는 케이스(희소하지만 중요):
fixed33건,last_affected128건,
introduced≠0 1,709건. 이게 "정상 패키지가 특정 버전만 탈취됐다 수정된" 고가치 케이스(예: pyphetools [0.9.120, 0.9.121))다 — 버리면 안 된다.
- ⚠️
versions는 불연속이 많다(예:easyascii2.4.1, 2.4.5, 3.2.1…). 절대
min~max 단일 구간으로 합치지 말 것 — 정상 버전을 악성으로 오탐하게 된다.
2. RAW 수집 방식
2.1 수집 메커니즘
- 최초:
git clone. 이후:git fetch + reset(=pull). repo가 raw 저장소. data/manifest.json에head_commit, 생태계별 카운트, 수집 시각 기록.- PoC:
~/labrador/tool/openssf-malicious/collect.py(DB 미연동, raw 확보까지).
2.2 적재 스키마
RAW 원본은 git working copy로 보존(repo 자체가 raw 저장소)하므로 DB에는 RAW_DATA나 별도 _RAW 테이블을 두지 않는다. _RAW/V1 2단 분리 없이 단일 TB_COMP_LIB_MAL + 활성 뷰로 간다.
CREATE TABLE TB_COMP_LIB_MAL (
VULN_ID VARCHAR(20) NOT NULL, -- MAL-YYYY-NNNN
REPOSITORY VARCHAR(30) NOT NULL, -- NPM / PYPI / COMPOSER / CARGO ... (대문자 정규, §2.4)
NAME VARCHAR(330) NOT NULL, -- 평문 패키지명 (affected.package.name)
PRODUCT_KEY VARCHAR(330) NOT NULL, -- npm=사내 해시키, 그 외=평문
VULN_RANGE TEXT NOT NULL, -- '|' 구분 토큰(구간/단일버전)의 합집합 (§2.3)
DEPRECATED TINYINT NOT NULL DEFAULT 0, -- 0=활성, 1=폐기(소프트삭제)
SOURCE_URL VARCHAR(400) NULL,
RECORD_CREATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
RECORD_UPDATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (VULN_ID, REPOSITORY, PRODUCT_KEY),
KEY idx_active (DEPRECATED, REPOSITORY)
);- PK는
(VULN_ID, REPOSITORY, PRODUCT_KEY)로 충분하고VULN_RANGE는 키에서 제외한다.
근거(수집본 227,060건 실측): 한 advisory의 affected는 항상 1개 패키지이고(다중 0건), 같은 product 중복도 0건 → (advisory, product)가 유일. 한 product에 range가 여러 개인 경우(747건)는 별도 행이 아니라 한 VULN_RANGE 셀에 합쳐 담는다(§2.3).
ALIASES(GHSA/RUSTSEC),SOURCE_FEEDS(다중 출처 신뢰도),PURL은 기본 제외 — git
raw에 다 있어 필요 시 재추출/추가 가능.
PRODUCT_KEY: npm만 사내 해시(암호화) 적용, 그 외 생태계는 평문 키. npm 해시는
패키지명에서 결정적으로 생성되므로 인벤토리 조회 없이도 계산 가능. (해시 로직 위치는 Open Question)
2.2.1 advisory 메타 테이블 TB_COMP_LIB_MAL_INFO
summary/details/references/aliases는 패키지가 아니라 advisory 단위 정보라 별도 테이블에 둔다. PK는 VULN_ID(advisory 1행).
CREATE TABLE TB_COMP_LIB_MAL_INFO (
VULN_ID VARCHAR(20) NOT NULL, -- MAL-YYYY-NNNN (MAL과 동일 ID)
SUMMARY TEXT NULL, -- osv.summary
DETAILS TEXT NULL, -- osv.details
MAL_REFERENCES JSON NULL, -- osv.references [{type,url}] (REFERENCES는 예약어)
ALIASES JSON NULL, -- osv.aliases [GHSA-..., RUSTSEC-...]
DEPRECATED TINYINT NOT NULL DEFAULT 0,
RECORD_CREATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
RECORD_UPDATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (VULN_ID),
KEY idx_active (DEPRECATED)
);
CREATE VIEW VIEW_COMP_LIB_MAL_INFO AS
SELECT VULN_ID, SUMMARY, DETAILS, MAL_REFERENCES, ALIASES,
RECORD_CREATED, RECORD_UPDATED
FROM TB_COMP_LIB_MAL_INFO WHERE DEPRECATED = 0;- MAL과 동일 라이프사이클: 같은
run_ts로 upsert, 증분 시 변경VULN_ID를
TB_COMP_LIB_MAL/TB_COMP_LIB_MAL_INFO 양쪽 deprecate→재upsert, full sweep도 양쪽.
- MAL_INFO 행은 MAL 행이 생긴 advisory에 대해서만 생성(매핑 없는 vscode/git 스킵분 제외 → 정합).
- 같은 컨테이너/스크래퍼가 두 테이블을 함께 적재(DAG·이미지 변경 없음).
- 재적재(백필):
full_reconcile=true로 실행하면 전 advisory 재파싱하며 MAL_INFO도 채워진다.
2.3 RANGE 변환 규칙 (OSV → 사내 표기)
VULN_RANGE는 |(파이프) 구분 토큰의 합집합으로 정의한다. 각 토큰은 구간([a,b)) 또는 단일 버전이다. (|는 구간·버전 문자열에 나타나지 않아 구분자로 안전)
| 입력 | 변환 | 비고 | |||
|---|---|---|---|---|---|
introduced:0 | (,) | 89% | |||
introduced:X (상한 없음) | [X,) | 1,709건 | |||
introduced:A + fixed:B | [A,B) | 33건 — 처리 필수 | |||
introduced:A + last_affected:B | [A,B] | 128건 | |||
versions:[v1,v2,…] (불연속) | `v1\ | v2\ | v3` | min~max 합치기 금지, `\ | `로 나열 |
| range 여러 개 (한 product) | union 단순화 | 747건. (,) 있으면 (,), 없으면 disjoint를 `[a,b)\ | [c,d)` |
- range가 여러 개인 747건은 대부분 여러 출처(origins)가 겹치는 범위를 신고한 것이라
대개 introduced:0을 포함 → (,) 하나로 접힌다. 그냥 이어붙이지 말고 union할 것.
- OSV 원문은 git working copy(raw 저장소)에 그대로 남아 무손실이며, DB에는 변환 결과만 적재한다.
2.4 ecosystem 정규화 (OSV → 내부 코드)
OSV ecosystem 명칭은 우리 내부 명칭과 다르다(예: Packagist→COMPOSER, crates.io→CARGO). 게다가 내부 명칭 자체가 테이블마다 케이싱이 일관되지 않다 (PyPI: product PYPI / vuln pypi, Rust: 테이블명 CARGO / product REPOSITORY Cargo). 따라서 MAL REPOSITORY은 대문자 정규 코드(vuln 테이블 suffix)로 통일하고, 조인 시 UPPER()로 케이싱 차이를 흡수한다.
| OSV ecosystem (raw) | 내부 REPOSITORY(정규) | LANGUAGE | 조인 시 실제 REPOSITORY |
|---|---|---|---|
npm | NPM | javascript | NPM |
PyPI | PYPI | python | product PYPI / vuln pypi ⚠️ |
RubyGems | RUBYGEMS | ruby | RUBYGEMS |
NuGet | NUGET | dotnet | NUGET |
Go | GOLANG | go | Golang ⚠️ |
crates.io | CARGO | rust | product Cargo ⚠️ (테이블명 CARGO) |
Maven | MAVEN | java | MAVEN |
Packagist | COMPOSER | php | COMPOSER |
vscode, vscode:open-vsx.org | — (매핑 없음) | — | VS Code 확장, 미수집 |
git | — (매핑 없음) | — | GIT range, 패키지 아님 |
규칙:
REPOSITORY은 위 대문자 정규 코드로 적재하고LANGUAGE도 동반 저장한다.- 케이싱 불일치는 우리 데이터 문제이므로 조인은
UPPER(REPOSITORY)비교로 흡수한다. vscode/vscode:open-vsx.org/git은 **매핑 대상이 없으므로 적재 스킵 + 로그로
명시**한다(조용히 버리지 않음). MAL 합계 21건.
구현 참고용 매핑(예시):
OSV_TO_INTERNAL = {
"npm": ("NPM", "javascript"),
"PyPI": ("PYPI", "python"),
"RubyGems": ("RUBYGEMS", "ruby"),
"NuGet": ("NUGET", "dotnet"),
"Go": ("GOLANG", "go"),
"crates.io": ("CARGO", "rust"),
"Maven": ("MAVEN", "java"),
"Packagist": ("COMPOSER", "php"),
# 매핑 없음 → 스킵 + 로그: "vscode", "vscode:open-vsx.org", "git"
}3. 증분 처리 (핵심 설계)
3.1 흐름
git pull후, 직전head_commit대비 변경 파일 추출:
git diff --name-only <prev>..<head> -- osv/malicious/.
- 변경된 각
MAL-*.json재파싱 → **현재 advisory가 가리키는
(PRODUCT_KEY, REPOSITORY, VULN_RANGE) 행 집합**을 계산.
- 같은
VULN_ID의 기존 행과 비교(reconcile):
- 지금 있고 과거 없음 → INSERT (DEPRECATED=0). - 과거·현재 모두 있음 → 유지(DEPRECATED=0, RECORD_UPDATED 갱신). - 과거 있고 지금 없음 → DEPRECATED=1 (소프트삭제, 물리 삭제 X).
- advisory 파일 자체가 repo에서 삭제됨 → 해당
VULN_ID행 전부DEPRECATED=1. - 처리 완료 후
head_commit을 상태로 저장 → 멱등(같은 commit 재처리 시 무변화).
3.2 왜 "패키지가 바뀌면" 폐기가 필요한가
advisory는 사후 수정된다. 예: MAL-xxxx-xx가 처음엔 패키지 A를 악성으로 지목했다가, 재분석 후 패키지 B로 정정될 수 있다. 그러면:
- 새 상태 = B → 활성(
DEPRECATED=0)으로 추가 - 옛 상태 = A → 더 이상 유효하지 않으므로 제거 대상
3.3 왜 물리 삭제하지 않는가
수집 DB의 데이터는 배포(distribution) DB로 동기화되어 흘러간다. 행을 물리 삭제하면 배포 동기화에서 누락·정합 문제가 생긴다. 그래서 A 행을 지우는 대신 DEPRECATED=1로 상태만 바꿔 폐기로 표시한다. 삭제가 아니라 상태 전이이므로 sync가 안전하게 전파한다.
3.4 증분 기준점(prev) 복구
prev(직전 head_commit)는 두 가지로 못 쓸 수 있다: ① 최초 실행이라 NULL, ② 존재했지만 upstream history rewrite/force-push/squash 또는 shallow clone으로 그 commit이 사라짐. 후자에서 git diff prev..HEAD가 실패하거나 엉뚱해진다.
기준점 유효성은 "존재"가 아니라 "HEAD의 조상인가"로 판정한다:
git cat-file -e "$prev^{commit}" 2>/dev/null \
&& git merge-base --is-ancestor "$prev" HEAD # 둘 다 통과해야 정상 diffcommit_time 폴백 (prev가 무효일 때): stats에 저장한 commit_time(committer date) 이하(≤)의 가장 최근 commit을 base로 잡는다 — 반드시 내림(round down).
base=$(git rev-list -1 --before="$commit_time" origin/HEAD)
git diff --name-only "$base"..HEAD -- osv/malicious/- 내려잡으면 그 지점부터 다시 보므로 빠지는 파일 없음(중복 재처리는 reconcile이
멱등이라 무해). 올려잡으면 사이 변경분을 놓쳐 데이터 유실 → 절대 금지.
의사결정 흐름
prev == NULL → 전체 스캔 INSERT (최초)
prev 가 HEAD의 조상 → git diff prev..HEAD (빠른 경로)
prev 무효 & commit_time 있음 → base=(≤commit_time 최신); diff base..HEAD
그 외 (둘 다 실패) → full reconcile (§3.5)3.5 full reconcile (최종 안전망)
"무엇이 활성이어야 하는가"의 진짜 기준은 diff가 아니라 현재 HEAD 체크아웃 트리다. diff는 "어디만 볼지" 최적화일 뿐이다. 따라서 기준점을 못 찾는 최악의 경우엔 전량 대조한다:
- repo의 전체 MAL 파일 집합 vs DB 활성 행(
DEPRECATED=0)을 통째로 비교:
- repo엔 있고 DB엔 없음 → INSERT (DEPRECATED=0) - DB엔 있고 repo엔 없음 → DEPRECATED=1
- 22.7만 건 set 비교라 비용 감당 가능, 무조건 정확.
- 폴백일 뿐 아니라 주기적(예: 주 1회) 실행 시 diff가 놓친 드리프트까지 self-heal 한다.
3.6 증분 상태(stats) 메타
기존 TB_CRAWLER_STATUS 컨벤션을 따라 TYPE='OPENSSF_MAL' 행에 메타를 둔다.
commit_id— 처리 완료 시점의 HEAD commit.commit_time— 그 commit의 committer date, TZ 포함 ISO8601(예:2026-06-15T00:00:42Z).
author date 아님.
- (선택) 매 실행 처리 건수(insert / deprecate)도 남겨 관측성 확보.
4. DEPRECATED 컬럼 + 활성 뷰
DEPRECATED TINYINT— 0 = not deprecated(활성), 1 = deprecated(폐기).- 소비자/배포는 원본 테이블이 아니라 활성 뷰만 본다.
-- 활성(미폐기) 악성 매핑만 노출
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;→ 폐기 이력은 원본 테이블에 남아 감사/복원이 가능하고, 다운스트림은 "현재 유효한 악성 매핑"만 단순 조회한다.
4.5 동작 시퀀스
초기 수집 (최초 실행 또는 full_reconcile=true)
sequenceDiagram
participant DAG as Airflow DAG
participant Pod as K8s Pod
participant PVC as PVC /resources
participant GH as OpenSSF repo
participant ST as TB_CRAWLER_STATUS
participant MAL as TB_COMP_LIB_MAL
participant V as VIEW_COMP_LIB_MAL
DAG->>DAG: check_running_instances
DAG->>DAG: push_db_url_to_xcom, db_url
DAG->>Pod: run scraper openssf_mal
Pod->>MAL: initialize, CREATE TABLE/VIEW
Pod->>PVC: ensure_repo, .git 없음
Pod->>GH: git clone 약 896MB
GH-->>PVC: working copy = raw 저장소
Pod->>Pod: head_commit, head_time, run_ts
Pod->>ST: load prev state TYPE=OPENSSF_MAL
ST-->>Pod: 없음 → FULL 모드
Pod->>PVC: list_all_files 약 227k
loop batch 2000 scrape_data
Pod->>PVC: 파일 read + 파싱
Note over Pod: REPOSITORY 정규화, npm AES, RANGE 합집합
Pod->>MAL: UPSERT DEPRECATED=0, RECORD_UPDATED=run_ts
end
Pod->>MAL: SWEEP, RECORD_UPDATED 이전 행 DEPRECATED=1 (최초 0건)
Pod->>ST: UPSERT commit_id, commit_time
V-->>V: 소비/배포는 DEPRECATED=0 만 조회증분 (이후 매일 실행)
sequenceDiagram
participant DAG as Airflow DAG
participant Pod as K8s Pod
participant PVC as PVC /resources
participant GH as OpenSSF repo
participant ST as TB_CRAWLER_STATUS
participant MAL as TB_COMP_LIB_MAL
DAG->>Pod: run, PVC 재마운트 repo 유지
Pod->>PVC: ensure_repo, .git 있음
Pod->>GH: git fetch + reset --hard origin/HEAD
GH-->>PVC: 변경분 반영, 새 HEAD
Pod->>ST: load prev state, prev commit_id/time
alt prev 가 HEAD의 조상 is-ancestor
Pod->>PVC: git diff prev..HEAD, 변경 파일만
else prev 무효 그리고 commit_time 있음
Pod->>PVC: base = commit_time 이하 최신, diff base..HEAD
else 둘 다 실패 또는 full_reconcile
Pod->>PVC: list_all_files 전체, FULL
end
loop 변경 advisory batch scrape_data
Pod->>MAL: 변경 VULN_ID DEPRECATED=1 일괄 폐기
Pod->>PVC: 현재 파일 파싱
Pod->>MAL: 현재 행 UPSERT DEPRECATED=0 재활성/정정
Note over MAL: A에서 B로 정정 시 A 폐기 유지, B 활성<br/>삭제 advisory도 폐기 유지, 물리삭제 안 함
end
Pod->>ST: UPSERT commit_id, commit_time, HEAD로 갱신핵심: PVC가 증분의 전제(clone 1회 후 pull), 폐기는 삭제가 아님(상태 전이로 배포 sync 보호), FULL은 전역 sweep, 증분은 변경 VULN_ID만 폐기 후 재upsert.
5. 운영 / 다음 단계
- 실행(현재 PoC):
cd ~/labrador/tool/openssf-malicious && python3 collect.py - 산출물:
data/malicious-packages/(raw 저장소),data/manifest.json(증분 상태)
Open Questions
- ~~npm
PRODUCT_KEY해시 생성 로직 위치~~ → 해소: AES-CBC,
base64(AES-CBC(pkg_name)), key=IV=b'NPMNPMNPMNPMNPMN', ai/labradorlabs/util/aesCbc.py(deterministic). npm만 이 변환 적용.
- takedown 패키지의 인벤토리 정합률 정량화(매핑 레이어 단계).
- 배포(distribution) sync가
DEPRECATED상태 전이를 어떻게 소비할지 연동 방식.
용어
- MAL: OpenSSF/OSV의 악성 패키지 advisory ID (
MAL-YYYY-NNNN). - OSV: Open Source Vulnerabilities 스키마. OpenSSF가 이 포맷으로 게시.
- ecosystem: 패키지 생태계(npm, PyPI, Maven …).
- affected / database_specific: 결론 / 출처(§1.2).
- DEPRECATED: 소프트삭제 플래그(0 활성 / 1 폐기).
근거: ~/labrador/tool/openssf-malicious(collect.py, manifest.json), 설계 결정 ai/decisions/2026-06-15-malicious-package-openssf-collection.md.