콘텐츠로 이동

CI/CD 연동

SBOM은 의존성의 특정 시점 스냅샷이므로, 의존성이 바뀔 때마다 다시 생성해야 항상 코드와 일치합니다. CI에 통합하면 매 빌드와 릴리스마다 SBOM이 자동 갱신되고, 릴리스 아티팩트에 첨부되며, 취약점 정책 게이트의 기준이 됩니다.

중요: 스캔은 게이트를 요청하지 않으면 취약점을 보고하고 성공으로 종료합니다. --fail-on을 주면 지정한 조건에 걸렸을 때(종료 4), 또는 스캔 결과만으로 조건을 판정할 수 없을 때(종료 5) 스캔 자체가 0이 아닌 코드로 종료합니다. 조건은 특정 심각도 이상의 취약점, 알려진 악성 패키지, 라이선스 충돌, 소프트웨어를 하나도 식별하지 못한 스캔(empty-result), 지정한 비율에 못 미치는 라이선스 포함률(license-coverage=<0-100>)입니다. --fail-on-conformance는 같은 방식으로 적합성 검사 보고서를 판정합니다(종료 2). 종료 코드를 참고하세요. 보고서 파일을 따로 검사하는 step은 필요 없습니다.

부하를 줄이려면 트리거에 따라 깊이를 나눕니다. PR에서는 SBOM만 빠르게 생성하고(--generate-only --no-report), main과 릴리스에서는 보안 보고서까지 전체 생성한 뒤(--all --generate-only) 게이트를 적용합니다.

GitHub Actions

name: SBOM

on:
  pull_request:
  push:
    branches: [main]
  release:
    types: [published]

jobs:
  # PR: SBOM만 가볍게 생성 (보고서 생략)
  sbom-pr:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: git clone --depth 1 https://github.com/sktelecom/bomlens.git /tmp/bomlens
      - run: docker pull ghcr.io/sktelecom/bomlens:latest
      - name: Generate SBOM (lightweight)
        run: |
          /tmp/bomlens/scripts/scan-sbom.sh \
            --project "${{ github.event.repository.name }}" \
            --version "${{ github.sha }}" \
            --generate-only --no-report
      - uses: actions/upload-artifact@v4
        with:
          name: sbom-pr
          path: "*/*_bom.json"

  # main/release: 전체 생성 + 취약점 게이트
  sbom-full:
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: git clone --depth 1 https://github.com/sktelecom/bomlens.git /tmp/bomlens
      - run: docker pull ghcr.io/sktelecom/bomlens:latest
      - name: Generate SBOM + reports
        run: |
          /tmp/bomlens/scripts/scan-sbom.sh \
            --project "${{ github.event.repository.name }}" \
            --version "${{ github.sha }}" \
            --all --generate-only --fail-on-conformance \
            --fail-on vulnerability=critical --fail-on malicious-package

      # 위 스캔이 이 step을 이미 실패시킨다. 적합성 검사 보고서가 "fail"이면 종료 2,
      # Critical 취약점이나 악성 패키지가 있으면 종료 4, 둘 중 하나를 판정할 수
      # 없으면 종료 5다. 라이선스 충돌도 막으려면
      # `--license <SPDX id> --fail-on license-conflict`를 더한다. 산출물은
      # {project}_{version}/ 하위 폴더에 생기므로(CLI 레퍼런스 참고) 아래에서 */ 글롭을 쓴다.

      - uses: actions/upload-artifact@v4
        if: always()   # 게이트 실패 시에도 보고서는 보존
        with:
          name: sbom
          path: |
            */*_bom.json
            */*_security.*
            */*_risk-report.*

GitLab CI

스캔 자체가 게이트하므로 job에 jq가 필요 없습니다.

generate-sbom:
  stage: security
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - apk add --no-cache git
    - git clone --depth 1 https://github.com/sktelecom/bomlens.git /tmp/bomlens
  script:
    - docker pull ghcr.io/sktelecom/bomlens:latest
    - /tmp/bomlens/scripts/scan-sbom.sh
        --project "$CI_PROJECT_NAME"
        --version "$CI_COMMIT_SHA"
        --all --generate-only --fail-on-conformance
        --fail-on vulnerability=critical --fail-on malicious-package
    # 스캔이 이 job을 이미 실패시킨다. 적합성 검사 보고서가 "fail"이면 종료 2,
    # Critical 취약점이나 악성 패키지가 있으면 종료 4, 둘 중 하나를 판정할 수
    # 없으면 종료 5다. 산출물은 {project}_{version}/ 하위 폴더에 생기므로(CLI
    # 레퍼런스 참고) 아래에서 */ 글롭을 쓴다.
  artifacts:
    when: always
    paths:
      - "*/*_bom.json"
      - "*/*_security.*"

CI에서 선언 구성요소 사용

직접 만든 실행 파일, 라이브러리, 펌웨어와, 스캔이 찾지 못하는 벤더 바이너리나 복사한 소스는 저장소에 둔 선언 파일에 적습니다(자체 구성요소 선언 참고). 스캔은 이 파일이 가리키는 빌드 산출물을 읽으므로, 작업은 먼저 빌드한 뒤 스캔합니다. 아래 릴리스 작업은 결과를 BSI TR-03183-2 기준으로 검사하고 버전을 태그에서 가져옵니다.

name: Release SBOM

on:
  push:
    tags: ['v*']

jobs:
  sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: make   # sbom-declared.cdx.json이 가리키는 파일(예: out/bin/media-daemon)을 만든다
      - run: git clone --depth 1 https://github.com/sktelecom/bomlens.git /tmp/bomlens
      - run: docker pull ghcr.io/sktelecom/bomlens:latest
      - name: Generate SBOM
        run: |
          /tmp/bomlens/scripts/scan-sbom.sh \
            --project "${{ github.event.repository.name }}" \
            --version "${GITHUB_REF_NAME#v}" \
            --components sbom-declared.cdx.json \
            --conformance-profile bsi-tr-03183-2 --fail-on-conformance \
            --supplier "Example Corp <https://example.com>" \
            --sbom-author "Example Corp <sbom@example.com>" \
            --license Apache-2.0 \
            --generate-only

      # 선언 파일이 거부되면(가리킨 빌드 산출물이 없거나, 구성요소에
      # bomlens:origin이 없는 경우 등) 이 step은 종료 1로 실패한다. 이때 SBOM은
      # _bom.json이 아니라 {project}_{version}_bom.refused.json으로 남으므로
      # 뒤 단계가 불완전한 SBOM을 가져가지 않는다. BSI 항목이 실패하면
      # 종료 2로 끝난다(--fail-on-conformance).

      - uses: actions/upload-artifact@v4
        if: always()   # 적합성 보고서와 거부된 SBOM도 보존
        with:
          name: sbom
          path: |
            */*_bom.json
            */*_conformance.*
            */*_bom.refused.json

${GITHUB_REF_NAME#v}는 태그 v2.1.0을 2.1.0으로 바꾸며, 자체 version이 없는 first-party 구성요소가 이 값을 씁니다. --components로 파일을 명시했지만, 옵션이 없어도 저장소 루트의 sbom-declared.cdx.json은 읽습니다. --license에는 SPDX 식별자를 줍니다.

러너에 Docker가 없을 때

scan-sbom.sh는 컨테이너를 띄우므로 Docker 엔진이 필요합니다. 엔진이 없는 러너(쿠버네티스 러너, docker:dind를 쓰지 않는 GitLab 작업)에서는 위 예시를 쓸 수 없습니다. 이때는 스캐너 이미지를 작업 이미지로 그대로 씁니다. lock 파일이 트리 전체를 덮으면(package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, bun.lock, gradle.lockfile 중 하나이고 루트 아래 다른 매니페스트가 없을 때) 이미지 안의 소스 스캔은 Docker 소켓 없이 lock 파일을 그대로 읽고, 빠진 라이선스는 패키지 레지스트리에서 채웁니다. 그런 lock 파일이 없는 트리는 syft가 매니페스트를 읽어 직접 의존성만 나오므로(의존성을 선언했는데 lock 파일이 없는 Node 프로젝트는 안내와 함께 거부됩니다) lock 파일을 커밋하거나 Docker가 있는 러너를 쓰세요.

generate-sbom:
  stage: security
  image:
    name: ghcr.io/sktelecom/bomlens:latest
    entrypoint: [""]
  variables:
    MODE: SOURCE
    SOURCE_ROOT: $CI_PROJECT_DIR
    PROJECT_NAME: $CI_PROJECT_NAME
    PROJECT_VERSION: $CI_COMMIT_SHA
    GENERATE_NOTICE: "true"
    GENERATE_SECURITY: "true"
    UPLOAD_ENABLED: "false"
    HOST_OUTPUT_DIR: $CI_PROJECT_DIR/sbom-out
    FAIL_ON: "vulnerability=critical,malicious-package"
  script:
    - mkdir -p sbom-out && cd sbom-out
    - /usr/local/bin/run-scan
    # 직접 실행에서 게이트 판정은 종료 코드가 아니라 파일로 남습니다. 조건에 걸렸거나
    # ("met") 판정할 수 없으면("unjudged") 작업을 실패시킵니다.
    - '! grep -qE "^(met|unjudged)" *_gate.result'
  artifacts:
    when: always
    paths:
      - sbom-out/

변수는 직접 docker run과 같습니다. 스캔은 SOURCE_ROOT에서 프로젝트를 읽고 실행한 폴더에 결과를 씁니다.


관련 문서: CLI 레퍼런스 | 고지문·보안·위험 보고서 생성 | 보고서 읽는 법