# OpenSSF Malicious Package (MAL) 수집·매핑 방안
AI Summary
Purpose:
- Decide how to collect OpenSSF
malicious-packages(OSVMAL-YYYY-NNNN) advisories
into gatheringdb and map them, with version ranges, onto the existing per-ecosystem vuln-mapping tables (TB_COMP_LIB_VULN_<eco>_V1).
Key points:
- Source =
ossf/malicious-packagesGitHub repo, OSV JSON format. Git-based, so
incremental raw collection is natural.
- The OSV affected-package shape (ecosystem / name / ranges / id) maps 1:1 onto
the existing (PRODUCT_KEY, VULN_RANGE, VULN_ID) model.
- MAL is ~94% npm, plus PyPI/RubyGems/NuGet; effectively no OS-distro coverage.
PRODUCT_KEYderivation differs per ecosystem: npm uses an internal hashed key;
others (Maven groupId:artifactId, PyPI/RubyGems/crates names) are plain.
- Two options documented: (A) merge into existing CVE vuln tables, (B) dedicated
MAL tables managed separately from CVE. User prefers B (separate from CVE).
Relevant when:
- Designing/implementing the MAL collector and the vuln-mapping ingestion.
- Deciding schema for malicious-package data in gatheringdb.
Do not read full document unless:
- You are implementing the collector, the range converter, or the schema DDL.
Linked documents:
ai/workspace/repos.md(crawler-lib-* repos own the per-ecosystem PRODUCT_KEY logic)
Open Questions
- ~~npm
PRODUCT_KEY해시 생성 로직 위치~~ → 해소: AES-CBC,base64(AES-CBC(pkg_name)),
key=IV=b'NPMNPMNPMNPMNPMN', ai/labradorlabs/util/aesCbc.py(deterministic).
- takedown된 악성 패키지(인벤토리에 product 미존재) 비율은? → V1 정합률 정량화 필요
- MAL
VULN_ID(MAL-...)를 다운스트림이 CVE와 어떻게 구분/소비할 것인가? - 증분 기준: git commit diff vs OSV
modified타임스탬프 중 무엇을 신뢰원으로 둘지?
Details
작성일 2026-06-15. 본문(방안 비교)은 검토·결정용이라 한국어로 작성. 표/예시는 실제
gatheringdb에 적재된 MAL 레코드와ossf/malicious-packages원본에서 발췌한 것이며 임의로 만들지 않았다.
1. 배경과 목표
- 목표: OpenSSF
malicious-packages(OSV 포맷,MAL-YYYY-NNNN)를 직접 수집해서,
버전 RANGE까지 포함한 형태로 라이브러리 생태계별 취약 매핑 테이블 구조 (TB_COMP_LIB_VULN_<eco>_V1)에 맞춰 적재한다.
- 요구사항:
- 증분 raw 수집 가능 - 버전 RANGE 매핑 (단일 버전이 아니라 affected 구간) - CVE와는 별도로 관리 (사용자 결정)
- 소스:
https://github.com/ossf/malicious-packages— 각 advisory가
osv/malicious/<ecosystem>/<package>/MAL-YYYY-NNNN.json 경로에 OSV JSON으로 존재. git 기반이라 pull diff 또는 OSV modified 타임스탬프로 증분이 자연스럽다.
2. OpenSSF/OSV MAL 데이터 메타 예시 (실데이터)
대표 레코드 (현재 DB 적재본 기준):
| VULN_ID | ecosystem | package | purl | summary |
|---|---|---|---|---|
MAL-2021-1 | npm | cxp-jquery | pkg:npm/cxp-jquery | Malicious code in cxp-jquery (npm) |
MAL-2022-1 | crates.io | rustdecimal | pkg:cargo/rustdecimal | Malicious code in rustdecimal (crates.io) — rust_decimal 타이포스쿼팅 |
MAL-2026-5772 | npm | npx-whoami-demo | pkg:npm/npx-whoami-demo | Malicious code in npx-whoami-demo (npm) |
MAL-2026-5773 | PyPI | generatellm | pkg:pypi/generatellm | Malicious code in generatellm (PyPI) |
MAL-2026-5768 | PyPI | bash8 | pkg:pypi/bash8 | Malicious code in bash8 (PyPI) |
원본 OSV JSON 한 건 전체 (MAL-2022-1, rustdecimal):
{
"id": "MAL-2022-1",
"summary": "Malicious code in rustdecimal (crates.io)",
"aliases": ["GHSA-7pwq-f4pq-78gm", "RUSTSEC-2022-0042"],
"details": "... 타이포스쿼팅 분석 (rust_decimal 사칭, crates.io에서 영구 삭제) ...",
"affected": [
{
"package": {
"name": "rustdecimal",
"purl": "pkg:cargo/rustdecimal",
"ecosystem": "crates.io"
},
"ranges": [
{ "type": "SEMVER", "events": [ { "introduced": "0" } ] }
],
"database_specific": {
"source": "https://github.com/ossf/malicious-packages/blob/main/osv/malicious/crates.io/rustdecimal/MAL-2022-1.json"
}
}
],
"references": [
{ "type": "ADVISORY", "url": "https://github.com/advisories/GHSA-7pwq-f4pq-78gm" },
{ "type": "WEB", "url": "https://rustsec.org/advisories/RUSTSEC-2022-0042.html" }
],
"published": "2022-08-11T15:43:35Z",
"modified": "2023-11-08T04:16:56Z",
"schema_version": "1.7.3",
"database_specific": {
"malicious-packages-origins": [
{
"id": "GHSA-7pwq-f4pq-78gm",
"source": "ghsa-malware",
"sha256": "2e33f42f05c60c6d9f9297bae15a43d6c445e2ad0fd67fa4ef144e5cc79d09c7",
"ranges": [ { "type": "SEMVER", "events": [ { "introduced": "0" } ] } ],
"import_time": "2023-07-30T21:57:59Z",
"modified_time": "2023-01-07T05:08:16Z"
}
]
}
}수집·매핑에 쓰이는 핵심 필드:
| OSV 필드 | 의미 | 매핑 대상 |
|---|---|---|
id | MAL-YYYY-NNNN | VULN_ID |
affected[].package.ecosystem | 생태계 | 테이블/컬럼 선택 |
affected[].package.name | 패키지명 | NAME → PRODUCT_KEY |
affected[].package.purl | 표준 식별자 | (보조 매핑 키) |
affected[].ranges[].events | affected 구간 | VULN_RANGE 변환 입력 |
aliases | GHSA/RUSTSEC 등 | 참조용 (별도 저장 선택) |
database_specific.source | 원본 advisory URL | SOURCE_URL |
modified / published | 시각 | 증분/감사 |
중요한 관찰: 악성 패키지는 보통 패키지 전체가 악성이라 거의 모든 MAL 레코드의 range가 events:[{introduced:"0"}](= 전 버전)다. 즉 변환 결과의 대다수는 VULN_RANGE = (,)가 된다.
3. 타깃 스키마 현황 (기존 vuln 테이블)
TB_COMP_LIB_VULN_<eco>_V1 (정합본):
| 컬럼 | 타입 | 비고 |
|---|---|---|
PRODUCT_KEY | varchar(330) PK | Maven=groupId:artifactId, npm=내부 해시키 |
VULN_ID | varchar(20) PK | MAL-... 들어감 (길이 OK) |
VULN_RANGE | text | 구간 표기 [a,b) / (,) |
MAPPED_TYPE | enum('RE','GA','GL','SN','SC','DE') | 소스 코드. OpenSSF용 값 없음 → 추가 필요 |
RECORD_CREATED/UPDATED | timestamp |
TB_COMP_LIB_VULN_<eco>_RAW (소스별 원본): 위 + LANGUAGE, REPOSITORY, NAME, SOURCE_URL, VULN_RANGE_HASH. 즉 기존 파이프라인은 RAW(소스별·NAME 보유) → 이름→PRODUCT_KEY 해석 → V1(product+vuln 정합) 흐름이다.
VULN_RANGE 실제 예: (,)(전 버전), (,0.15.0), [1.0.0,2.0.1), [4.0-M3,].
4. PRODUCT_KEY 처리 규칙
| 생태계 | PRODUCT_KEY 생성 | 비고 |
|---|---|---|
| npm | 내부 해시(암호화) 키 적용 | 우리가 만든 해시 규칙으로 평문 패키지명 → 해시키 변환 |
| Maven | groupId:artifactId (평문) | OSV name과 직접 매칭 |
| PyPI / RubyGems / crates.io / NuGet / Packagist / Go | 평문 패키지명(생태계 정규화 규칙) |
→ npm만 해시 변환 단계가 추가로 필요하고 나머지는 평문 키를 그대로 쓴다. takedown되어 product 인벤토리에 없는 패키지는 RAW까지만 남고 V1 승격은 보류한다 (기존 RAW→V1 철학 유지).
5. VULN_RANGE 변환 규칙 (OSV events → 구간 표기)
OSV events 배열을 정렬해 구간으로 환원한다.
VULN_RANGE는 |(파이프) 구분 토큰의 합집합 — 각 토큰은 구간([a,b)) 또는 단일 버전.
| OSV 입력 | VULN_RANGE | 의미 | |||
|---|---|---|---|---|---|
[{introduced:"0"}] | (,) | 전 버전 악성 (MAL 대다수) | |||
[{introduced:"0"},{fixed:"X"}] | (,X) | X 미만 | |||
[{introduced:"A"},{fixed:"B"}] | [A,B) | A 이상 B 미만 (33건) | |||
[{introduced:"A"},{last_affected:"B"}] | [A,B] | A 이상 B 이하 (128건) | |||
versions:[v1,v2,...] (불연속) | `v1\ | v2\ | v3` | min~max 합치기 금지, `\ | `로 나열 |
| range 여러 개 (한 product) | union 단순화 | 747건. (,) 있으면 (,), 없으면 `[a,b)\ | [c,d)` |
range.type이SEMVER/ECOSYSTEM이면 그대로,GIT이면 버전 매핑 불가 → 스킵/보류.- range 여러 개(747건)는 대개 출처 중복이라
(,)로 접힘 — 이어붙이지 말고 union. - 토큰 문자열 포맷은 기존 vuln 테이블과 동일하므로 변환기만 새로 만들면 된다.
5.5 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 | 내부 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 ⚠️ |
Maven | MAVEN | java | MAVEN |
Packagist | COMPOSER | php | COMPOSER |
vscode, vscode:open-vsx.org | — (매핑 없음) | — | VS Code 확장, 미수집 |
git | — (매핑 없음) | — | GIT range, 패키지 아님 |
규칙: ① 대문자 정규 코드 + LANGUAGE 동반 저장, ② 조인은 UPPER(REPOSITORY) 비교, ③ vscode/vscode:open-vsx.org/git은 매핑 대상 없음 → 적재 스킵 + 로그(조용히 버리지 않음, MAL 합계 21건).
6. 증분 raw 수집 전략
ossf/malicious-packagesgit clone, 이후git pull로 증분.- 변경 파일 추출:
git diff --name-only <last_sha>..HEAD -- osv/malicious/또는
전체 워크 후 OSV modified 비교.
- 각
MAL-*.json을 raw로 보존 (파일 경로 + sha256 +modified저장 → 재처리 판단). - raw → 파싱 → 생태계 분기 → RANGE 변환 → (npm 해시) → 매핑 테이블 적재.
- 마지막 처리 commit sha를 상태로 저장 (
TB_CRAWLER_STATUSTYPE='OPENSSF_MAL'에
{commit_id, commit_time}; commit_time은 committer date, TZ 포함 ISO8601).
기준점(prev) 복구. prev는 최초 NULL이거나, upstream history rewrite/force-push/ shallow clone으로 사라질 수 있다. 유효성은 "존재"가 아니라 HEAD의 조상인지로 판정: git cat-file -e prev^{commit} && git merge-base --is-ancestor prev HEAD.
prev == NULL → 전체 스캔 INSERT
prev 가 HEAD의 조상 → git diff prev..HEAD (빠른 경로)
prev 무효 & commit_time 있음 → base = (≤commit_time 최신 commit); diff base..HEAD
그 외 → full reconcile (안전망)commit_time폴백은 반드시 내림(≤) — 내려잡으면 빠짐 없음(중복은 멱등이라 무해),
올려잡으면 데이터 유실.
- full reconcile: 현재 HEAD 트리 전체 vs DB 활성 행(
DEPRECATED=0) 대조(repo만 있음
→INSERT / DB만 있음→DEPRECATED=1). 22.7만 건이라 감당 가능하고 무조건 정확. 폴백이자 주기 실행 시 드리프트 self-heal. "활성이어야 하는 것"의 진짜 기준은 diff가 아니라 HEAD 트리.
방안 A — 기존 CVE vuln 테이블에 합치기
기존 TB_COMP_LIB_VULN_<eco>_RAW/_V1에 MAL을 그대로 적재.
VULN_ID=MAL-...,MAPPED_TYPE에 OpenSSF 값(예:'OF') 추가(enum ALTER).- MAL 여부는
VULN_ID LIKE 'MAL-%'로만 구분.
장점
- 새 테이블/파이프라인 없이 기존 변환·해석 로직 재사용.
- 한 패키지의 CVE+MAL을 한 테이블에서 조회.
단점
- 악성과 취약이 한 테이블에 섞임 → 구분자가
MAL-접두어 하나뿐. - 다운스트림(스코어링/리포트)이 MAL을 CVE로 오인할 위험 → 모든 소비 쿼리 수정 필요.
- enum 등 기존 운영 테이블 스키마 변경 리스크.
- 사용자 요구("CVE와 따로 관리")와 어긋남.
방안 B — MAL 전용 테이블 (CVE와 분리) ✅ 선호
CVE 매핑과 물리적으로 분리한 전용 테이블. 생태계는 별도 컬럼(REPOSITORY)으로 둬서, 사용자가 처음 원했던 "ecosystem 컬럼(라이브러리=NPM/MAVEN…)" 형태를 그대로 만족. MAL 총량이 크지 않아(현재 ~22.7만, 94% npm) 단일 테이블 세트로 충분.
확정: RAW 원본은 git working copy로 보존(repo 자체가 raw 저장소)하므로 DB에 별도
_RAW/RAW_DATA를 두지 않는다._RAW/V12단 분리 없이 단일TB_COMP_LIB_MAL+ 활성 뷰로 간다. 생태계는REPOSITORY컬럼(대문자 정규 코드, §5.5).
CREATE TABLE TB_COMP_LIB_MAL (
VULN_ID VARCHAR(20) NOT NULL, -- MAL-YYYY-NNNN
REPOSITORY VARCHAR(30) NOT NULL, -- NPM / PYPI / COMPOSER / CARGO ... (대문자 정규)
NAME VARCHAR(330) NOT NULL, -- 평문 패키지명 (OSV affected.package.name)
PRODUCT_KEY VARCHAR(330) NOT NULL, -- npm=사내 해시키, 그 외=평문
VULN_RANGE TEXT NOT NULL, -- '|' 구분 토큰(구간/단일버전)의 합집합 (§5)
DEPRECATED TINYINT NOT NULL DEFAULT 0, -- 0=활성, 1=폐기(소프트삭제)
SOURCE_URL VARCHAR(400) NULL, -- database_specific.source
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)
);
-- 소비자·배포는 활성(미폐기) 행만 노출하는 뷰만 조회
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;- PK는
(VULN_ID, REPOSITORY, PRODUCT_KEY)로 충분 —VULN_RANGE/해시 키 불필요. 근거
(227,060건 실측): advisory당 affected는 항상 1패키지(다중 0건), product 중복 0건. 한 product에 range가 여러 개인 747건은 별도 행이 아니라 한 VULN_RANGE에 합쳐 담는다.
DEPRECATED(소프트삭제) + 뷰는 필수: 증분 시 정정으로 사라진 행(A→B의 A)을 물리
삭제하면 배포(distribution) sync 정합이 깨진다. 삭제 대신 DEPRECATED=1 상태 전이.
SOURCE_FEEDS(다중 출처 신뢰도)는 git raw에서 재추출 가능해 기본 스키마에서 제외.PURL도 동일.
추가 테이블 TB_COMP_LIB_MAL_INFO (2026-06-16): advisory 단위 메타 (summary/details/MAL_REFERENCES=osv.references/ALIASES=osv.aliases)를 PK=VULN_ID 별도 테이블에 수집. REFERENCES는 SQL 예약어라 MAL_REFERENCES로 명명. MAL과 동일 라이프사이클(같은 run_ts upsert, 변경 시 양쪽 deprecate→재upsert, full sweep 양쪽) + VIEW_COMP_LIB_MAL_INFO. MAL 행이 생긴 advisory에 대해서만 생성. 백필은 full_reconcile=true.
장점
- CVE와 완전 분리 → 소비처가 "악성"만 명확히 질의(
SELECT ... FROM VIEW_COMP_LIB_MAL). - 기존 운영 vuln 테이블/enum 변경 없음 (리스크 격리).
- 사용자가 처음 원한
REPOSITORY컬럼 모델을 그대로 구현, OS 축 확장 여지도 둠. - MAL 특유의 메타(aliases, malicious-packages-origins, takedown 여부)를 자유롭게 보강.
단점
- 신규 테이블 + 적재 파이프라인을 새로 만들어야 함.
- "한 패키지의 CVE+MAL 동시 조회"는 두 테이블 JOIN/UNION 필요 (단, 키 규칙은 동일).
비교 요약
| 항목 | A. CVE 테이블 합침 | B. MAL 전용 분리 (선호) |
|---|---|---|
| CVE와 분리 | ✗ (접두어로만 구분) | ✅ 물리 분리 |
| 기존 스키마 변경 | enum ALTER 등 필요 | 없음 (신규만) |
| 다운스트림 영향 | 큼 (오인 위험) | 작음 (선택적 소비) |
| 신규 개발량 | 적음 | 중간 (테이블+파이프라인) |
| ecosystem 컬럼 모델 | ✗ (생태계별 테이블) | ✅ |
| 사용자 요구 부합 | ✗ | ✅ |
추천: 방안 B. PRODUCT_KEY/VULN_RANGE 키 규칙은 기존 vuln과 동일하게 맞춰 두면, 나중에 통합 조회가 필요할 때 JOIN/UNION으로 언제든 합칠 수 있다(역은 어렵다).
다음 단계
- npm 내부 해시키 생성 로직 위치 확인 (
crawler-lib-*). - takedown 영향 정량화: Maven/PyPI MAL 샘플 ↔ product 인벤토리 매칭률 실측.
- 본 방안 B 기준으로 구현 스펙 작성 (수집기(git) → RANGE 변환기 →
TB_COMP_LIB_MAL적재 + 증분 DEPRECATED reconcile).