Copy & Run

취약한 의존성 스캔하고 CycloneDX SBOM 만들기

취약한 버전이 고정된 requirements.txt 하나로 pip-audit 스캔, CycloneDX SBOM 생성, Trivy 교차 확인, 버전 상향 후 재검증까지 해 봅니다. 마지막에는 같은 흐름을 PR마다 돌리는 GitHub Actions 워크플로를 붙입니다.

실습 노트 · 2026-10-15 발행 · 9분 · 2026-10 확인

오래된 버전을 고정한 파이썬 의존성 목록을 만들고 알려진 취약점을 찾아 SBOM(소프트웨어 자재명세서)으로 남깁니다. pip-audit로 취약점과 수정 버전을 확인하고, cyclonedx-py로 CycloneDX 형식의 SBOM을 만듭니다. 같은 목록을 Trivy로도 스캔해 두 도구의 결과가 어떻게 다른지 비교합니다. 마지막으로 버전을 올려 취약점이 사라지는 것까지 확인합니다.

취약한 버전은 직접 만든 실습용 목록에만 적용합니다. 실제 서비스의 의존성 파일로 이 목록을 설치하지 마세요.

준비물

항목 버전 용도
Python 3.12.10 가상환경과 스캐너 실행
pip-audit 2.10.1 의존성 취약점 스캔
cyclonedx-bom (cyclonedx-py) 7.5.0 SBOM 생성
Trivy 0.76.0 교차 확인 (Docker 이미지로 실행)

명령은 Git Bash 또는 리눅스·macOS 셸 기준입니다. 예상 소요 시간은 25분 안팎입니다. pip-audit는 취약점 데이터베이스를 온라인으로 조회하므로 인터넷 연결이 필요합니다. Trivy는 Docker가 있으면 설치 없이 이미지로 실행할 수 있습니다.

1. 실습 폴더와 취약한 목록 만들기

아래 여섯 패키지는 일부러 오래된 버전으로 고정했습니다. 설치는 하지 않고 목록만 스캔합니다.

mkdir sca-lab && cd sca-lab
python -m venv .venv
source .venv/bin/activate        # Windows Git Bash: source .venv/Scripts/activate
pip install pip-audit cyclonedx-bom

cat > requirements.txt <<'EOT'
flask==0.12.2
jinja2==2.10
werkzeug==0.15.3
urllib3==1.23
pyyaml==5.1
requests==2.19.1
EOT

설치가 끝나면 아래 명령으로 도구 버전을 확인합니다.

pip-audit --version
cyclonedx-py --version

기대 출력:

pip-audit 2.10.1
7.5.0

2. pip-audit로 알려진 취약점 찾기

pip-audit는 목록의 각 패키지 버전을 PyPI 보안 권고 데이터베이스와 대조합니다. 직접 적은 6개뿐 아니라 이들이 끌고 오는 의존성도 함께 검사합니다.

pip-audit -r requirements.txt
echo "exit=$?"

기대 출력(앞부분만, 건수는 데이터베이스가 갱신되면 달라집니다):

Found 82 known vulnerabilities in 7 packages
Name     Version ID              Fix Versions
-------- ------- --------------- -------------
flask    0.12.2  PYSEC-2019-179  1.0
flask    0.12.2  PYSEC-2018-66   0.12.3
flask    0.12.2  PYSEC-2023-62   2.2.5,2.3.2
...
exit=1

취약점이 하나라도 있으면 종료 코드가 1입니다. CI에서 이 값을 그대로 게이트로 씁니다. 같은 ID가 표에 두 번씩 나오기도 하는데, 같은 취약점이 여러 번 보고된 것입니다. 중복을 빼고 세면 패키지별로 flask 4개, jinja2 6개, werkzeug 11개, urllib3 12개, pyyaml 3개, requests 5개이고, 직접 적지 않은 idna에서도 2개가 나옵니다.

3. CycloneDX SBOM 만들기

같은 목록으로 SBOM을 JSON 파일로 만들고, 어떤 구성요소가 들어 있는지 확인합니다.

cyclonedx-py requirements requirements.txt -o sbom.json --output-format JSON
cat > show_sbom.py <<'EOT'
import json
d = json.load(open('sbom.json'))
print(d['bomFormat'], d['specVersion'], len(d['components']))
for c in d['components']:
    print(c['name'], c['version'], c['purl'].split('/')[0])
EOT
python show_sbom.py

기대 출력:

CycloneDX 1.6 6
flask 0.12.2 pkg:pypi
jinja2 2.10 pkg:pypi
pyyaml 5.1 pkg:pypi
requests 2.19.1 pkg:pypi
urllib3 1.23 pkg:pypi
werkzeug 0.15.3 pkg:pypi

각 구성요소에는 pkg:pypi/이름@버전 형태의 purl이 들어 있습니다. 이 값은 패키지를 도구와 무관하게 가리키는 표준 식별자입니다. 이 방식은 requirements.txt에 적힌 패키지만 담고, 라이선스 정보는 채우지 않습니다. 전이 의존성과 라이선스까지 담으려면 패키지를 설치한 환경을 대상으로 cyclonedx-py environment를 쓰는 방법이 있는데, 이 글에서는 실행해 보지 않았습니다.

4. Trivy로 교차 확인하기

Trivy는 pip-audit와 데이터 출처와 매칭 방식이 다르므로 결과도 다릅니다. Docker로 같은 폴더를 마운트해 실행합니다.

docker run --rm -v "$PWD:/work" aquasec/trivy:0.76.0 \
  fs --scanners vuln /work/requirements.txt

PowerShell에서는 $PWD 대신 ${PWD}를 쓰고, Git Bash에서는 명령 앞에 MSYS_NO_PATHCONV=1을 붙입니다. 경로가 윈도우 형식으로 바뀌어 마운트가 실패하는 것을 막기 위해서입니다.

기대 출력(앞부분만, 건수는 날짜에 따라 달라집니다):

Report Summary

┌──────────────────┬──────┬─────────────────┐
│      Target      │ Type │ Vulnerabilities │
├──────────────────┼──────┼─────────────────┤
│ requirements.txt │ pip  │       39        │
└──────────────────┴──────┴─────────────────┘
...
Total: 39 (UNKNOWN: 0, LOW: 2, MEDIUM: 20, HIGH: 14, CRITICAL: 3)

Trivy는 CVE 번호와 심각도를 함께 보여 줍니다. 위 테이블에서 pyyaml 5.1이 CVE-2019-20477(GHSA-3pqx-4fqf-j49f)로 CRITICAL, 수정 버전 5.2로 표시됩니다. 앞에서 만든 SBOM도 그대로 입력으로 쓸 수 있습니다.

docker run --rm -v "$PWD:/work" aquasec/trivy:0.76.0 sbom /work/sbom.json

기대 출력의 요약은 날짜에 따라 달라지지만 이 실습에서는 Total: 39 (UNKNOWN: 0, LOW: 2, MEDIUM: 20, HIGH: 14, CRITICAL: 3)로, 목록을 직접 스캔했을 때와 같습니다. 심각도 기준으로 막고 싶다면 --severity CRITICAL --exit-code 1을 붙입니다. 이 목록에서는 종료 코드 1이 반환됩니다.

5. 버전을 올려 재검증하기

2단계 표의 Fix Versions를 참고해 수정본을 만들고 다시 스캔합니다.

cat > requirements.fixed.txt <<'EOT'
flask>=3.0
jinja2>=3.1.4
werkzeug>=3.0.3
urllib3>=2.2.2
pyyaml>=6.0.1
requests>=2.32.0
EOT

pip-audit -r requirements.fixed.txt
echo "exit=$?"

기대 출력:

No known vulnerabilities found
exit=0

실제 프로젝트에서는 버전을 올린 뒤 테스트를 돌려 호환성이 깨지지 않았는지 확인해야 합니다. flask 0.x에서 3.x로의 변경은 코드 수정을 동반할 가능성이 큽니다.

6. PR마다 자동으로 돌리기

같은 흐름을 GitHub Actions로 옮기면 PR 단계에서 취약한 의존성이 걸러지고, SBOM이 빌드 산출물로 남습니다. 이 워크플로는 로컬에서만 실행해 보았고 Actions 위에서는 돌려 보지 않았습니다.

name: sca-sbom
on: [pull_request]
jobs:
  sca:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: pip-audit
        run: |
          pip install pip-audit cyclonedx-bom
          pip-audit -r requirements.txt
      - name: SBOM 생성
        run: cyclonedx-py requirements requirements.txt -o sbom.json --output-format JSON
      - uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.json

pip-audit가 종료 코드 1을 반환하면 그 단계에서 잡이 멈추므로 SBOM 단계가 건너뛰어집니다. 취약점이 있어도 SBOM은 남기고 싶다면 SBOM 단계를 앞에 두거나 if: always()를 붙입니다.

자주 막히는 곳

pip-audit 실행 중 WARNING:venv:Actual environment location may have moved due to redirects, links or junctions. Windows에서 임시 폴더 경로가 짧은 이름으로 잡힐 때 나오는 경고입니다. 결과에는 영향이 없었고 무시해도 됩니다.

cyclonedx-py 옵션 오류 버전에 따라 서브커맨드와 옵션이 다릅니다. 이 글은 7.5.0 기준입니다. cyclonedx-py requirements --help로 현재 버전의 옵션을 확인합니다.

Trivy가 Unable to find python site-packages directory. License detection is skipped. 경고를 냅니다 requirements.txt만 스캔하면 설치된 패키지가 없어 라이선스를 읽지 못한다는 뜻입니다. 취약점 스캔 결과에는 영향이 없습니다.

pip-audit와 Trivy의 건수가 다릅니다 이 실습에서 pip-audit는 중복 포함 82건, Trivy는 39건이었습니다(날짜에 따라 달라집니다). 데이터 출처와 중복 처리 방식이 달라서 생기는 차이이므로, 한쪽만 믿지 말고 두 결과를 함께 봅니다.

이어서 해 볼 것

  • 3단계에서 만든 SBOM에 cyclonedx-py environment를 써서 전이 의존성과 라이선스까지 담아 보고, 구성요소 수가 얼마나 늘어나는지 비교합니다.
  • 4단계의 Trivy 옵션에 --severity HIGH,CRITICAL을 주고, 어느 패키지부터 올려야 하는지 우선순위를 정해 봅니다.
  • requirements.txt에 --require-hashes를 쓰려면 해시를 어떻게 고정해야 하는지 pip 문서로 찾아 적용합니다. 이름을 비슷하게 위장한 악성 패키지 설치를 막는 데 쓰는 옵션입니다.

확인 범위

1~5단계의 명령은 Windows에서 Python 3.12.10 가상환경과 Docker로 직접 실행했고, 취약점 건수는 2026-10-15 기준입니다. 6단계의 GitHub Actions 워크플로는 자료 기준이며 실제 Actions에서 실행하지 않았습니다. 3단계의 cyclonedx-py environment와 이어서 해 볼 것에 적은 --require-hashes도 실행하지 않았습니다.

DevSecOps 실무 과정의 일부입니다

이 실습은 교육에서 참가자마다 독립된 환경으로 진행합니다. 팀 교육으로 구성이 필요하면 문의해 주세요.

과정 보기교육 문의