<p align="center">
<img src="https://github.com/user-attachments/assets/88b976b4-cc46-4706-a3e4-3cfa0e6877d5" alt="zanadir"></p>
<p align="center">
<a href="#coverage-score"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/MustacheCase/zanadir/badges/zanadir-badge.json" alt="zanadir coverage"></a>
</p>
zanadir는 리포지토리의 CI(Continuous Integration) 구성을 읽어 어떤 보안 및 품질 통제가 누락되었거나, 빌드를 실패시킬 수 없도록 연결되어 있는지 보고합니다.
답변하는 내용
- 우리 파이프라인에 무엇이 빠져 있나요? 종속성 스캐닝(dependency scanning)과 비밀 탐지(secrets detection)부터 SBOM(Software Bill of Materials) 및 워크플로우 강화(workflow hardening)까지, 12가지 범주 각각을 해당 기능을 다루는 도구와 비교하여 확인하고, 리포지토리의 언어에 맞는 제안을 제공합니다.
- 우리 CI가 실제로 보안 문제로 빌드를 차단하나요?
continue-on-error, --exit-code 0, || true 등이 존재하거나 푸시 전용 트리거(push-only trigger) 또는 allow_failure가 사용되어 병합을 막지 못하는 경우를 탐지합니다. zanadir는 단순히 커버 여부뿐 아니라 각 통제를 강제적(enforcing), 부분적(partial), 권고적(advisory) 또는 *알 수 없음(unknown)*으로 등급화하여 점수를 매깁니다. 자세한 내용은 Effectiveness를 참조하세요.
- 그 검사가 병합에 필수인가요?
--required-checks를 사용하면, 브랜치 보호(branch protection)나 규칙 세트(ruleset)가 요구하는 경우에만 해당 통제가 강제적(enforcing)으로 간주됩니다.
- 무엇을 추가해야 하나요?
zanadir fix는 누락된 부분을 채울 수 있는 CI 구성을 바로 붙여넣기 할 수 있는 형태로 출력하거나, --write 옵션을 사용하여 워크플로우를 작성해 줍니다.
GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines, Azure Pipelines, Jenkins 등에서 체크아웃(--dir)을 통해 읽거나 GitHub에서 직접(--repo) 읽어와 테이블, JSON, SARIF 또는 Markdown 형식으로 보고합니다.
zanadir scan --dir .
zanadir scan --repo github.com/owner/name --output json
zanadir fix --dir .
zanadir와 OpenSSF Scorecard
OpenSSF Scorecard와 zanadir는 서로 다른 질문에 답하며, 함께 사용하면 시너지가 좋습니다.
| OpenSSF Scorecard | zanadir |
|---|
| 질문 | 이 프로젝트의 보안 관행은 얼마나 건전한가요? | 어떤 CI 제어가 누락되었으며, 현재 존재하는 제어는 빌드를 실패시킬 수 있나요? |
| ... | | |
| 사용 가능한 모든 CI를 실제로 막을 수 있는지, 그리고 무엇을 추가해야 하는지 확인하려면 Scorecard를 프로젝트 전반의 보안 태세에 사용하고, zanadir를 사용하세요. | | |
AI 코딩 에이전트와 함께 zanadir 사용하기
코딩 에이전트가 변경하기 전에 zanadir로 CI를 검사하도록 하려면, 리포지토리의 AGENTS.md, CLAUDE.md 또는 이에 상응하는 파일에 다음 내용을 추가하세요:
## CI 변경 사항
CI 구성을 추가하거나 변경하기 전에 `zanadir scan --dir . --output json`을 실행하세요.
...
llms.txt는 리포지토리를 읽는 에이전트를 위해 zanadir를 요약하며, 사이트 루트에서 확인하는 경우 mustachecase.github.io/llms.txt에서 제공됩니다.
기능
- 📂 스캔(Scan): 보안 서비스 및 모범 사례를 포함하여 CI/CD 개선 제안을 위해 리포지토리를 분석합니다.
- 🔍 CI 분석: 단순히 존재하는지 여부뿐만 아니라, 각 제어가 실제로 빌드를 실패시킬 수 있는지 등급을 매깁니다.
- 🛠️ 수정(Fix): 스캔에서 누락되었다고 보고된 카테고리에 대한 CI 구성을 출력하거나 워크플로우로 작성합니다.
- 🚀 오픈 소스: Zanadir의 기능을 향상시키기 위한 기여를 환영합니다!
지원하는 CI 액션
Zanadir는 현재 다음을 지원합니다:
- GitHub Actions
- CircleCI
- GitLab
- Bitbucket Pipelines
- Azure Pipelines
- Jenkins
향후 작업에는 다음 지원이 포함될 예정입니다:
제안하는 카테고리
Zanadir는 다음 카테고리에서 리포지토리를 분석합니다. 굵은 글씨의 이름은 --excluded-categories에 전달할 정확한 값이며 (대소문자를 구분하지 않습니다):
- 🛡️ SCA: 소프트웨어 구성 분석 (Software Composition Analysis)
- 🔐 Secrets Detection: 비밀 정보 감지 (Secrets Management)
- 📜 License Compliance: 라이선스 준수 (License Compliance)
- 🛠️ End Of Life: 지원 종료된 소프트웨어 패키지 (End-of-Life Software Packages)
- 📊 Coverage: 커버리지 (Test Coverage)
- 📊 Performance Testing: 성능 테스트 및 신뢰성 (Test Performance and Reliability)
- 🧑💻 Linter: 코드 린팅 (Code Linting)
- 🧪 Unit Tests: 자동 단위 테스트 (Automated Unit Testing)
- 🔎 SAST: 정적 애플리케이션 보안 테스트 (Static Application Security Testing)
- 🏗️ IaC Security: IaC 미구성 스캔 (Infrastructure-as-Code Misconfiguration Scanning)
- 🔗 Supply Chain: SBOM 생성, 서명 및 출처 추적 (SBOM Generation, Signing and Provenance)
- 🧱 Workflow Security: 파이프라인 정의 자체 강화 (Hardening of the pipeline definition itself)
사용 예시
기본 사용법
CI/CD 개선 제안을 위해 저장소를 스캔합니다:
zanadir scan --dir /path/to/your/repo
또는 클론하지 않고 GitHub 저장소를 스캔합니다:
zanadir scan --repo github.com/owner/name
zanadir scan --repo owner/[email protected]
zanadir fix --repo owner/name
--repo는 저장소의 아카이브를 임시 디렉터리에 다운로드하고, 체크아웃된 것처럼 정확하게 스캔한 다음 제거합니다. zanadir가 읽은 파일만 내용이 유지됩니다. GitHub의 아카이브는 .gitattributes가 export-ignore로 표시하는 내용은 제외하는데, 여기에는 종종 .github가 포함되므로, zanadir는 CI가 없는 저장소라고 보고하기보다는 해당 파일을 별도로 가져옵니다. 공개 저장소는 토큰이 필요 없지만, 비공개 저장소에 접근하거나 GitHub의 속도 제한을 높이려면 GITHUB_TOKEN (또는 GH_TOKEN)을 설정해야 합니다. GitLab 및 Bitbucket 저장소는 여전히 클론이 필요합니다.
출력 형식
Zanadir는 세 가지 출력 형식을 지원합니다: 테이블(기본값), JSON, SARIF.
테이블 출력 (기본값)
zanadir scan --dir . --output table
샘플 출력:
Coverage 10/11 - 1 카테고리가 주의를 요합니다:
|--------------------------------|--------------------------------|-------------------|
...
누락된 항목이 없을 경우, 빈 테이블을 출력하는 대신 스캔 결과에 그렇게 명시합니다:
Coverage 11/11 - 모든 카테고리가 커버되었습니다.
헤드라인은 인터랙티브 터미널에서 굵게 표시됩니다. 출력이 파이프되거나 --output-file로 작성될 때, 그리고 NO_COLOR가 설정되었을 때는 색상이 생략됩니다.
JSON 출력
zanadir scan --dir . --output json
한 객체에는 점수(score), 발견된 CI 시스템 구성(CI systems whose configuration was found), 커버되지 않은 카테고리 및 이를 커버하는 도구(uncovered categories and the tools that cover them), 저장소에 완전히 보호할 수 없는 커버된 카테고리(the covered categories whose tooling cannot fully protect the repository), 그리고 점수에서 제외된 카테고리(and the categories left out of the score)가 포함됩니다. 모든 리스트는 배열이며, 내용이 없을 경우 null 대신 빈 배열을 사용합니다.
{
"score": { "covered": 4, "total": 11 },
"platforms": ["github"],
...
language는 특정 언어에만 해당할 때 제안된 도구(suggested tool)에 표시되며, location은 저장소 내부를 가리킬 때 취약한 카테고리(weakened category)에만 표시됩니다. 0.10.0 이전에는 --output json이 자본화된 키와 함께 현재는 uncovered인 순수한 배열만 출력했습니다.
SARIF 출력
zanadir scan --dir . --output sarif
SARIF는 정적 분석 결과의 표준 상호 교환 형식(standard interchange format)입니다. 이를 출력하면 커버되지 않은 카테고리가 GitHub의 보안 탭(Security tab)과 다른 모든 SARIF를 소비하는 플랫폼에서 사용자의 다른 스캐너와 함께 표시될 수 있습니다.
각 커버되지 않은 카테고리는 warning 수준의 하나의 SARIF 규칙 및 결과가 되며, 제안된 도구는 결과의 도움말 텍스트(help text)에 나열됩니다. 결과는 CI 구성 파일에 고정(anchored)됩니다. 즉, 누락된 도구가 추가될 위치입니다. 누락된 통제(control)는 특정 라인의 결함이 아니지만, 모든 SARIF 소비자는 위치를 기대하며, GitHub 코드 스캐닝은 결과에 위치가 없는 보고서를 거부합니다.
Markdown 출력
zanadir scan --dir . --output markdown
마크다운(Markdown)은 파일을 통해서가 아니라 사람들에게 스캔을 보여주기 위한 형식입니다. 도구들은 자신들의 저장소로 링크하며, 보고서는 숨겨진 마커와 함께 열려 워크플로우가 푸시할 때마다 새로운 댓글을 추가하는 대신 마지막으로 게시한 댓글을 업데이트할 수 있게 합니다.
샘플 출력:
<!-- zanadir-report -->
### zanadir
...
아무것도 누락된 것이 없을 경우 표는 생략되고 한 줄만 남습니다.
파일로 보고서 작성하기
--output-file을 사용하여 보고서를 표준 출력(stdout) 대신 특정 경로에 작성할 수 있습니다. 이 기능은 모든 형식에서 작동하며, 기계가 읽을 수 있는 보고서를 생성하는 신뢰할 수 있는 방법입니다: 표준 출력을 리디렉션하면 디버그 로그도 캡처되므로 --output sarif --debug > report.sarif는 유효한 JSON이 아닌 파일을 산출합니다.
zanadir scan --dir . --output sarif --output-file zanadir.sarif
GitHub Actions 워크플로우에서 보고서를 게시하는 방법:
- name: Run zanadir
run: zanadir scan --dir . --output sarif --output-file zanadir.sarif
...
발견되지 않은 카테고리는 다른 스캐너와 함께 저장소의 Security 탭에 나타납니다.
풀 리퀘스트에 보고서 게시하기
SARIF는 발견되지 않은 카테고리를 사용자들이 가끔 방문하는 Security 탭에 배치합니다. 반면, 풀 리퀘스트 댓글은 사람들이 이미 머무르는 곳입니다.
permissions:
contents: read
pull-requests: write
...
--edit-last는 푸시(push)마다 하나씩 추가하는 대신 하나의 댓글을 최신 상태로 유지합니다. 이는 동일한 사용자가 작성한 마지막 댓글과 일치시키므로, 만약 다른 것이 github-actions으로 댓글을 달더라도 마커를 일치시키는 것이 좋습니다:
- name: Find the previous report
uses: peter-evans/find-comment@v4
id: previous
...
포크(fork)에서 온 풀 리퀘스트는 읽기 전용 토큰을 받으므로, 댓글 단계가 게시할 수 없습니다. 대신 pull_request_target을 사용하기보다는 다음으로 보호해야 합니다. 이는 포크의 코드에 쓰기 접근 권한으로 워크플로우를 실행하게 할 것입니다.
커버리지 점수 (Coverage Score)
모든 스캔은 커버리지 점수로 요약됩니다: 저장소에 적용되는 카테고리에서 아직 발견되지 않은 카테고리를 뺀 값입니다.
세 가지 규칙이 이 숫자를 정확하게 유지합니다:
- 제외된 카테고리는 분모에 포함됩니다. 저장소는
--excluded-categories로 관련 없다고 선언한 카테고리에 대해 마크다운되지 않습니다.
- 스캔할 내용이 없는 카테고리도 마찬가지입니다. Terraform, Helm chart, Kubernetes manifest 또는 Dockerfile이 없는 저장소에는 Checkov를 추가하라는 지시가 내려지지 않으며, 10점 만점에 11점이 아닌 점수로 평가됩니다. 스캔 결과는 다음과 같이 나타납니다:
IaC Security is not applicable and is out of the score: nothing in this repository needs it.
이런 방식으로 두 개의 카테고리가 제외됩니다. IaC Security는 스캔할 인프라가 필요하며, 탐지 범위가 의도적으로 광범위합니다—infra, k8s 또는 charts라는 디렉토리만 있어도 포함됩니다. Workflow Security는 GitHub Actions가 필요한데, 이는 해당 형식의 모든 도구가 읽기 때문입니다. 따라서 GitLab 전용 저장소에는 actionlint를 추가하라는 지시가 내려지지 않습니다.
나머지는 모든 코드베이스에 적용되며, 논란의 여지가 있지만 그렇지 않은 경우(라이브러리 로드 테스트, 아무것도 배포하지 않는 것에 대한 SBOM 등)는 해당되는 경우와 구분할 수 없으므로, 추측하면 저장소가 필요로 하는 카테고리를 제외하게 됩니다.
- 기준선으로 승인된 격차도 여전히 미해결로 계산됩니다. 기준선은 격차가 용인된다는 것을 기록할 뿐, 닫혔다는 것을 기록하지 않습니다. 누군가 파일을 커밋해서 점수가 상승했더라도 아무것도 측정하지 못합니다.
README 배지
--badge는 점수를 shields.io 엔드포인트 파일로 작성합니다:
zanadir scan --dir . --badge zanadir-badge.json
{
"schemaVersion": 1,
"label": "zanadir",
...
이 파일을 CI에서 게시하고 배지에 연결합니다. 기본 브랜치에 커밋하기보다는 별도의 브랜치에 두는 것이 좋습니다. 보호된 기본 브랜치는 풀 리퀘스트를 통해서만 변경 사항을 받기 때문에, 해당 배지를 푸시하는 워크플로우는 거부됩니다.

기본 브랜치로 푸시될 때마다 최신 상태를 유지하고, 첫 실행 시 생성되는 badges 브랜치에 게시하는 작업:
badge:
if: github.event_name == 'push'
runs-on: ubuntu-latest
...
badges 브랜치는 해당 파일 하나만 가지고 있고 그 외에는 아무것도 없습니다. 만약 규칙 세트(ruleset)가 기본 브랜치뿐만 아니라 모든 브랜치를 다룬다면, badges를 제외하세요.
색상은 점수를 추적합니다: 80% 이상은 녹색, 60%는 노란색, 40%는 주황색, 그 미만은 빨간색이며, 완전한 커버리지일 때만 밝은 녹색입니다.
효과성 (Effectiveness)
존재한다는 것이 보호를 의미하지는 않습니다. 빌드를 절대 실패시킬 수 없도록 연결된 스캐너는 아예 스캐너가 없는 저장소보다 더 나쁜 상태로 만듭니다. 왜냐하면 모두가 그 통제가 존재한다고 믿기 때문입니다. 이 모든 것은 다음을 보고하는 데 사용되었습니다:
covered:
- uses: gitleaks/gitleaks-action@v2
continue-on-error: true
on:
schedule: [{cron: "0 3 * * *"}]
on:
push: {branches: [main]}
스캔은 이제 발견한 각 통제(control)를 등급 매기고, 그 존재가 시사하는 것보다 보호 수준이 낮은 항목들을 보고합니다:
3 covered categories are weakened:
Linter is partial (.github/workflows/pushonly.yml)
SCA is advisory (.github/workflows/nightly.yml)
...