스캔 보존(retention)
CI·웹 훅 자동화는 push·pull request·merge request 마다 스캔을 트리거합니다. 제한이 없으면 프로젝트당 거의 동일한 스냅샷이 수천 개 쌓입니다. 포털은 이력을 유용하게, 디스크를 유한하게 유지하기 위해 보존 모델을 둡니다 — 타겟별 최신 성공 스캔만 live로 남고, 더 오래된 스냅샷은 superseded 되어 grace 윈도우 후 회수되며, release로 명시한 스캔은 영구 보존됩니다.
자동 스캔을 수신하는 포털을 운영하는 super_admin 및 team_admin. .env 편집과 docker-compose restart에 익숙해야 합니다. 스캔 단위 라이프사이클(queued → running → succeeded)은 스캔을 참고하십시오.
보존 모델
모든 스캔은 자신의 프로젝트와 실행 대상 브랜치·PR의 정규화된 ref에서 파생된 보존 타겟을 가집니다. 동일한 타겟에 더 새로운 성공 스캔이 도착하면 이전 스캔은 superseded 됩니다.
target = (project_id, normalized_ref)
scan #1 on main ──► live
scan #2 on main ──► live, scan #1 superseded ──► grace 후 회수
scan #3 on main ──► live, scan #2 superseded ──► grace 후 회수
- Live — 타겟의 가장 최근 성공 스캔. 항상 조회 가능하며 나이만으로는 회수되지 않습니다.
- Superseded — 동일 타겟의 더 새로운 성공 스캔으로 대체된, 이전에 live였던 스캔. diff·롤백을 위해 grace 윈도우(
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS, 기본 7일) 동안 보존된 뒤 sweep이 회수합니다. - Release —
metadata.release라벨이 설정된 스캔. 영구입니다 — 나이나 supersession과 무관하게 sweep이 절대 삭제하지 않습니다. 같은 라벨을 주장하는 다른 스캔만이 이를 Releases 목록에서 밀어낼 수 있고, 그 경우에도 행 자체는 남습니다. 스캔을 영구 보존하는 법을 참고하십시오. - Ref-less / 실패 — ref 타겟이 없는 스캔(ad-hoc UI 스캔)과 실패 스캔은 supersession 체인에 속하지 않습니다. 프로젝트당 하한(
SCAN_RETENTION_KEEP_LAST, 기본 30)과 age 상한(SCAN_RETENTION_MAX_AGE_DAYS, 기본 180)으로 보호됩니다.
ref 정규화
보존 타겟은 ref의 정규화된 형태를 사용하므로, CI가 어떻게 표기하든 동일한 논리적 브랜치·PR이 함께 묶입니다. 포털은 metadata.ref로 수신한 ref를 다음과 같이 정규화합니다.
| 수신 ref | 정규화 | 비고 |
|---|---|---|
refs/heads/main | main | 브랜치 ref는 refs/heads/ prefix를 제거합니다. |
refs/pull/12/merge | pr-12 | GitHub PR merge ref는 pr-<number>가 됩니다. |
refs/merge-requests/7/head | mr-7 | GitLab MR ref는 mr-<iid>가 됩니다. |
main, release/2.0 | main, release/2.0 | bare 브랜치명은 그대로 유지합니다. |
GitHub Action은 github.ref(또는 PR 번호)를, GitLab CI 템플릿은 CI_COMMIT_REF_NAME / MR IID를 전달하므로 설정 없이 올바르게 그룹화됩니다. Jenkinsfile 스니펫도 동일하게 BRANCH_NAME을 전달합니다.
보존 정책 변수
네 키 모두 런타임에 os.getenv로 읽힙니다 — .env를 편집하고 Celery 워커와 beat 서비스를 재시작하면 적용됩니다. 서비스명은 스택마다 다릅니다. 프로덕션 compose(docker-compose.yml)에서는 worker·beat이고, dev compose(docker-compose.dev.yml)에서는 celery-worker·celery-beat입니다. 정식 레퍼런스는 환경변수 → 스캔 보존을 참고하십시오.
# 포털의 .env
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS=7 # superseded 스냅샷을 회수 전 이만큼 보존
SCAN_RETENTION_KEEP_LAST=30 # ref-less / 실패 스캔의 프로젝트당 하한
SCAN_RETENTION_MAX_AGE_DAYS=180 # ref-less / 실패 스캔의 age 상한
| 키 | 기본값 | 효과 |
|---|---|---|
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS | 7 | superseded 스냅샷이 sweep에 회수되기 전 생존하는 일수. 브랜치별 롤백 이력을 더 길게 유지하려면 높이십시오. |
SCAN_RETENTION_KEEP_LAST | 30 | 나이와 무관하게 프로젝트당 보존되는 ref-less·실패 스캔의 최소 개수. sweep은 이 하한 아래로 트림하지 않습니다. |
SCAN_RETENTION_MAX_AGE_DAYS | 180 | ref 없는 성공 스캔과 실패/취소 스캔 중 이보다 오래된 것(그리고 keep-last 하한을 넘은 것)이 회수됩니다. ref의 live 스냅샷과 release 라벨 스캔은 예외입니다 — 이들은 나이가 아니라 retire가 관리합니다. |
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS나 SCAN_RETENTION_MAX_AGE_DAYS를 낮추면 다음 sweep이 더 많은 스냅샷을 회수합니다. sweep은 되돌릴 수 없습니다 — 회수된 스캔과 그 결과는 사라집니다. 먼저 올려서 디스크를 관찰한 뒤 낮추십시오.
보존 sweep
회수는 스캔 완료 시 동기적으로가 아니라 6시간 주기 Celery beat 태스크로 실행됩니다. superseded 마킹은 더 새로운 성공이 도착하는 즉시 일어나며, 디스크·DB 행은 grace 윈도우를 지난 스냅샷을 다음 sweep이 발견할 때 회수됩니다.
각 sweep은:
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS보다 오래된 superseded 스냅샷을 회수합니다.- ref 없는 성공 스캔과 모든 실패/취소 스캔 중 프로젝트당 최신
SCAN_RETENTION_KEEP_LAST개를 보존하고, 나머지 중SCAN_RETENTION_MAX_AGE_DAYS보다 오래된 것을 회수합니다. - ref의 live 스냅샷(sweep이 아니라 retire가 관리)과
metadata.release라벨이 붙은 스캔은 절대 건드리지 않습니다.
스캔 회수는 그 workspace 아티팩트(소스 클론, cdxgen SBOM, scancode 출력)와 DB 행(컴포넌트·라이선스·결과)을 삭제합니다. 감사 로그는 회수된 스캔마다 사유 — superseded(1단계) 또는 aged(2단계) — 와 함께 scans delete 이벤트를 기록합니다.
스캔을 영구 보존하는 법 (release 라벨)
보존이 절대 회수하지 않도록 스캔을 고정하려면 — 예를 들어 태그 릴리스를 뒷받침하는 스캔 — 트리거 시 metadata.release 라벨을 설정하십시오. release 라벨 스캔은 불변입니다 — grace 윈도우·age 상한·keep-last 트림에서 모두 면제됩니다.
curl -sS -X POST \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/scans" \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"kind": "source", "metadata": {"ref": "refs/tags/v2.0.0", "release": "v2.0.0"}}' | jq .
release 값은 자유 형식 라벨입니다(버전 문자열이 관례). CI에서는 태그 push에서 실행되는 워크플로에만 설정하여, 일상적 브랜치·PR 스캔은 회수 가능한 상태로 두고 release 스캔만 영구 컴플라이언스 기록으로 누적되게 하십시오.
라벨이 서로 다른 두 릴리스는 같은 브랜치에 있어도 나란히 live로 남고, 그 브랜치에 push가 들어와도 둘 다 물러나지 않습니다. 라벨을 두는 이유가 이것입니다. 브랜치 포인터는 계속 움직이지만, 출시한 버전은 그 브랜치의 무관한 트래픽 때문에 물러나서는 안 됩니다.