GitHub Actions
TRUSCA 컴포지트 액션은 TRUSCA 스캔을 트리거하고 종료를 기다린 다음 빌드 게이트를 평가하고 (pull request에서는) SCA 보고서를 PR로 다시 게시합니다. 게이트가 실패하면 non-zero로 종료해 PR 체크가 빨갛게 변하고 브랜치 보호 룰이 머지를 차단합니다.
GitHub Actions를 사용하는 GitHub 저장소를 운영하는 엔지니어. 포털용 API Key가 필요합니다 — API keys 참고.
모노레포의 actions/scan/action.yml 컴포지트 액션을 uses: trustedoss/trusca/actions/scan@v0.23.0로 직접 참조하세요. 독립된 Marketplace 게시는 로드맵에 있습니다.
시작 전 준비
아래 워크플로가 돌기 전에 세 가지가 갖춰져야 합니다.
- 러너가 접근할 수 있는 포털. GitHub 호스트 러너는
http://localhost:5173에 접근할 수 없습니다 — Quickstart의 노트북 데모 스택으로는 부족합니다. 네트워크로 접근 가능한 URL을 가진 TRUSCA 배포가 필요하고 (Docker Compose 설치 참고), 그 URL이api-url이 됩니다. 같은 네트워크 안의 셀프 호스트 러너라면 내부 URL도 됩니다. - API Key. 포털의 /integrations → API keys에서 발급합니다 — 아래 설정 1단계에서 다룹니다.
- **프로젝트 id.**이 저장소가 매핑되는 포털 프로젝트의 id로, Project Settings → CI/CD에서 확인합니다 — 아래 설정 3단계에서 다룹니다.
빠른 시작
# .github/workflows/sca.yml
name: TRUSCA SCA
on:
pull_request:
push:
branches: [main]
jobs:
sca:
runs-on: ubuntu-latest
permissions:
contents: read # action이 체크아웃 외에 필요로 하는 권한은 없습니다
steps:
- uses: actions/checkout@v4
- name: TRUSCA SCA scan
uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
이게 최소 구성입니다. action은 다음을 수행합니다.
kind=source로POST /v1/projects/{project-id}/scans를 호출해 cdxgen + scancode + Trivy를 큐에 넣습니다.- 30초마다
GET /v1/scans/{scan-id}를 폴링해 최종 상태(succeeded/failed/cancelled)에 도달할 때까지 대기, 30분 타임아웃. GET /v1/projects/{project-id}/gate-result?ref=<github.ref>를 호출해 verdict를 워크플로의 job summary에 기록.pull_request이벤트에서는POST /v1/scans/{scan-id}/post-pr-comment를 호출해 SCA Markdown 보고서를 PR 코멘트로 게시.- 게이트 verdict가
fail이면 1로 종료.
3단계의 ref가 풀 리퀘스트의 판정을 그 브랜치의 것으로 지켜 줍니다. ref 없이 프로젝트의 게이트 결과를 물으면 포털은 메인 라인에서 가장 최근에 성공한 스캔으로 답합니다. 그래서 자기 스캔이 끝나기를 기다린 PR 빌드가 정작 main 기준으로 판정됩니다. 자기가 넣지도 않은 심각한 취약점 때문에 막히거나, 반대로 자기가 들여온 취약점을 안은 채 통과합니다. action은 스캔을 요청할 때 보낸 github.ref를 그대로 다시 보내고, 포털은 양쪽을 같은 규칙으로 정규화하므로(refs/pull/12/merge와 pr-12는 같은 키입니다) 판정은 언제나 실제로 스캔한 코드를 가리킵니다.
성공한 스캔이 없는 ref는 다른 브랜치의 결과 대신 신호 없음(pass)을 돌려줍니다. 의도된 동작입니다. 브랜치를 지정했다면 그 브랜치이거나 아무것도 아니어야 합니다.
셋업
1. API Key 생성
포털에서 /integrations → API keys → Create API key. 스코프는 project를 선택하고 CI가 스캔할 프로젝트에 바인딩합니다(한 팀의 모든 프로젝트를 커버해야 한다면 team). "이 키가 할 수 있는 일"은 읽기 및 쓰기로 설정합니다. 스캔을 실행하는 동작이라 기본값인 읽기 전용 키는 거부됩니다. 그 외에는 발급한 사용자의 역할을 그대로 물려받습니다. 스코프와 권한 범위 모델은 API keys 참고.
2. GitHub에 Key 저장
저장소에서 Settings → Secrets and variables → Actions → New repository secret.
- Name —
TRUSTEDOSS_API_KEY - Value — 전체 Key(
tos_<prefix>_<secret>)
3. 프로젝트 ID를 변수로 저장
같은 화면에서 Variables로 전환 후 추가.
- Name —
TRUSTEDOSS_PROJECT_ID - Value — 프로젝트의 UUID. 포털 주소
/projects/<uuid>의 마지막 경로 조각입니다.
(시크릿이 아니라) 변수에 두면 워크플로 로그에서 프로젝트 ID가 그대로 보입니다 — 민감 정보가 아니므로 무방합니다.
4. 워크플로 추가
위 .github/workflows/sca.yml을 저장소에 두세요. 다음 PR부터 SCA 체크가 PR 상태로 나타납니다.
입력
| 이름 | 필수 | 기본값 | 설명 |
|---|---|---|---|
api-url | yes | — | 포털 base URL, 예: https://trustedoss.example.com. 끝의 슬래시는 무방. |
api-key | yes | — | API Key. 항상 ${{ secrets.* }}로 공급. |
project-id | yes | — | 프로젝트 UUID. |
scan-kind | no | source | source(cdxgen + scancode + Trivy) 또는 container(Trivy 이미지 스캔). |
image-ref | scan-kind: container일 때 필수 | — | 포털이 받아 올 이미지. 예: ghcr.io/acme/api:1.4.0. 이 스텝 전에 푸시해 두세요. |
fail-on-gate | no | true | true이면 게이트 verdict가 fail일 때 잡이 1로 종료. |
post-pr-comment | no | true | true이고 pull_request 이벤트로 트리거되면 SCA 보고서를 PR 코멘트로 게시. |
sarif-file | no | - | 스캔의 미해결 결과를 SARIF 2.1.0 문서로 쓸 경로입니다. 저장소의 Code scanning 탭에 올리는 데 씁니다. 비워 두면 아무것도 쓰지 않습니다. 코드 스캐닝에 결과 게시를 보세요. |
poll-timeout-seconds | no | 1800 | 스캔이 최종 상태에 도달할 때까지 기다리는 최대 초. |
poll-interval-seconds | no | 30 | 스캔 상태 폴링 간격(초). |
출력
| 이름 | 설명 |
|---|---|
scan-id | 큐에 넣고 평가한 스캔의 UUID. |
gate | pass 또는 fail. |
reason | gate == 'fail'일 때 사람이 읽는 사유, 그 외에는 빈 문자열. |
critical-cve-count | 평가된 스캔의 미해결 critical 발견 수. |
forbidden-license-count | 금지 분류 라이선스를 가진 고유 컴포넌트 수. |
epss-gate-count | EPSS score가 구성된 EPSS 임계 이상인 미해결 결과 수. EPSS 게이트가 비활성(기본)이면 0. EPSS로 빌드 게이팅 참고. |
malicious-component-count | 악성 패키지 스냅샷이 지목한 컴포넌트 수. 심각도와 무관하게 빌드를 막습니다. 패키지를 제거하고 이 빌드가 닿을 수 있던 자격 증명을 교체하세요. |
epss-outcome | 게이트의 EPSS 축이 무엇을 판정할 수 있었는지입니다. not_configured는 임계가 설정되지 않아 축이 꺼져 있었다는 뜻이고, evaluated는 미해결 결과에 모두 EPSS 점수가 있어 epss-gate-count가 완전한 답이라는 뜻입니다. partial은 점수가 없는 미해결 결과가 섞여 있어 0이 곧 임계를 넘는 것이 없었다는 증거가 되지 못한다는 뜻이고, no_data는 점수를 가진 결과가 하나도 없어 축이 아무것도 판정하지 못했다는 뜻입니다. 이 값을 보고하지 못하는 구버전 포털에서는 비어 있습니다. |
sarif-file | 기록한 SARIF 문서의 경로입니다. sarif-file 입력을 주지 않았으면 비어 있습니다. |
kev-gate-count | CVE가 CISA Known Exploited Vulnerabilities 목록에 있는 미해결 결과 수입니다. KEV 축이 꺼져 있으면(기본) 0입니다. 0을 이상 없음으로 읽기 전에 kev-outcome을 보세요. |
kev-outcome | KEV 축이 무엇을 판단할 수 있었는지입니다. not_configured는 꺼 둔 상태, evaluated는 모든 미해결 결과가 목록 동기화를 거친 상태, partial은 마지막 동기화 이후에 발견된 결과가 있어 그 플래그가 답이 아니라 기본값인 상태, no_data는 이 배포에서 목록이 한 번도 동기화되지 않아 축이 아무것도 판단하지 못한 상태입니다. |
eol-gate-count | 평가한 스캔에서 지원 종료된 릴리스 라인을 쓰는 컴포넌트 수입니다. 지원 종료 축이 꺼져 있으면(기본) 0입니다. |
eol-outcome | 지원 종료 축이 무엇을 판단할 수 있었는지이며 값 네 개는 같습니다. 여기서는 partial이 통상적인 상태입니다. 수명 카탈로그가 런타임과 프레임워크를 추린 목록이라 대부분의 애플리케이션 의존성에는 항목이 없습니다. |
component-outcome | 스캔의 SBOM에 무엇이 담겼는지입니다. components_found가 통상적인 경우입니다. empty_no_manifests와 empty_with_manifests는 둘 다 컴포넌트가 하나도 나오지 않았다는 뜻이라, 게이트 통과는 깨끗하다는 판정이 아니라 판단할 대상이 없었다는 뜻입니다. 앞은 TRUSCA가 읽지 못하는 빌드 시스템에서 정상적으로 나오는 값이고, 뒤는 스캔이 실패했다는 신호입니다. 이 값을 보고하지 못하는 구버전 포털에서는 비어 있습니다. |
후속 스텝에서 사용:
- name: TRUSCA SCA scan
id: sca
uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
fail-on-gate: 'false' # 수집만, 실패 안 함
- name: Branch on the gate verdict
if: steps.sca.outputs.gate == 'fail'
run: |
echo "Critical CVEs: ${{ steps.sca.outputs.critical-cve-count }}"
echo "Forbidden licenses: ${{ steps.sca.outputs.forbidden-license-count }}"
exit 1
레시피
Advisory 모드(실패시키지 않고 보고만)
정책을 시드하는 동안 PR을 차단하지 않으려는 경우에 유용합니다.
- uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
fail-on-gate: 'false'
PR 코멘트는 그대로 게시되며 체크는 green으로 유지됩니다.
컨테이너 스캔
- uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
scan-kind: container
image-ref: ghcr.io/acme/api:${{ github.sha }}
컨테이너 스캔은 이미지의 OS 패키지에 Trivy를 실행합니다. image-ref는 필수입니다. 포털에는 프로젝트별 기본 이미지가 없고, 이 값이 없는 컨테이너 스캔은 워커에서 실패하는 대신 트리거 시점에 거부됩니다.
이미지는 포털이 직접 받아 오므로 이 스텝 전에 푸시해 두세요. 러너의 로컬 Docker 데몬에만 있는 태그는 닿지 않고, 비공개 레지스트리라면 러너가 아니라 포털에 자격 증명을 설정해야 합니다.
소스와 컨테이너 둘 다
서로 다른 id로 두 스텝 실행:
- name: SCA — source
uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
scan-kind: source
- name: SCA — container
uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
scan-kind: container
image-ref: ghcr.io/acme/api:${{ github.sha }}
기본적으로 어느 한 스텝의 실패가 잡 전체를 실패시킵니다.
두 스텝은 같은 포털 프로젝트를 스캔하고 포털은 (project, ref) 하나당 진행 중인 스캔을 하나만 허용하므로, 위처럼 순서대로 두세요. 병렬 잡으로 나누면 뒤엣것이 자기 스캔을 시작하는 대신 앞엣것에 붙습니다.
브랜치별 게이트
main에서만 게이트를 적용하고 PR에서는 advisory:
- uses: trustedoss/trusca/actions/scan@v0.23.0
with:
api-url: https://trustedoss.example.com
api-key: ${{ secrets.TRUSTEDOSS_API_KEY }}
project-id: ${{ vars.TRUSTEDOSS_PROJECT_ID }}
fail-on-gate: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'true' || 'false' }}