diff --git a/.claude/harness-guide.md b/.claude/harness-guide.md index 96a87cc7..78ce1fe3 100644 --- a/.claude/harness-guide.md +++ b/.claude/harness-guide.md @@ -56,7 +56,10 @@ │ ├── sync-kwg-reference.sh # KWG 원본 md 파일 동기화 │ ├── check-kwg-drift.py # KWG 원본과의 드리프트 검사 │ ├── check-i18n-parity.py # ko/en 문서 파일 패리티 검사 -│ └── check-redirects.py # 사이트 개편 시 사라진 URL의 리다이렉트·목적지 확인 +│ ├── check-redirects.py # 사이트 개편 시 사라진 URL의 리다이렉트·목적지 확인 +│ ├── check-code-blocks.py # 문서 코드블록 문법·스키마 검사 (L1·L2) +│ ├── check-code-refs.py # 문서가 인용하는 외부 참조 실재 확인 (L3) +│ └── example-e2e.sh # samples/ 실습을 실제로 돌려 재현 확인 (L4) │ dry-run/ └── run-dryrun.sh # OpenWave 프로필 드라이런 오케스트레이터 @@ -362,6 +365,81 @@ bash .claude/scripts/sync-output-samples.sh docs/ 파일 저장 시 `cd agents/` 블록 직전 `:::tip 실행 전 확인`이 없으면 즉시 경고 출력. +### `check-code-blocks.py`: 코드블록 문법·스키마 검사 (L1·L2) + +독자가 복사해 쓰는 예시가 깨지지 않았는지 본다. yaml·json·xml·toml 을 파서에 넣고, +GitHub Actions 는 actionlint, GitLab CI 는 구조 검사, bash 는 `bash -n` 과 shellcheck 로 검사한다. + +```bash +python3 .claude/scripts/check-code-blocks.py # 전체 검사 +python3 .claude/scripts/check-code-blocks.py --stats # 인벤토리만 +python3 .claude/scripts/check-code-blocks.py --selftest # 검사기가 실제로 도는지 확인 +``` + +통과할 수 없는 블록은 펜스에 `validate=skip` 을 단다(예: ` ```yaml validate=skip `). +안티패턴 예시나 `<다이제스트>` 같은 자리표시자가 든 블록에만 쓴다. 현재 2개 블록에 달려 있고 +ko·en 양쪽이라 파일에서는 4곳으로 잡힌다. + +`website/reference/samples/` 는 검사하지 않는다. `output-sample/` 에서 생성되는 파생물이라 +거기서 고치면 다음 재생성에 원복된다. 원본인 `output-sample/` 을 검사한다. + +`--selftest` 는 일부러 깨뜨린 블록을 넣어 검출되는지 본다. 결과가 0건일 때 그것이 진짜 0인지 +검사기가 아무것도 안 본 것인지 구분하기 위해서다. CI 도 본 검사 앞에 이것을 먼저 돌린다. + +같은 이유로 도구가 없어 돌지 못한 검사가 있으면 통과로 세지 않고 실패한다. 필요한 것은 +Python 3.11 이상(toml 검사용 tomllib), PyYAML, actionlint, shellcheck 넷이다. 로컬에서 일부를 +갖추지 못했다면 `--allow-missing-tools` 로 넘길 수 있지만, 그때 무엇을 못 봤는지 출력에 남는다. +CI 는 이 플래그를 쓰지 않는다. + +### `check-code-refs.py`: 외부 참조 실재 확인 (L3) + +`uses:` 가 가리키는 액션 태그와 설치 스크립트 URL 이 실제로 있는지 확인한다. +actionlint 는 문법만 보고 참조 실재는 확인하지 않아서 따로 둔다. + +```bash +python3 .claude/scripts/check-code-refs.py # 전량 +python3 .claude/scripts/check-code-refs.py --changed origin/main # 변경분만 +``` + +문서를 고치지 않아도 깨질 수 있다. 태그가 삭제되거나 URL 이 옮겨 가면 실패한다. 그래서 +PR 게이트(`pre-merge.yml` Layer 5)는 변경분만 보고, 전량 확인은 `example-refs.yml` 이 +주간 예약으로 돌려 실패 시 이슈만 만든다. 병합은 막지 않는다. + +### `example-e2e.sh`: samples 실습 재현 확인 (L4) + +`samples/` 세 프로젝트를 문서에 적힌 순서대로 실제로 돌려 기대한 결과가 나오는지 본다. +앞 계층이 문법만 보는 것과 달리 도구를 실행한다. + +```bash +bash .claude/scripts/example-e2e.sh # 전체 +bash .claude/scripts/example-e2e.sh --selftest # 단언이 실제로 검출하는지 확인 +bash .claude/scripts/example-e2e.sh --no-network # 네트워크가 필요한 항목을 건너뛴다 +``` + +단언은 종료 코드가 아니라 값이다. java 컴포넌트 4개 이상, python 5개 이상, 생성한 SBOM 을 +grype 가 실제로 파싱하는지, java 스캔에 `GHSA-jfh8-c2jp-5v3q` 가 있는지, nodejs 는 설치 후 +컴포넌트가 0보다 큰지와 `vendor/legacy-parser` 에 license 필드가 없는지를 본다. + +종료 코드를 믿지 않는 이유가 있다. macOS 의 Docker 는 공유 목록에 없는 호스트 경로를 +마운트하면 오류 대신 빈 디렉터리를 붙인다. 그러면 syft 는 종료 코드 0 으로 컴포넌트가 하나도 +없는 SBOM 을 내놓는다. `--selftest` 가 빈 입력을 넣어 이 상황이 실패로 판정되는지 확인한다. + +자동 실행은 `example-e2e.yml` 이 맡는다. 주간 예약(월요일 03:30 UTC)과 수동 실행, +그리고 `samples/**`·`docs/05-tools/**`·이 스크립트와 워크플로 자신을 건드리는 PR 에서 돈다. +PR 게이트로 상시 도는 계층이 아닌 이유는 grype 취약점 DB 가 2.0GB 단일 파일이라 내려받는 데만 +7분이 넘고 컨테이너 이미지 넷과 외부 API 에 의존하기 때문이다. 실패하면 이슈를 만들되 PR 에서는 +체크에 이미 보이므로 만들지 않는다. + +도구 버전은 태그로 고정한다(syft v1.51.1, grype v0.118.0). 이 조합이 CycloneDX 1.7 을 내고 +읽는 짝이다. 낮은 grype 를 쓰면 `sbom format not recognized` 로 이 검사가 걸린다. + +`GRYPE_DB_CACHE` 에 디렉터리를 주면 취약점 DB 를 재사용한다. + +작업 디렉터리는 저장소 안 `.example-e2e-tmp/` 에 만든다(gitignore 대상, 종료 시 삭제). +`mktemp -d` 를 쓰지 않는 이유는 macOS 에서 그 경로가 `/var/folders` 아래라 Docker Desktop 의 +공유 목록에 없기 때문이다. 마운트가 빈 디렉터리로 붙어 실습이 통째로 실패한다. 이 위치를 +옮기지 마라. + --- ## 5. 시나리오별 사용 예시 diff --git a/.claude/scripts/check-code-blocks.py b/.claude/scripts/check-code-blocks.py new file mode 100644 index 00000000..ccb71847 --- /dev/null +++ b/.claude/scripts/check-code-blocks.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 +"""문서의 코드블록을 문법(L1)과 스키마(L2) 수준에서 검사한다. + +독자가 그대로 복사해 실행하는 예시가 깨져 있으면 가이드 전체의 신뢰가 무너진다. +2026-09 진단에서 워크플로 하나가 YAML 로 파싱조차 되지 않는 상태로 발행돼 있었고, +액션 태그 하나는 실존하지 않는 참조였다. 사람이 매번 확인할 수 없으니 상시 검사한다. + +검사 계층 + L1 문법 yaml / json / xml / toml 을 파서에 넣는다. + L2 스키마 GitHub Actions 는 actionlint, GitLab CI 는 구조 검사, + bash 는 `bash -n` 과 shellcheck 를 돌린다. + +외부 참조(액션 태그 실재, 설치 URL 응답)는 네트워크가 필요하므로 +check-code-refs.py 가 따로 맡는다. + +사용법: + python3 .claude/scripts/check-code-blocks.py # 전체 검사 + python3 .claude/scripts/check-code-blocks.py -v # 블록별 집계까지 + python3 .claude/scripts/check-code-blocks.py --stats # 인벤토리만 출력 + python3 .claude/scripts/check-code-blocks.py --selftest # 검사기가 실제로 도는지 확인 + +검사에서 빼는 방법: + 펜스에 validate=skip 을 단다. 예: ```yaml validate=skip + 안티패턴 예시나 자리표시자가 든 블록처럼 통과할 수 없는 것에만 쓴다. +""" + +import argparse +import json +import re +import shutil +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ET +from pathlib import Path + +# PyYAML 이 없는 채로 진행하면 yaml 블록을 한 건도 보지 않고 통과한다. +# check-kwg-drift.py 와 같은 방식으로 즉시 멈춘다. CI 는 static-verify 에서 미리 넣는다. +try: + import yaml +except ImportError: + print("PyYAML 이 없다. 의존성을 갖춘 뒤 다시 실행하라 (pyyaml).") + sys.exit(2) + +ROOT = Path(__file__).resolve().parents[2] + +# 검사 대상 경로. 셸 변수로 넘기면 zsh 가 단어 분리를 하지 않아 통째로 한 인자가 되고 +# 결과가 조용히 0건이 된다(2026-09 K17 에서 실제로 겪었다). 파이썬 리스트로 고정한다. +TARGETS = [ + "docs", + "website/ai-coding", + "website/devsecops", + "website/reference", + "website/i18n", + "samples", + "agents", + "templates", + "output-sample", +] + +# 어느 깊이에 있든 제외한다. +# build, node_modules 빌드 산출물. 원본을 고쳐도 여기 사본은 남아 이중 보고가 된다. +# _plan 로컬 작업 노트. 문서가 아니다. +# .claude 세션 기록. +EXCLUDE_DIRS = {"build", "node_modules", "_plan", ".claude", ".docusaurus"} + +# 파생물. output-sample/ 이 원본이고 update-reference-samples 스킬이 이걸 만든다. +# 스킬은 코드블록 내부를 바꾸지 않으므로(변환 규칙 4) 원본만 검사하면 충분하다. +# 여기서 위반을 고치면 다음 재생성에서 원복돼 CI 가 같은 위반을 되풀이 보고한다. +DERIVED_PREFIXES = ( + "website/reference/samples/", + "website/i18n/en/docusaurus-plugin-content-docs-reference/current/samples/", +) + +EXTS = {".md", ".mdx"} + +# 문서 안내용이라 결함이 아닌 shellcheck 규칙 +# SC2164 `cd foo` 뒤에 `|| exit` 이 없다. 독자 안내 블록에는 붙이지 않는다. +# SC2148 셔뱅이 없다. 블록을 떼어냈으니 당연하다. +# SC1083 중괄호. GitHub Actions 표현식이 섞이면 오탐이다. +# SC2034 변수를 쓰지 않았다. 앞뒤 문맥이 잘린 발췌에서는 늘 뜬다. +# SC2154 변수에 값이 없다. 위와 같은 이유다. +# SC2046(인용 없는 명령 치환)은 실제 결함이라 남긴다. `$(pwd)` 미인용이 여기서 잡힌다. +SHELLCHECK_EXCLUDE = "SC2164,SC2148,SC1083,SC2034,SC2154" + + +class Block: + __slots__ = ("path", "line", "lang", "meta", "code") + + def __init__(self, path, line, lang, meta, code): + self.path = path + self.line = line + self.lang = lang + self.meta = meta + self.code = code + + @property + def where(self): + return f"{self.path}:{self.line}" + + @property + def skipped(self): + return "validate=skip" in self.meta + + +def iter_files(): + for target in TARGETS: + base = ROOT / target + if not base.is_dir(): + continue + for path in sorted(base.rglob("*")): + if path.suffix not in EXTS: + continue + rel = path.relative_to(ROOT) + if EXCLUDE_DIRS & set(rel.parts): + continue + posix = rel.as_posix() + if posix.startswith(DERIVED_PREFIXES): + continue + yield rel, path + + +def extract(rel, path): + """중첩 펜스를 스택으로 처리해 바깥 블록만 센다. + + 4-백틱 안에 3-백틱이 든 예시가 문서에 있어서, 단순 짝 맞추기로는 어긋난다. + """ + lines = path.read_text(encoding="utf-8").split("\n") + stack = [] + out = [] + for i, line in enumerate(lines): + m = re.match(r"^(\s*)(`{3,})\s*([A-Za-z0-9_+-]*)\s*(.*)$", line) + if not m: + continue + _, ticks, lang, meta = m.groups() + if stack and len(ticks) >= len(stack[-1][1]) and not lang and not meta.strip(): + start, _, slang, smeta = stack.pop() + if not stack: + out.append( + Block(rel.as_posix(), start + 1, slang, smeta, + "\n".join(lines[start + 1:i])) + ) + else: + stack.append((i, ticks, lang, meta)) + return out + + +def is_gha(code): + return bool(re.search(r'^\s*(on|"on"):', code, re.M)) and bool( + re.search(r"^\s*jobs:", code, re.M) + ) + + +def is_compose(data): + """docker-compose 를 GitLab CI 로 오분류하지 않는다. + + tools-setup.md 의 Dependency-Track compose 파일이 services 아래 image 를 갖는데, + stages 없는 GitLab 잡과 모양이 비슷해 진단 때 실제로 오분류됐다. + """ + if not isinstance(data, dict) or "services" not in data: + return False + services = data["services"] + if not isinstance(services, dict): + return False + return any(isinstance(v, dict) and "image" in v for v in services.values()) + + +def is_gitlab(code, data): + if is_compose(data): + return False + return bool(re.search(r"^\s*stages:", code, re.M)) or bool( + re.search(r"^\s*(image|script):", code, re.M) + ) + + +def strip_json_comments(code): + """선두의 `// 파일명` 주석 줄을 걷어낸다. + + renovate.json 은 JSON5 주석을 실제로 허용하고, SARIF·grype 발췌 블록도 + 파일명을 주석으로 붙여 둔다. 엄격 파서에 그대로 넣으면 전부 실패한다. + """ + out = [] + for line in code.split("\n"): + if not out and line.strip().startswith("//"): + continue + out.append(line) + return "\n".join(out) + + +def check_yaml(block, findings): + try: + data = yaml.safe_load(block.code) + except Exception as exc: + findings.append((block.where, "yaml", str(exc).split("\n")[0])) + return None + return data + + +def check_json(block, findings): + try: + json.loads(strip_json_comments(block.code)) + except Exception as exc: + findings.append((block.where, "json", str(exc))) + + +def check_xml(block, findings): + """조각을 가짜 루트로 감싼다. + + pom.xml 발췌는 가 여럿이라 루트가 하나가 아니다. 조각인 것이 + 정상이므로 감싼 뒤 파싱한다. 그래도 실패하면 진짜 문법 오류다. + """ + try: + ET.fromstring(f"{block.code}") + except Exception as exc: + findings.append((block.where, "xml", str(exc))) + + +def check_toml(block, findings, skips): + try: + import tomllib + except ImportError: + skips.add("toml 검사 (tomllib 없음, Python 3.11 이상 필요)") + return + try: + tomllib.loads(block.code) + except Exception as exc: + findings.append((block.where, "toml", str(exc))) + + +def check_gitlab(block, data, findings): + reserved = { + "stages", "variables", "default", "include", "workflow", + "image", "services", "before_script", "after_script", "cache", + } + if not isinstance(data, dict): + return + stages = data.get("stages") + for name, job in data.items(): + if name in reserved or not isinstance(job, dict): + continue + if not {"script", "trigger", "extends"} & set(job): + findings.append((block.where, "gitlab", f'잡 "{name}" 에 script 가 없다')) + stage = job.get("stage") + if stages is not None and stage is not None and stage not in stages: + findings.append( + (block.where, "gitlab", + f'잡 "{name}" 의 stage "{stage}" 가 stages 목록에 없다 {stages}') + ) + + +def run_actionlint(blocks, findings, verbose, skips): + if not shutil.which("actionlint"): + skips.add("GitHub Actions 스키마 검사 (actionlint 없음)") + return False + with tempfile.TemporaryDirectory() as tmp: + wf = Path(tmp) / ".github" / "workflows" + wf.mkdir(parents=True) + index = {} + for n, block in enumerate(blocks, 1): + name = f"wf{n:03d}.yml" + (wf / name).write_text(block.code + "\n", encoding="utf-8") + index[name] = block.where + # actionlint 는 git 저장소 안에서만 워크플로 디렉터리를 찾는다 + subprocess.run(["git", "init", "-q"], cwd=tmp, capture_output=True) + proc = subprocess.run( + ["actionlint", "-no-color", "-oneline", + "-shellcheck=", "-pyflakes="], + cwd=tmp, capture_output=True, text=True, + ) + for line in proc.stdout.strip().split("\n"): + if not line: + continue + m = re.match(r"^\.github/workflows/(wf\d+\.yml):(\d+):\d+: (.*)$", line) + if m: + findings.append((index[m.group(1)], "actionlint", m.group(3))) + else: + findings.append(("(actionlint)", "actionlint", line)) + if verbose: + print(f" actionlint: GitHub Actions 블록 {len(blocks)}개 검사") + return True + + +def run_bash(blocks, findings, verbose, skips): + have_shellcheck = shutil.which("shellcheck") is not None + if not have_shellcheck: + skips.add("bash 정적 분석 (shellcheck 없음)") + with tempfile.TemporaryDirectory() as tmp: + index = {} + paths = [] + for n, block in enumerate(blocks, 1): + p = Path(tmp) / f"b{n:04d}.sh" + p.write_text("#!/bin/bash\n" + block.code + "\n", encoding="utf-8") + index[p.name] = block.where + paths.append(p) + proc = subprocess.run(["bash", "-n", str(p)], capture_output=True, text=True) + if proc.returncode: + first = proc.stderr.strip().split("\n")[0] + findings.append((block.where, "bash -n", first.split(": ", 1)[-1])) + if have_shellcheck and paths: + proc = subprocess.run( + ["shellcheck", "-f", "gcc", "-S", "warning", + "-e", SHELLCHECK_EXCLUDE] + [str(p) for p in paths], + capture_output=True, text=True, + ) + for line in proc.stdout.strip().split("\n"): + m = re.match(r"^(.*?/)?(b\d+\.sh):(\d+):\d+: \w+: (.*)$", line) + if m and m.group(2) in index: + findings.append((index[m.group(2)], "shellcheck", m.group(4))) + if verbose: + print(f" bash: {len(blocks)}개 블록에 bash -n" + f"{' 과 shellcheck' if have_shellcheck else ''} 실행") + + +def collect_blocks(): + blocks = [] + for rel, path in iter_files(): + blocks.extend(extract(rel, path)) + return blocks + + +def analyse(blocks, findings, verbose, skips): + counts = {} + gha, gitlab_pairs, bash_blocks = [], [], [] + for block in blocks: + counts[block.lang or "(표기없음)"] = counts.get(block.lang or "(표기없음)", 0) + 1 + if block.skipped: + continue + lang = block.lang + if lang == "yaml": + data = check_yaml(block, findings) + if data is None: + continue + if is_gha(block.code): + gha.append(block) + elif is_gitlab(block.code, data): + gitlab_pairs.append((block, data)) + elif lang == "json": + check_json(block, findings) + elif lang == "xml": + check_xml(block, findings) + elif lang == "toml": + check_toml(block, findings, skips) + elif lang == "bash": + bash_blocks.append(block) + + for block, data in gitlab_pairs: + check_gitlab(block, data, findings) + if gha: + run_actionlint(gha, findings, verbose, skips) + if bash_blocks: + run_bash(bash_blocks, findings, verbose, skips) + return counts, len(gha), len(gitlab_pairs), len(bash_blocks) + + +def selftest(): + """검사기가 실제로 도는지 확인한다. + + 결과가 0건일 때 그것이 진짜 0인지 검사기가 아무것도 안 본 것인지 구분되지 + 않으면 검사가 무의미하다. 일부러 깨뜨린 블록을 넣어 잡히는지 본다. + """ + cases = [ + ("yaml", "a:\n b: c\n bad-indent: x", "yaml"), + ("json", '{"a": 1,}', "json"), + ("bash", 'if [ -z "$x" ]; then\necho hi', "bash -n"), + # SC2046 은 제외 목록에 넣지 않았다는 것을 고정한다. + # K17 이 고친 `$(pwd)` 미인용이 다시 들어오면 여기서 막힌다. + ("bash", "docker run --rm -v $(pwd):/x img", "shellcheck"), + ] + ok = True + print("[셀프테스트] 일부러 깨뜨린 블록이 잡히는지 확인한다") + for lang, code, kind in cases: + findings = [] + block = Block("", 1, lang, "", code) + analyse([block], findings, False, set()) + hit = any(f[1] == kind for f in findings) + print(f" {'OK ' if hit else 'FAIL'} {lang} 위반 -> {kind} 검출 " + f"{'됨' if hit else '안 됨'}") + ok &= hit + + findings = [] + block = Block("", 1, "yaml", "validate=skip", "a:\n b: c\n bad: x") + analyse([block], findings, False, set()) + skipped_ok = not findings + print(f" {'OK ' if skipped_ok else 'FAIL'} validate=skip 블록은 건너뛴다") + ok &= skipped_ok + return 0 if ok else 1 + + +def main(): + ap = argparse.ArgumentParser(description="코드블록 문법·스키마 검사 (L1·L2)") + ap.add_argument("-v", "--verbose", action="store_true") + ap.add_argument("--stats", action="store_true", help="인벤토리만 출력한다") + ap.add_argument("--selftest", action="store_true", help="검사기 자체를 확인한다") + ap.add_argument("--allow-missing-tools", action="store_true", + help="도구가 없어 돌지 못한 검사를 실패로 세지 않는다 (로컬 전용)") + args = ap.parse_args() + + if args.selftest: + return selftest() + + blocks = collect_blocks() + if not blocks: + print("FAIL: 코드블록을 하나도 찾지 못했다. 대상 경로 설정을 확인하라.") + return 1 + + skipped = [b for b in blocks if b.skipped] + findings = [] + skips = set() + counts, n_gha, n_gitlab, n_bash = analyse(blocks, findings, args.verbose, skips) + + print(f"[코드블록 검사] 파일에서 블록 {len(blocks)}개 수집 " + f"(validate=skip {len(skipped)}개 제외)") + if args.verbose or args.stats: + for lang, n in sorted(counts.items(), key=lambda kv: -kv[1]): + print(f" {lang:14} {n}") + print(f" GitHub Actions {n_gha} / GitLab CI {n_gitlab} / bash {n_bash}") + for block in skipped: + print(f" skip: {block.where} ({block.lang})") + if args.stats: + return 0 + + if findings: + print(f" FAIL: {len(findings)}건") + for where, kind, msg in findings: + print(f" {where} [{kind}] {msg}") + return 1 + + # 도구가 없어 돌지 못한 검사가 있으면 통과로 세지 않는다. 검사기가 아무것도 보지 + # 않고 초록을 내는 것이 이 스크립트가 막으려는 실패 유형이다. + if skips and not args.allow_missing_tools: + print(f" FAIL: 돌지 못한 검사 {len(skips)}종") + for item in sorted(skips): + print(f" {item}") + print(" 도구를 갖추고 다시 실행하라. 로컬에서 일부러 건너뛰려면" + " --allow-missing-tools 를 준다.") + return 1 + + print(" PASS: 코드블록 문법·스키마 이상 없음") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/scripts/check-code-refs.py b/.claude/scripts/check-code-refs.py new file mode 100644 index 00000000..aec25a28 --- /dev/null +++ b/.claude/scripts/check-code-refs.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""문서가 인용하는 외부 참조가 실재하는지 확인한다 (L3). + +actionlint 는 워크플로 문법만 본다. `uses:` 가 가리키는 태그가 실제로 있는지는 +확인하지 않는다. 2026-09 진단에서 `aquasecurity/trivy-action@0.36.0` 이 7곳에 +있었는데 실제 태그는 `v0.36.0` 이었고, 문법 검사는 전부 통과했다. 복사하면 액션 +해석 단계에서 바로 실패한다. 그래서 참조 실재를 따로 확인한다. + +문서를 고치지 않아도 깨질 수 있는 검사라는 점이 앞의 계층과 다르다. 태그가 삭제 +되거나 설치 URL 이 옮겨 가면 우리 잘못 없이 빨간불이 된다. 그래서 PR 게이트에서는 +변경된 파일만 보고, 전량 확인은 주간 예약으로 돌린다. + +사용법: + python3 .claude/scripts/check-code-refs.py # 전량 + python3 .claude/scripts/check-code-refs.py --changed BASE # BASE 이후 변경분만 + python3 .claude/scripts/check-code-refs.py -v +""" + +import argparse +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + +TARGETS = [ + "docs", + "website/ai-coding", + "website/devsecops", + "website/reference", + "website/i18n", + "samples", + "agents", + "templates", + "output-sample", +] +EXCLUDE_DIRS = {"build", "node_modules", "_plan", ".claude", ".docusaurus"} +DERIVED_PREFIXES = ( + "website/reference/samples/", + "website/i18n/en/docusaurus-plugin-content-docs-reference/current/samples/", +) +EXTS = {".md", ".mdx"} + +# 자리표시자는 실재를 확인할 수 없다. 문서가 일부러 비워 둔 자리다. +PLACEHOLDER = re.compile(r"[<>{}\[\]]|커밋 SHA|commit SHA") + +# "독자가 자기 것으로 바꿔 쓰라"는 뜻의 가상 소유자. 실재하지 않는 것이 정상이다. +PLACEHOLDER_OWNERS = { + "owner", "your-org", "your-organization", "myorg", "my-org", + "example", "example-org", "ORG", "org", +} + +USES = re.compile( + r"uses:\s*([A-Za-z0-9._-]+/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._/-]+)?)@([A-Za-z0-9._-]+)" +) +# 설치 스크립트로 널리 쓰이는 형태만 본다. 문서 안의 일반 링크는 대상이 아니다. +INSTALL_URL = re.compile( + r"(https://(?:get\.anchore\.io/[a-z]+|raw\.githubusercontent\.com/[^\s'\"`)]+\.sh))" +) + + +def iter_files(changed=None): + if changed is not None: + for rel in changed: + path = ROOT / rel + if path.suffix in EXTS and path.is_file(): + yield rel, path + return + for target in TARGETS: + base = ROOT / target + if not base.is_dir(): + continue + for path in sorted(base.rglob("*")): + if path.suffix not in EXTS: + continue + rel = path.relative_to(ROOT) + if EXCLUDE_DIRS & set(rel.parts): + continue + if rel.as_posix().startswith(DERIVED_PREFIXES): + continue + yield rel.as_posix(), path + + +def changed_files(base): + proc = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=ACMR", f"{base}...HEAD"], + cwd=ROOT, capture_output=True, text=True, + ) + if proc.returncode: + print(f" 경고: git diff 실패({base}). 전량 검사로 전환한다.") + return None + out = [] + for rel in proc.stdout.split(): + if EXCLUDE_DIRS & set(Path(rel).parts): + continue + if rel.startswith(DERIVED_PREFIXES): + continue + out.append(rel) + return out + + +def collect(files): + refs, urls = {}, {} + for rel, path in files: + text = path.read_text(encoding="utf-8") + for i, line in enumerate(text.split("\n"), 1): + for m in USES.finditer(line): + repo, tag = m.group(1), m.group(2) + if PLACEHOLDER.search(tag) or PLACEHOLDER.search(repo): + continue + if repo.split("/")[0] in PLACEHOLDER_OWNERS: + continue + refs.setdefault((repo, tag), []).append(f"{rel}:{i}") + for m in INSTALL_URL.finditer(line): + urls.setdefault(m.group(1), []).append(f"{rel}:{i}") + return refs, urls + + +def ref_exists(repo, tag): + """git ls-remote 로 확인한다. + + GitHub tags API 는 페이지네이션 때문에 태그가 많은 저장소에서 조용히 놓친다. + 진단 때 checkov-action@v12 를 그렇게 없는 것으로 잘못 판정했다. ls-remote 는 + 전체 ref 를 한 번에 주므로 그런 착오가 없다. + """ + base = "/".join(repo.split("/")[:2]) + try: + proc = subprocess.run( + ["git", "ls-remote", f"https://github.com/{base}"], + capture_output=True, text=True, timeout=60, + ) + except subprocess.TimeoutExpired: + # 조회 자체가 안 된 것과 태그가 없는 것은 다르다. 없다고 단정하지 않는다. + return None + if proc.returncode: + return None + pat = re.compile(rf"refs/(?:tags|heads)/{re.escape(tag)}$", re.M) + return bool(pat.search(proc.stdout)) + + +def url_ok(url): + proc = subprocess.run( + ["curl", "-sSL", "-o", "/dev/null", "-w", "%{http_code}", + "--max-time", "30", url], + capture_output=True, text=True, + ) + return proc.stdout.strip() == "200" + + +def main(): + ap = argparse.ArgumentParser(description="외부 참조 실재 확인 (L3)") + ap.add_argument("--changed", metavar="BASE", + help="BASE 이후 변경된 파일만 검사한다") + ap.add_argument("-v", "--verbose", action="store_true") + args = ap.parse_args() + + changed = changed_files(args.changed) if args.changed else None + if args.changed and changed is not None and not changed: + print("[외부 참조 확인] 변경된 문서가 없다") + return 0 + + files = list(iter_files(changed)) + refs, urls = collect(files) + scope = f"변경분 {len(files)}개 파일" if changed is not None else f"{len(files)}개 파일" + print(f"[외부 참조 확인] {scope} 에서 액션 참조 {len(refs)}종, 설치 URL {len(urls)}종") + + failures = [] + for (repo, tag), where in sorted(refs.items()): + result = ref_exists(repo, tag) + if result is None: + print(f" 조회 실패(네트워크): {repo}@{tag}") + continue + if result: + if args.verbose: + print(f" OK {repo}@{tag} ({len(where)}곳)") + else: + failures.append( + (f"{repo}@{tag}", "해결되는 태그·브랜치가 없다", where) + ) + + for url, where in sorted(urls.items()): + if url_ok(url): + if args.verbose: + print(f" OK {url} ({len(where)}곳)") + else: + failures.append((url, "HTTP 200 이 아니다", where)) + + if failures: + print(f" FAIL: {len(failures)}건") + for what, why, where in failures: + print(f" {what}: {why}") + for w in where[:5]: + print(f" {w}") + if len(where) > 5: + print(f" (외 {len(where) - 5}곳)") + return 1 + + if not refs and not urls: + print(" 확인할 외부 참조가 없다") + return 0 + print(" PASS: 모든 외부 참조가 실재한다") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/scripts/example-e2e.sh b/.claude/scripts/example-e2e.sh new file mode 100755 index 00000000..7339134d --- /dev/null +++ b/.claude/scripts/example-e2e.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# samples/ 실습을 문서에 적힌 그대로 끝까지 돌려 본다 (L4). +# +# 앞 계층은 예시가 문법적으로 성립하는지만 본다. 실제로 도구를 실행해 기대한 결과가 +# 나오는지는 여기서 확인한다. 2026-09 진단에서 syft·grype 버전이 어긋나 생성한 SBOM 을 +# 스캔 단계가 읽지 못하는 조합이 있었는데, 문법 검사로는 잡히지 않는 종류였다. +# +# 단언은 종료 코드가 아니라 값이다. 이유가 있다. macOS 의 Docker 는 공유 목록에 없는 +# 호스트 경로를 마운트하면 오류 대신 빈 디렉터리를 붙인다. 그러면 syft 는 종료 코드 0 으로 +# 유효하지만 컴포넌트가 하나도 없는 SBOM 을 내놓는다. 종료 코드만 보는 검사는 이걸 통과시킨다. +# 그래서 컴포넌트 수와 특정 취약점 ID 를 직접 확인한다. +# +# 사용법: +# bash .claude/scripts/example-e2e.sh # 전체 +# bash .claude/scripts/example-e2e.sh --selftest # 단언이 실제로 검출하는지 확인 +# bash .claude/scripts/example-e2e.sh --no-network # 네트워크가 필요한 항목을 건너뛴다 + +set -uo pipefail + +# 이미지는 태그를 고정한다. latest 는 언제든 다른 것을 가리킬 수 있고, +# 이 검사는 "지금 문서대로 하면 되는가" 를 보는 것이라 재현 가능해야 한다. +SYFT_IMAGE="anchore/syft:v1.51.1" +GRYPE_IMAGE="anchore/grype:v0.118.0" +NODE_IMAGE="node:22-bookworm-slim" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# 작업 디렉터리를 저장소 안에 만든다. macOS 의 Docker Desktop 은 공유 목록에 없는 경로를 +# 마운트하면 오류 대신 빈 디렉터리를 붙이는데, `mktemp -d` 가 주는 /var/folders 가 그렇다. +# 저장소는 이미 공유되는 위치라 여기에 두면 macOS 와 리눅스에서 같게 동작한다. +# .gitignore 에 등록돼 있다. +TMPBASE="$ROOT/.example-e2e-tmp" +WORK="$TMPBASE/$$" +mkdir -p "$WORK" + +# 컨테이너가 만든 파일이 root 소유로 남으면 러너 사용자가 지우지 못하고 Permission denied 가 +# 파일 수만큼 쏟아져 실제 오류를 덮는다. 컨테이너를 호출자 UID 로 돌려 예방하되, +# 그래도 남으면 컨테이너 안에서 지운다. +cleanup() { + [ -d "$WORK" ] || return 0 + rm -rf "$WORK" 2>/dev/null + if [ -d "$WORK" ] && command -v docker >/dev/null 2>&1; then + docker run --rm -v "$TMPBASE":/w "$NODE_IMAGE" rm -rf "/w/$$" >/dev/null 2>&1 + fi + rmdir "$TMPBASE" 2>/dev/null + return 0 +} +trap cleanup EXIT + +RAN=() +SKIPPED=() +FAILED=() + +ok() { RAN+=("$1"); printf ' OK %s\n' "$1"; } +fail() { FAILED+=("$1"); printf ' FAIL %s\n' "$1"; } +skip() { SKIPPED+=("$1"); printf ' 건너뜀 %s\n' "$1"; } + +syft() { docker run --rm -v "$1":/project "$SYFT_IMAGE" /project "${@:2}"; } + +# grype 취약점 DB 는 내려받는 데 몇 분이 걸린다. CI 에서 캐시 디렉터리를 넘겨 주면 +# 컨테이너 안 DB 경로에 붙여 재사용한다. 비어 있으면 매번 새로 받는다(로컬 기본값). +grype_scan() { + local args=(--rm -i) + if [ -n "${GRYPE_DB_CACHE:-}" ]; then + mkdir -p "$GRYPE_DB_CACHE" + args+=(-v "$GRYPE_DB_CACHE":/grype-db -e GRYPE_DB_CACHE_DIR=/grype-db) + fi + docker run "${args[@]}" "$GRYPE_IMAGE" "$@" +} + +# SBOM 의 컴포넌트 수를 센다. 파일 자체가 없거나 JSON 이 아니면 -1 을 돌려 +# "0개 탐지" 와 "생성 실패" 를 구분한다. +components() { + [ -s "$1" ] || { echo -1; return; } + jq -r '.components | length' "$1" 2>/dev/null || echo -1 +} + +require_tools() { + local missing=0 + for t in docker jq; do + command -v "$t" >/dev/null 2>&1 || { printf ' 필요한 도구가 없다: %s\n' "$t"; missing=1; } + done + docker info >/dev/null 2>&1 || { printf ' docker 데몬이 돌지 않는다\n'; missing=1; } + return $missing +} + +# 샘플 하나에 대해 SBOM 을 만들고 컴포넌트 수가 기준 이상인지 본다. +# 만든 경로는 SBOM_PATH 로 돌려준다. 명령 치환으로 받으면 ok() 출력까지 섞인다. +SBOM_PATH="" +check_sbom() { + local name="$1" min="$2" out="$WORK/$1.cdx.json" + SBOM_PATH="" + syft "$ROOT/samples/$name" --output cyclonedx-json > "$out" 2>/dev/null + local n; n=$(components "$out") + if [ "$n" -lt 0 ]; then + fail "$name SBOM 생성 (파일이 비었거나 JSON 이 아니다)" + return 1 + fi + if [ "$n" -lt "$min" ]; then + # 빈 디렉터리를 마운트했을 때 정확히 이 모양이 된다. 위 주석 참조. + fail "$name 컴포넌트 $n 개 (기대 $min 개 이상). 마운트가 비었는지 확인하라" + return 1 + fi + ok "$name 컴포넌트 $n 개 (기대 $min 개 이상)" + SBOM_PATH="$out" +} + +main_run() { + printf '[예제 E2E] syft %s / grype %s\n' "$SYFT_IMAGE" "$GRYPE_IMAGE" + + # 1. java: 컴포넌트와 Log4Shell 탐지 + local java_sbom + if check_sbom java-vulnerable 4; then + java_sbom="$SBOM_PATH" + # 생성한 SBOM 을 스캔 도구가 실제로 읽는지 본다. 버전이 어긋나면 + # "sbom format not recognized" 로 여기서 걸린다. + local scan="$WORK/java.scan.json" + if grype_scan -o json < "$java_sbom" > "$scan" 2>/dev/null \ + && [ -s "$scan" ]; then + ok "java SBOM 을 grype 가 파싱함" + if jq -e '[.matches[].vulnerability.id] | index("GHSA-jfh8-c2jp-5v3q")' \ + "$scan" >/dev/null 2>&1; then + ok "java 스캔에 Log4Shell(GHSA-jfh8-c2jp-5v3q) 포함" + else + fail "java 스캔에 Log4Shell(GHSA-jfh8-c2jp-5v3q) 없음" + fi + else + fail "java SBOM 을 grype 가 읽지 못함 (버전 조합 확인)" + fi + fi + + # 2. python: 컴포넌트만 본다. 라이선스 필드는 비어 있는 것이 정상이고 + # README 도 그렇게 적고 있다. + check_sbom python-mixed-license 5 + + # 3. nodejs: README 가 npm install 선행을 요구한다. 설치 전에는 컴포넌트가 0 이므로 + # 설치까지 해야 이 샘플의 실습이 재현된다. + if [ "${NO_NETWORK:-0}" = "1" ]; then + skip "nodejs 실습 (--no-network)" + else + local proj="$WORK/nodejs" + cp -R "$ROOT/samples/nodejs-unlicensed" "$proj" + # 호출자 UID 로 돌려 산출물이 root 소유가 되지 않게 한다. npm 은 쓸 수 있는 HOME 이 + # 필요하므로 컨테이너 안 경로를 준다. + if docker run --rm -v "$proj":/app -w /app \ + --user "$(id -u):$(id -g)" -e HOME=/tmp -e npm_config_cache=/tmp/.npm \ + "$NODE_IMAGE" npm install --no-audit --no-fund >/dev/null 2>&1; then + ok "nodejs 의존성 설치" + local out="$WORK/nodejs.cdx.json" + syft "$proj" --output cyclonedx-json > "$out" 2>/dev/null + local n; n=$(components "$out") + if [ "$n" -gt 0 ]; then + ok "nodejs 컴포넌트 $n 개 (설치 후)" + else + fail "nodejs 컴포넌트 $n 개 (설치 후 0 개는 실습이 성립하지 않는다)" + fi + # 이 샘플의 학습 지점. 로컬 vendor 패키지에 license 필드가 없어야 한다. + if jq -e '.license == null' "$proj/vendor/legacy-parser/package.json" >/dev/null 2>&1; then + ok "vendor/legacy-parser 에 license 필드 없음 (README 전제와 일치)" + else + fail "vendor/legacy-parser 에 license 필드가 생겼다. README 전제가 깨진다" + fi + else + fail "nodejs 의존성 설치 실패" + fi + fi +} + +# 단언이 정말 검출하는지 확인한다. 빈 디렉터리를 스캔해 "컴포넌트 0" 이 실패로 +# 판정되는지 본다. 이걸 통과시키면 마운트가 비어도 초록이 나온다는 뜻이다. +selftest() { + printf '[셀프테스트] 빈 입력이 실패로 판정되는지 확인한다\n' + local empty="$WORK/empty"; mkdir -p "$empty" + local out="$WORK/empty.cdx.json" + syft "$empty" --output cyclonedx-json > "$out" 2>/dev/null + local n; n=$(components "$out") + if [ "$n" -le 0 ]; then + printf ' OK 빈 디렉터리 -> 컴포넌트 %s 개로 집계됨 (단언이 걸러낸다)\n' "$n" + else + printf ' FAIL 빈 디렉터리에서 컴포넌트 %s 개가 나왔다. 집계가 잘못됐다\n' "$n" + return 1 + fi + # 없는 파일에 대해 -1 을 돌려 "생성 실패" 와 "0개 탐지" 를 구분하는지 + local missing; missing=$(components "$WORK/does-not-exist.json") + if [ "$missing" -eq -1 ]; then + printf ' OK 파일 없음 -> -1 (생성 실패와 0개 탐지를 구분한다)\n' + else + printf ' FAIL 파일이 없는데 %s 를 돌려줬다\n' "$missing" + return 1 + fi + return 0 +} + +NO_NETWORK=0 +MODE=run +for arg in "$@"; do + case "$arg" in + --selftest) MODE=selftest ;; + --no-network) NO_NETWORK=1 ;; + *) printf '알 수 없는 인자: %s\n' "$arg"; exit 2 ;; + esac +done +export NO_NETWORK + +if ! require_tools; then + printf 'FAIL: 필요한 도구를 갖추지 못해 아무것도 확인하지 못했다\n' + exit 1 +fi + +if [ "$MODE" = selftest ]; then + selftest; exit $? +fi + +main_run + +printf '\n[결과] 통과 %d / 실패 %d / 건너뜀 %d\n' \ + "${#RAN[@]}" "${#FAILED[@]}" "${#SKIPPED[@]}" +for s in "${SKIPPED[@]:-}"; do [ -n "$s" ] && printf ' 건너뜀: %s\n' "$s"; done +for f in "${FAILED[@]:-}"; do [ -n "$f" ] && printf ' 실패: %s\n' "$f"; done + +# 아무것도 돌리지 못했으면 통과로 세지 않는다. K18 에서 겪은 실패 유형이다. +if [ "${#RAN[@]}" -eq 0 ]; then + printf 'FAIL: 확인한 항목이 하나도 없다\n' + exit 1 +fi +[ "${#FAILED[@]}" -eq 0 ] || exit 1 +printf 'PASS: 문서대로 실습이 재현된다\n' diff --git a/.github/workflows/example-e2e.yml b/.github/workflows/example-e2e.yml new file mode 100644 index 00000000..fbd79a8e --- /dev/null +++ b/.github/workflows/example-e2e.yml @@ -0,0 +1,91 @@ +name: Example E2E + +# samples/ 실습을 문서에 적힌 그대로 끝까지 돌려 본다 (L4). +# +# PR 게이트로 두지 않는 이유가 있다. grype 취약점 DB 를 내려받는 데만 몇 분이 걸리고, +# 컨테이너 이미지 세 개를 받아야 하며, npm 레지스트리와 취약점 DB 서버에 의존한다. +# 문서와 무관한 이유로 PR 이 막히면 게이트가 신뢰를 잃는다. 그래서 주간 예약과 수동 실행을 +# 기본으로 두고, 실습 자체를 건드리는 PR 에서만 경로 한정으로 함께 돈다. + +on: + schedule: + # 매주 월요일 03:30 UTC (KST 12:30). example-refs 와 한 시간 띄운다. + - cron: "30 3 * * 1" + workflow_dispatch: + pull_request: + paths: + - "samples/**" + - "docs/05-tools/**" + - ".claude/scripts/example-e2e.sh" + - ".github/workflows/example-e2e.yml" + +permissions: + contents: read + issues: write + +jobs: + e2e: + runs-on: ubuntu-latest + # grype DB 내려받기가 가장 오래 걸린다. 실측 기준 7분을 넘겼고, 이미지 세 개와 + # npm 설치까지 더해지므로 넉넉히 잡는다. + timeout-minutes: 40 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run the sample walkthroughs + id: e2e + run: | + bash .claude/scripts/example-e2e.sh 2>&1 | tee e2e.log + exit "${PIPESTATUS[0]}" + + - name: Open an issue when the walkthrough stops reproducing + # 예약·수동 실행에서만 이슈를 만든다. PR 에서는 실패가 체크에 바로 보이므로 + # 이슈까지 만들면 중복이다. + if: failure() && github.event_name != 'pull_request' + uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + let log = '(로그를 읽지 못했다)'; + try { + log = fs.readFileSync('e2e.log', 'utf8').slice(-4000); + } catch (e) { + core.warning(`로그 읽기 실패: ${e.message}`); + } + const body = [ + '`example-e2e` 실행에서 samples/ 실습이 문서대로 재현되지 않았다.', + '', + '문서를 고치지 않아도 발생할 수 있다. 도구 버전이 올라가면서 출력 형식이', + '바뀌었거나, 취약점 DB 에서 기대하던 항목이 빠졌을 수 있다.', + '로그의 실패 항목을 확인하고 문서나 고정 태그를 갱신하라.', + '', + '```', + log, + '```', + '', + `실행: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + ].join('\n'); + + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'example-e2e', + }); + if (existing.data.length > 0) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.data[0].number, + body, + }); + return; + } + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'samples/ 실습이 문서대로 재현되지 않는다', + body, + labels: ['example-e2e'], + }); diff --git a/.github/workflows/example-refs.yml b/.github/workflows/example-refs.yml new file mode 100644 index 00000000..5053a3cd --- /dev/null +++ b/.github/workflows/example-refs.yml @@ -0,0 +1,85 @@ +name: Example refs + +# 문서가 인용하는 액션 태그와 설치 URL 이 아직 실재하는지 주기적으로 확인한다. +# 이 검사는 우리가 문서를 고치지 않아도 깨진다. 태그가 삭제되거나 URL 이 옮겨 가면 +# 우리 잘못 없이 실패한다. 그래서 PR 게이트로 두지 않고 예약으로 돌리며, +# 실패해도 병합을 막지 않고 이슈만 남긴다. PR 에서는 pre-merge 의 Layer 5 가 +# 이번 변경으로 들어온 참조만 확인한다. + +on: + schedule: + # 매주 월요일 02:30 UTC (KST 11:30) + - cron: "30 2 * * 1" + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + refs: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Check every external reference + id: check + run: | + python3 .claude/scripts/check-code-refs.py -v | tee refs.log + exit "${PIPESTATUS[0]}" + + - name: Open an issue when a reference stops resolving + if: failure() + uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + let log = '(로그를 읽지 못했다)'; + try { + log = fs.readFileSync('refs.log', 'utf8').slice(-4000); + } catch (e) { + core.warning(`로그 읽기 실패: ${e.message}`); + } + const title = '문서가 인용하는 외부 참조가 해결되지 않는다'; + const body = [ + '`example-refs` 예약 실행에서 실패했다.', + '', + '문서를 고치지 않아도 발생할 수 있다. 액션 태그가 삭제됐거나', + '설치 URL 이 옮겨 갔을 수 있다. 아래 로그의 참조를 확인하고', + '현행 값으로 갱신하라.', + '', + '```', + log, + '```', + '', + `실행: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + ].join('\n'); + + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'example-refs', + }); + if (existing.data.length > 0) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.data[0].number, + body, + }); + return; + } + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['example-refs'], + }); diff --git a/.github/workflows/notify-ai-coding-update.yml b/.github/workflows/notify-ai-coding-update.yml index 23bd2b04..840cfb4d 100644 --- a/.github/workflows/notify-ai-coding-update.yml +++ b/.github/workflows/notify-ai-coding-update.yml @@ -23,9 +23,11 @@ jobs: id: changed run: | FILES=$(git diff --name-only HEAD~1 HEAD -- agents/ai-coding-setup/ website/ai-coding/ | head -20 | sed 's/^/- /' | tr '\n' '\n') - echo "files<> $GITHUB_OUTPUT - echo "$FILES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + { + echo "files<> "$GITHUB_OUTPUT" - name: Create issue in ai-coding-best-practice env: diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index de770f1e..e1f324a7 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -87,6 +87,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + # Layer 5 가 base 와 비교해 변경분을 고른다. 얕은 클론이면 base 가 없다. + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 @@ -112,7 +115,8 @@ jobs: NODE_OPTIONS: --max_old_space_size=8192 - name: Install Python dependencies - run: pip install anthropic + # anthropic 은 Layer 3 카세트 재생, pyyaml 은 Layer 4 코드블록 검사에 쓴다. + run: pip install anthropic pyyaml - name: Layer 1 — Agent 스펙 구조 run: python3 .claude/scripts/test-agent-specs.py @@ -131,3 +135,29 @@ jobs: - name: output/ 산출물 완전성 run: python3 .claude/scripts/validate-output.py + + # 문서 코드블록 검사에 쓴다. 이동 태그 대신 버전을 고정한다. + - name: Install actionlint + env: + ACTIONLINT_VERSION: 1.7.12 + run: | + curl -sSfL -o actionlint.tar.gz \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + tar -xzf actionlint.tar.gz actionlint + sudo mv actionlint /usr/local/bin/ + actionlint --version + + # 검사기가 실제로 도는지 먼저 확인한다. 결과가 0건일 때 그것이 진짜 0인지 + # 검사기가 아무것도 안 본 것인지 구분되지 않으면 이 단계는 의미가 없다. + - name: Layer 4 자체 점검 + run: python3 .claude/scripts/check-code-blocks.py --selftest + + - name: Layer 4 코드블록 문법 + run: python3 .claude/scripts/check-code-blocks.py + + # 외부 참조는 우리가 문서를 고치지 않아도 깨질 수 있다. PR 에서는 이번 변경으로 + # 들어온 참조만 본다. 전량 확인은 example-refs.yml 이 주간으로 돌린다. + - name: Layer 5 외부 참조 (변경분) + run: | + python3 .claude/scripts/check-code-refs.py \ + --changed "origin/${{ github.base_ref }}" diff --git a/.github/workflows/sync-agents.yml b/.github/workflows/sync-agents.yml index 848a8d26..215a0153 100644 --- a/.github/workflows/sync-agents.yml +++ b/.github/workflows/sync-agents.yml @@ -26,7 +26,7 @@ jobs: env: AGENTS_REPO_TOKEN: ${{ secrets.AGENTS_REPO_TOKEN }} run: | - git clone https://x-access-token:${AGENTS_REPO_TOKEN}@github.com/trustedoss/trustedoss-agents.git /tmp/trustedoss-agents + git clone "https://x-access-token:${AGENTS_REPO_TOKEN}@github.com/trustedoss/trustedoss-agents.git" /tmp/trustedoss-agents - name: Sync files run: | @@ -50,7 +50,7 @@ jobs: cd /tmp/trustedoss-agents git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${AGENTS_REPO_TOKEN}@github.com/trustedoss/trustedoss-agents.git + git remote set-url origin "https://x-access-token:${AGENTS_REPO_TOKEN}@github.com/trustedoss/trustedoss-agents.git" git add -A if [ -z "$(git status --porcelain)" ]; then echo "변경 없음 — sync 스킵" diff --git a/.gitignore b/.gitignore index 576fe21f..97f40578 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ venv/ .venv # 훅 생성 로컬 로그 -.claude/progress-infra.md +**/.claude/progress-infra.md # 브라우저 자동화 부산물 .playwright-mcp/ @@ -72,3 +72,6 @@ docs/_plan/ .claude/talk-script-ossummit-2026.md deck/STATUS.md POSITIONING.md + +# example-e2e.sh 작업 디렉터리 (실행 중에만 존재) +.example-e2e-tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad0cf9f1..1ed28176 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,20 +45,23 @@ Step 5 git push # 모든 검증 통과 후에 ## 검증 명령어 빠른 참조 -| 명령어 | 역할 | 소요시간 | -| ------------------------------------------------------------- | ------------------------------------------------------- | -------- | -| `/qa changed` | 변경 파일 품질 자동 검사·수정 | ~2분 | -| `bash .claude/scripts/verify.sh` | 정적 검증 13항목 일괄 실행 | ~30초 | -| `python3 .claude/scripts/test-coverage.py` | ISO G항목 커버리지 정합성 확인 | ~5초 | -| `python3 .claude/scripts/validate-output.py` | output/ 산출물 완전성 확인 | ~5초 | -| `python3 .claude/scripts/check-redirects.py <기준선> ` | 사이트 개편 시 사라진 URL의 리다이렉트 존재·목적지 확인 | ~5초 | -| `/kwg-check` | KWG 원본 싱크 상태 확인 | ~1분 | +| 명령어 | 역할 | 소요시간 | +| ------------------------------------------------------------- | ----------------------------------------------------------- | -------- | +| `/qa changed` | 변경 파일 품질 자동 검사·수정 | ~2분 | +| `bash .claude/scripts/verify.sh` | 정적 검증 13항목 일괄 실행 | ~30초 | +| `python3 .claude/scripts/test-coverage.py` | ISO G항목 커버리지 정합성 확인 | ~5초 | +| `python3 .claude/scripts/validate-output.py` | output/ 산출물 완전성 확인 | ~5초 | +| `python3 .claude/scripts/check-redirects.py <기준선> ` | 사이트 개편 시 사라진 URL의 리다이렉트 존재·목적지 확인 | ~5초 | +| `python3 .claude/scripts/check-code-blocks.py` | 문서 코드블록 문법·스키마 검사 (L1·L2) | ~40초 | +| `python3 .claude/scripts/check-code-refs.py` | 문서가 인용하는 액션 태그·설치 URL 실재 확인 (L3, 네트워크) | ~2분 | +| `bash .claude/scripts/example-e2e.sh` | samples/ 실습을 실제로 돌려 재현 확인 (L4, Docker) | ~15분 | +| `/kwg-check` | KWG 원본 싱크 상태 확인 | ~1분 | --- ## verify.sh FAIL 시 자주 발생하는 오류 -### [1/12] Docusaurus 빌드 실패 +### [1/13] Docusaurus 빌드 실패 ``` FAIL: Docusaurus 빌드 실패 @@ -69,7 +72,7 @@ FAIL: Docusaurus 빌드 실패 --- -### [2/12] 내부 링크 오류 +### [2/13] 내부 링크 오류 ``` FAIL: 깨진 링크 발견 @@ -80,7 +83,7 @@ FAIL: 깨진 링크 발견 --- -### [3/12] front matter YAML 오류 +### [3/13] front matter YAML 오류 ``` FAIL: front matter YAML 오류 @@ -91,7 +94,7 @@ FAIL: front matter YAML 오류 --- -### [5/12] 로컬 경로 노출 +### [5/13] 로컬 경로 노출 ``` FAIL: 로컬 사용자 경로 노출 @@ -102,7 +105,7 @@ FAIL: 로컬 사용자 경로 노출 --- -### [6/12] ISO 섹션 번호 형식 오류 +### [6/13] ISO 섹션 번호 형식 오류 ``` FAIL: 18974 섹션 번호 형식 오류 @@ -118,7 +121,7 @@ FAIL: 18974 섹션 번호 형식 오류 --- -### [7/12] agent 실행 admonition 누락 +### [7/13] agent 실행 admonition 누락 ``` FAIL: agent 실행 admonition 누락 @@ -228,20 +231,23 @@ Step 5 git push # Only after all checks pass ## Verification Command Reference -| Command | Role | Time | -| --------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------- | -| `/qa changed` | Auto-check and fix changed file quality | ~2 min | -| `bash .claude/scripts/verify.sh` | Run all 13 static validation checks | ~30 sec | -| `python3 .claude/scripts/test-coverage.py` | Verify ISO requirement coverage | ~5 sec | -| `python3 .claude/scripts/validate-output.py` | Verify output/ deliverable completeness | ~5 sec | -| `python3 .claude/scripts/check-redirects.py ` | During a site redesign, confirm every dropped URL has a redirect that resolves | ~5 sec | -| `/kwg-check` | Check sync status with KWG source | ~1 min | +| Command | Role | Time | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------- | +| `/qa changed` | Auto-check and fix changed file quality | ~2 min | +| `bash .claude/scripts/verify.sh` | Run all 13 static validation checks | ~30 sec | +| `python3 .claude/scripts/test-coverage.py` | Verify ISO requirement coverage | ~5 sec | +| `python3 .claude/scripts/validate-output.py` | Verify output/ deliverable completeness | ~5 sec | +| `python3 .claude/scripts/check-redirects.py ` | During a site redesign, confirm every dropped URL has a redirect that resolves | ~5 sec | +| `python3 .claude/scripts/check-code-blocks.py` | Check code block syntax and schema in the docs (L1, L2) | ~40 sec | +| `python3 .claude/scripts/check-code-refs.py` | Confirm the action tags and install URLs the docs cite still resolve (L3, network) | ~2 min | +| `bash .claude/scripts/example-e2e.sh` | Run the samples/ walkthroughs for real and check they reproduce (L4, Docker) | ~15 min | +| `/kwg-check` | Check sync status with KWG source | ~1 min | --- ## Common verify.sh FAIL Errors -### [1/12] Docusaurus Build Failure +### [1/13] Docusaurus Build Failure ``` FAIL: Docusaurus 빌드 실패 @@ -252,7 +258,7 @@ FAIL: Docusaurus 빌드 실패 --- -### [2/12] Broken Internal Links +### [2/13] Broken Internal Links ``` FAIL: 깨진 링크 발견 @@ -263,7 +269,7 @@ FAIL: 깨진 링크 발견 --- -### [3/12] Front Matter YAML Error +### [3/13] Front Matter YAML Error ``` FAIL: front matter YAML 오류 @@ -274,7 +280,7 @@ FAIL: front matter YAML 오류 --- -### [5/12] Local Path Exposed +### [5/13] Local Path Exposed ``` FAIL: 로컬 사용자 경로 노출 @@ -285,7 +291,7 @@ FAIL: 로컬 사용자 경로 노출 --- -### [6/12] ISO Section Number Format Error +### [6/13] ISO Section Number Format Error ``` FAIL: 18974 섹션 번호 형식 오류 @@ -301,7 +307,7 @@ FAIL: 18974 섹션 번호 형식 오류 --- -### [7/12] Missing Agent Execution Admonition +### [7/13] Missing Agent Execution Admonition ``` FAIL: agent 실행 admonition 누락 diff --git a/agents/05-sbom-guide/CLAUDE.md b/agents/05-sbom-guide/CLAUDE.md index 81ab6391..a57a9e36 100644 --- a/agents/05-sbom-guide/CLAUDE.md +++ b/agents/05-sbom-guide/CLAUDE.md @@ -66,14 +66,14 @@ Copyleft 리스크와 실제 CVE 취약점이 탐지된다. `[project-name]`은 분석 대상 프로젝트 이름으로 치환한다. 실행 전 `mkdir -p output/sbom`으로 출력 디렉토리를 미리 만들어 둔다. -| 언어 | 도구 | Docker 명령어 | -| ----------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| Java/Maven | cdxgen | `docker run --rm -v $(pwd):/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | -| Java/Gradle | cdxgen | `docker run --rm -v $(pwd):/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | -| Python | syft | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | -| Node.js | syft | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | -| Go | syft | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | -| 기타 | syft | syft 범용 스캔으로 대응(위 syft 명령과 동일한 형태). 결과가 비면 cdxgen 재시도 안내 | +| 언어 | 도구 | Docker 명령어 | +| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------- | +| Java/Maven | cdxgen | `docker run --rm -v "$(pwd)":/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | +| Java/Gradle | cdxgen | `docker run --rm -v "$(pwd)":/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | +| Python | syft | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | +| Node.js | syft | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | +| Go | syft | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | +| 기타 | syft | syft 범용 스캔으로 대응(위 syft 명령과 동일한 형태). 결과가 비면 cdxgen 재시도 안내 | ## 출력 산출물 diff --git a/agents/en/05-sbom-guide/CLAUDE.md b/agents/en/05-sbom-guide/CLAUDE.md index 33958d78..7d2e689d 100644 --- a/agents/en/05-sbom-guide/CLAUDE.md +++ b/agents/en/05-sbom-guide/CLAUDE.md @@ -70,14 +70,14 @@ Generate the commands that match the language and package manager. The commands and runnable as-is; substitute `[project-name]` with the name of the project being analyzed. Run `mkdir -p output/sbom` first to create the output directory. -| Language | Tool | Docker command | -| ----------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| Java/Maven | cdxgen | `docker run --rm -v $(pwd):/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | -| Java/Gradle | cdxgen | `docker run --rm -v $(pwd):/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | -| Python | syft | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | -| Node.js | syft | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | -| Go | syft | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | -| Other | syft | Use the general syft command above. If the result is empty, suggest trying cdxgen | +| Language | Tool | Docker command | +| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------- | +| Java/Maven | cdxgen | `docker run --rm -v "$(pwd)":/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | +| Java/Gradle | cdxgen | `docker run --rm -v "$(pwd)":/app ghcr.io/cyclonedx/cdxgen -o /app/output/sbom/[project-name].cdx.json /app` | +| Python | syft | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | +| Node.js | syft | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | +| Go | syft | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/[project-name].cdx.json` | +| Other | syft | Use the general syft command above. If the result is empty, suggest trying cdxgen | ## Output deliverables diff --git a/docs/04-process/index.md b/docs/04-process/index.md index 232cab06..0befb5b5 100644 --- a/docs/04-process/index.md +++ b/docs/04-process/index.md @@ -251,7 +251,7 @@ jobs: - uses: actions/checkout@v7 - name: SBOM 생성 run: | - docker run --rm -v $(pwd):/project \ + docker run --rm -v "$(pwd)":/project \ anchore/syft:latest /project \ --output cyclonedx-json > sbom.cdx.json - name: 라이선스 확인 diff --git a/docs/05-tools/sbom-generation/docker-cicd.md b/docs/05-tools/sbom-generation/docker-cicd.md index 24892a83..91973799 100644 --- a/docs/05-tools/sbom-generation/docker-cicd.md +++ b/docs/05-tools/sbom-generation/docker-cicd.md @@ -17,12 +17,12 @@ sidebar_label: 'Docker·CI/CD 실행 가이드' ## Docker로 syft 실행 — 언어/패키지매니저별 명령어 -| 언어 | 패키지매니저 | 명령어 | -| ------- | ------------ | ---------------------------------------------------------------------------------------------------- | -| Java | Maven/Gradle | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | -| Python | pip | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | -| Node.js | npm | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | -| Go | go mod | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| 언어 | 패키지매니저 | 명령어 | +| ------- | ------------ | ------------------------------------------------------------------------------------------------------ | +| Java | Maven/Gradle | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Python | pip | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Node.js | npm | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Go | go mod | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | 전체 명령어 (각 언어 동일, 디렉토리만 조정). `agents/05-sbom-guide/CLAUDE.md`가 실제로 생성하는 명령어와 동일한 형태입니다: @@ -32,7 +32,7 @@ mkdir -p output/sbom # syft로 SBOM 생성 docker run --rm \ - -v $(pwd):/src \ + -v "$(pwd)":/src \ anchore/syft \ dir:/src \ -o cyclonedx-json \ @@ -45,7 +45,7 @@ docker run --rm \ ```bash docker run --rm \ - -v $(pwd):/app \ + -v "$(pwd)":/app \ -w /app \ ghcr.io/cyclonedx/cdxgen:latest \ -o /app/output/sbom/sbom-cdxgen.cdx.json \ @@ -102,7 +102,7 @@ jobs: ```bash # java-vulnerable 샘플로 실습 docker run --rm \ - -v $(pwd)/samples/java-vulnerable:/src \ + -v "$(pwd)"/samples/java-vulnerable:/src \ anchore/syft \ dir:/src -o cyclonedx-json \ > output/sbom/java-vulnerable.cdx.json diff --git a/docs/05-tools/sbom-generation/index.md b/docs/05-tools/sbom-generation/index.md index aed42461..eb2126b4 100644 --- a/docs/05-tools/sbom-generation/index.md +++ b/docs/05-tools/sbom-generation/index.md @@ -110,7 +110,7 @@ FOSSLight, SW360, FOSSology 등 SCA·컴플라이언스 도구의 도입 및 활 | 필드 | 설명 | | ----------------------------- | ----------------------------------------------------------------------------------------- | -| `bomFormat`, `specVersion` | CycloneDX 포맷 식별자와 사양 버전. syft와 cdxgen 모두 기본 출력이 1.7입니다 | +| `bomFormat`, `specVersion` | CycloneDX 포맷 식별자와 사양 버전. cdxgen 12.x 와 syft 1.51 이상이 1.7 을 기본으로 냅니다 | | `metadata.timestamp` | SBOM 생성 시각 | | `metadata.tools.components[]` | SBOM을 만든 도구의 이름과 버전. CISA 2026 최소 요소의 "SBOM 생성 도구명"에 해당합니다 | | `metadata.lifecycles[]` | SBOM을 만든 수명주기 단계. "생성 맥락"에 해당합니다 | @@ -135,6 +135,13 @@ FOSSLight, SW360, FOSSology 등 SCA·컴플라이언스 도구의 도입 및 활 수명주기 단계나 해시가 필요한데 syft 출력에 비어 있다면 같은 프로젝트를 cdxgen으로 한 번 더 생성해 비교하세요. 사양 버전을 명시하려면 syft는 `-o cyclonedx-json@1.7`, cdxgen은 `--spec-version 1.7` 을 씁니다. +:::warning 도구 최소 버전을 먼저 확인하세요 +CycloneDX 1.7 은 syft 1.51 이상에서만 나옵니다. 그보다 낮은 syft 는 1.6 까지만 지원해 +`-o cyclonedx-json@1.7` 을 주면 `unsupported output format` 으로 실패합니다. +스캔에 쓰는 grype 도 1.7 을 읽으려면 0.118 이상이 필요합니다. 낮은 grype 에 1.7 SBOM 을 주면 +`sbom format not recognized` 로 끝납니다. `syft version` 과 `grype version` 으로 먼저 확인하세요. +::: + :::tip MCP 서버도 SBOM 에 담을 수 있습니다 AI 에이전트가 호출하는 MCP(Model Context Protocol, 에이전트가 외부 도구를 호출하는 프로토콜) 서버를 SBOM 에 등재하는 방법은 [에이전트와 MCP 도구 거버넌스](/ai-coding/agent-governance)에서 다룹니다. @@ -289,7 +296,7 @@ docker run --rm \ - [ ] `output/sbom/[project].cdx.json` 생성됨 - [ ] SBOM 파일에 `components` 배열이 비어있지 않음 -- [ ] `specVersion` 이 `1.7` 임 (낮은 값이면 도구 버전을 올리거나 사양 버전을 명시해 다시 생성) +- [ ] `specVersion` 이 `1.7` 임 (낮은 값이면 syft 를 1.51 이상으로 올린 뒤 다시 생성) - [ ] `metadata.timestamp` 와 `metadata.tools` 에 생성 시각과 생성 도구명이 기록됨 - [ ] `components[]` 각 항목에 `purl` 이 있음 - [ ] 컴포넌트 해시(`hashes`)와 라이선스(`licenses`) 상태를 확인함 (도구에 따라 비어 있을 수 있으며, 비어 있으면 cdxgen 출력과 비교) diff --git a/docs/05-tools/sbom-management/index.md b/docs/05-tools/sbom-management/index.md index f225962e..325f9486 100644 --- a/docs/05-tools/sbom-management/index.md +++ b/docs/05-tools/sbom-management/index.md @@ -184,6 +184,11 @@ jobs: 이 워크플로우를 통해 매주 최신 SBOM을 기반으로 취약점을 자동 스캔하고, 심각도 높은 취약점 발견 시 CI 빌드를 실패로 처리하여 팀에 알릴 수 있습니다. +:::warning grype 가 `sbom format not recognized` 를 낼 때 +syft 1.51 이상은 CycloneDX 1.7 을 냅니다. grype 는 0.118 이상에서만 이 버전을 읽습니다. +두 도구를 함께 올리거나, 올릴 수 없으면 `syft ... -o cyclonedx-json@1.6` 으로 낮춰 생성하세요. +::: + --- ## 3. 셀프 스터디 diff --git a/docs/05-tools/vulnerability/tools-setup.md b/docs/05-tools/vulnerability/tools-setup.md index a256ba00..9b6bb0f3 100644 --- a/docs/05-tools/vulnerability/tools-setup.md +++ b/docs/05-tools/vulnerability/tools-setup.md @@ -98,10 +98,10 @@ curl -X POST https://api.osv.dev/v1/querybatch \ ## 트러블슈팅 -| 증상 | 원인 | 해결 방법 | -| --------------------------- | ------------------ | --------------------------------------------------------- | -| Dependency-Track 접속 안 됨 | 초기화 중 | 3~5분 대기 후 재시도 | -| 취약점 0개 | NVD 데이터 로딩 중 | 10~30분 대기 (최초 실행 시) | -| OSV API 응답 없음 | 네트워크 문제 | `curl -I https://api.osv.dev` 로 연결 확인 | -| SBOM 업로드 오류 | 파일 형식 문제 | CycloneDX JSON 형식 확인, `bomFormat` 필드 존재 여부 확인 | -| agent 실행 오류 | SBOM 파일 없음 | `output/sbom/` 에 `.cdx.json` 파일 존재 여부 확인 | +| 증상 | 원인 | 해결 방법 | +| --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dependency-Track 접속 안 됨 | 초기화 중 | 3~5분 대기 후 재시도 | +| 취약점 0개 | NVD 데이터 로딩 중 | 10~30분 대기 (최초 실행 시) | +| OSV API 응답 없음 | 네트워크 문제 | `curl -sS -X POST https://api.osv.dev/v1/query -H 'Content-Type: application/json' -d '{"package":{"name":"requests","ecosystem":"PyPI"},"version":"2.25.0"}'` 실행. `vulns` 배열이 있는 JSON 이 오면 정상 | +| SBOM 업로드 오류 | 파일 형식 문제 | CycloneDX JSON 형식 확인, `bomFormat` 필드 존재 여부 확인 | +| agent 실행 오류 | SBOM 파일 없음 | `output/sbom/` 에 `.cdx.json` 파일 존재 여부 확인 | diff --git a/output-sample/sbom/sbom-management-plan.md b/output-sample/sbom/sbom-management-plan.md index 3e6173cf..dd67ed47 100644 --- a/output-sample/sbom/sbom-management-plan.md +++ b/output-sample/sbom/sbom-management-plan.md @@ -41,7 +41,7 @@ cyclonedx convert --input-file sbom.cdx.json \ ``` cdxgen은 SBOM 생성 도구로 포맷 간 변환 기능이 없으므로, 변환에는 cyclonedx-cli -(`docker run --rm -v $(pwd):/data cyclonedx/cyclonedx-cli convert ...`) 또는 납품처 지정 도구를 사용한다. +(`docker run --rm -v "$(pwd)":/data cyclonedx/cyclonedx-cli convert ...`) 또는 납품처 지정 도구를 사용한다. --- diff --git a/samples/java-vulnerable/README.md b/samples/java-vulnerable/README.md index a00c79c3..e4c2c508 100644 --- a/samples/java-vulnerable/README.md +++ b/samples/java-vulnerable/README.md @@ -64,12 +64,16 @@ Apache Log4j 2의 JNDI 조회 기능을 악용하여 원격 코드 실행(RCE) # 출력 디렉토리 생성 (fresh clone 직후에는 없음) mkdir -p ../../output/sbom -docker run --rm -v $(pwd):/project \ +docker run --rm -v "$(pwd)":/project \ anchore/syft:latest \ /project --output cyclonedx-json \ > ../../output/sbom/java-vulnerable.cdx.json ``` +스캔 단계에서 `sbom format not recognized` 가 나오면 grype 가 낡은 것입니다. +`anchore/syft:latest` 는 CycloneDX 1.7 을 내는데 grype 는 0.118 이상에서만 읽습니다. +`grype version` 으로 확인하고 올리세요. + ## 프로젝트 구조 ``` @@ -149,12 +153,16 @@ Change the log4j-core version in `pom.xml` to **2.17.1 or later**: # Create the output directory (it does not exist in a fresh clone) mkdir -p ../../output/sbom -docker run --rm -v $(pwd):/project \ +docker run --rm -v "$(pwd)":/project \ anchore/syft:latest \ /project --output cyclonedx-json \ > ../../output/sbom/java-vulnerable.cdx.json ``` +If the scan step reports `sbom format not recognized`, grype is too old. +`anchore/syft:latest` emits CycloneDX 1.7, which grype only reads from 0.118 onwards. +Check with `grype version` and upgrade. + ## Project layout ``` diff --git a/samples/nodejs-unlicensed/README.md b/samples/nodejs-unlicensed/README.md index 3ed788d4..39a3255b 100644 --- a/samples/nodejs-unlicensed/README.md +++ b/samples/nodejs-unlicensed/README.md @@ -86,12 +86,16 @@ npm install # 출력 디렉토리 생성 (fresh clone 직후에는 없음) mkdir -p ../../output/sbom -docker run --rm -v $(pwd):/project \ +docker run --rm -v "$(pwd)":/project \ anchore/syft:latest \ /project --output cyclonedx-json \ > ../../output/sbom/nodejs-unlicensed.cdx.json ``` +스캔 단계에서 `sbom format not recognized` 가 나오면 grype 가 낡은 것입니다. +`anchore/syft:latest` 는 CycloneDX 1.7 을 내는데 grype 는 0.118 이상에서만 읽습니다. +`grype version` 으로 확인하고 올리세요. + ## 프로젝트 구조 ``` @@ -187,12 +191,16 @@ npm ci # Create the output directory (it does not exist in a fresh clone) mkdir -p ../../output/sbom -docker run --rm -v $(pwd):/project \ +docker run --rm -v "$(pwd)":/project \ anchore/syft:latest \ /project --output cyclonedx-json \ > ../../output/sbom/nodejs-unlicensed.cdx.json ``` +If the scan step reports `sbom format not recognized`, grype is too old. +`anchore/syft:latest` emits CycloneDX 1.7, which grype only reads from 0.118 onwards. +Check with `grype version` and upgrade. + ## Project layout ``` diff --git a/samples/python-mixed-license/README.md b/samples/python-mixed-license/README.md index 2cb85b4f..7d2c0b3f 100644 --- a/samples/python-mixed-license/README.md +++ b/samples/python-mixed-license/README.md @@ -70,12 +70,16 @@ GPL 컴포넌트를 동등한 기능의 Permissive 라이선스 패키지로 교 # 출력 디렉토리 생성 (fresh clone 직후에는 없음) mkdir -p ../../output/sbom -docker run --rm -v $(pwd):/project \ +docker run --rm -v "$(pwd)":/project \ anchore/syft:latest \ /project --output cyclonedx-json \ > ../../output/sbom/python-mixed.cdx.json ``` +스캔 단계에서 `sbom format not recognized` 가 나오면 grype 가 낡은 것입니다. +`anchore/syft:latest` 는 CycloneDX 1.7 을 내는데 grype 는 0.118 이상에서만 읽습니다. +`grype version` 으로 확인하고 올리세요. + ## 프로젝트 구조 ``` @@ -155,12 +159,16 @@ Or get legal review and prepare to publish the source. # Create the output directory (it does not exist in a fresh clone) mkdir -p ../../output/sbom -docker run --rm -v $(pwd):/project \ +docker run --rm -v "$(pwd)":/project \ anchore/syft:latest \ /project --output cyclonedx-json \ > ../../output/sbom/python-mixed.cdx.json ``` +If the scan step reports `sbom format not recognized`, grype is too old. +`anchore/syft:latest` emits CycloneDX 1.7, which grype only reads from 0.118 onwards. +Check with `grype version` and upgrade. + ## Project layout ``` diff --git a/website/devsecops/pipeline-design.md b/website/devsecops/pipeline-design.md index cb13e116..69b6a397 100644 --- a/website/devsecops/pipeline-design.md +++ b/website/devsecops/pipeline-design.md @@ -47,7 +47,7 @@ SAST, SCA, 시크릿 탐지, 컨테이너 보안, IaC 보안, DAST 6개 영역 ### 실제 분할 사례 — TRUSCA -오픈소스 프로젝트 TRUSCA 는 워크플로우를 스물세 개로 나눠 운영합니다. 트리거를 기준으로 보면 +오픈소스 프로젝트 TRUSCA 는 워크플로우를 스물다섯 개로 나눠 운영합니다. 트리거를 기준으로 보면 설계 의도가 드러납니다. | 실행 시점 | 워크플로우 | 성격 | diff --git a/website/devsecops/pipeline-security.md b/website/devsecops/pipeline-security.md index aa51715c..e5c630b1 100644 --- a/website/devsecops/pipeline-security.md +++ b/website/devsecops/pipeline-security.md @@ -168,7 +168,7 @@ jobs: `pull_request_target` 은 반대로 대상 저장소의 시크릿과 쓰기 권한을 가진 채로 실행됩니다. 여기서 PR 브랜치의 코드를 체크아웃해 실행하면, 외부인이 보낸 코드가 저장소 시크릿을 쥐고 돌아갑니다. -```yaml +```yaml validate=skip # 위험한 조합. 이렇게 쓰지 마세요 on: pull_request_target @@ -282,7 +282,7 @@ Fulcio가 OIDC 신원을 확인해 단기 인증서를 발급하고, 서명 기 서명자가 개인 키를 장기 보관할 필요가 없고, 누가 언제 무엇에 서명했는지 공개 로그로 확인할 수 있습니다. 컨테이너 이미지에는 cosign으로 서명과 검증을 붙입니다. -```bash +```bash validate=skip # 키리스 서명. 실행 환경의 OIDC 신원으로 서명합니다 cosign sign ghcr.io/myorg/myapp@sha256:<다이제스트> diff --git a/website/devsecops/sca.mdx b/website/devsecops/sca.mdx index 2d1f3cbc..1378023d 100644 --- a/website/devsecops/sca.mdx +++ b/website/devsecops/sca.mdx @@ -124,6 +124,11 @@ sca: - if: $CI_PIPELINE_SOURCE == "merge_request_event" ``` +:::warning grype 가 `sbom format not recognized` 를 낼 때 +syft 1.51 이상은 CycloneDX 1.7 을 냅니다. grype 는 0.118 이상에서만 이 버전을 읽습니다. +두 도구를 함께 올리거나, 올릴 수 없으면 `syft ... -o cyclonedx-json@1.6` 으로 낮춰 생성하세요. +::: + ### cicd-quick 워크플로와의 차이 [AI 코딩 — 30분 완성 Quick CI/CD](/ai-coding/cicd-quick)에도 같은 구조의 워크플로가 있습니다. 그쪽을 이미 적용했다면 아래 세 가지만 더하면 이 페이지의 구성이 됩니다. @@ -367,5 +372,5 @@ agent가 아래를 자동으로 수행합니다. - 직접 띄워 보기: [TRUSCA 저장소](https://github.com/trustedoss/trusca) (Docker Compose 또는 Helm 배포) - 예산·인력·폐쇄망 제약이 있는 환경에서 상시 운영 SCA를 갖추는 경로입니다. -호스팅된 공개 데모 인스턴스는 제공하지 않습니다. 위 가이드로 본인 환경에 직접 띄워 확인하세요. +TRUSCA 포털에서 공개 데모 인스턴스를 열어 볼 수 있습니다. 다만 데모는 화면을 둘러보는 용도이므로, 실제 운영은 위 가이드로 본인 환경에 띄워야 합니다. ::: diff --git a/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-design.md b/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-design.md index 4a36e8af..b1103a11 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-design.md +++ b/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-design.md @@ -47,7 +47,7 @@ The stage-2 scans can run in parallel to keep the overall time within 5 minutes. ### How one project splits it — TRUSCA -The open source project TRUSCA runs twenty-three workflows. Grouping them by trigger shows the intent. +The open source project TRUSCA runs twenty-five workflows. Grouping them by trigger shows the intent. | When | Workflow | Purpose | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | diff --git a/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-security.md b/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-security.md index 856b90a7..18d51076 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-security.md +++ b/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/pipeline-security.md @@ -172,7 +172,7 @@ The `pull_request` event runs fork pull requests in a read-only environment with `pull_request_target` does the opposite: it runs with the target repository's secrets and write permissions. If you check out and execute the pull request branch's code there, an outsider's code runs while holding your secrets. -```yaml +```yaml validate=skip # A dangerous combination. Do not write it this way on: pull_request_target @@ -288,7 +288,7 @@ to the Rekor transparency log. Signers need no long-lived private key, and anyone can check who signed what and when in a public log. For container images, cosign handles signing and verification. -```bash +```bash validate=skip # Keyless signing, using the execution environment's OIDC identity cosign sign ghcr.io/myorg/myapp@sha256: diff --git a/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/sca.mdx b/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/sca.mdx index 63496673..8695d0dd 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/sca.mdx +++ b/website/i18n/en/docusaurus-plugin-content-docs-devsecops/current/sca.mdx @@ -124,6 +124,11 @@ sca: - if: $CI_PIPELINE_SOURCE == "merge_request_event" ``` +:::warning When grype reports `sbom format not recognized` +syft 1.51 and newer emit CycloneDX 1.7, which only grype 0.118 and newer can read. +Upgrade both tools together, or generate at the older version with `syft ... -o cyclonedx-json@1.6`. +::: + ### How this differs from the cicd-quick workflow [AI Coding — Quick CI/CD in 30 minutes](/en/ai-coding/cicd-quick) carries a workflow of the same shape. If you already applied that one, adding the three items below turns it into what this page describes. @@ -380,5 +385,5 @@ The analyzer above is for one-off checks. If your whole team needs a continuousl - Try it yourself: [TRUSCA repository](https://github.com/trustedoss/trusca) (Docker Compose or Helm deployment) - It is a path to a continuously operated SCA where budget, staffing, or an air-gapped network constrain the options. -No hosted public demo instance is provided. Use the guide above to run it in your own environment. +The TRUSCA portal links a hosted demo instance you can open. The demo is for looking around, so run it in your own environment with the guide above for real use. ::: diff --git a/website/i18n/en/docusaurus-plugin-content-docs-reference/current/samples/sbom.md b/website/i18n/en/docusaurus-plugin-content-docs-reference/current/samples/sbom.md index 34c50241..62e91f87 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs-reference/current/samples/sbom.md +++ b/website/i18n/en/docusaurus-plugin-content-docs-reference/current/samples/sbom.md @@ -249,7 +249,7 @@ cyclonedx convert --input-file sbom.cdx.json \ ``` cdxgen is an SBOM generation tool and has no format conversion capability, so use cyclonedx-cli -(`docker run --rm -v $(pwd):/data cyclonedx/cyclonedx-cli convert ...`) or the tool designated by the recipient for conversion. +(`docker run --rm -v "$(pwd)":/data cyclonedx/cyclonedx-cli convert ...`) or the tool designated by the recipient for conversion. --- diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/04-process/index.md b/website/i18n/en/docusaurus-plugin-content-docs/current/04-process/index.md index 7a037be6..8520527a 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/04-process/index.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/04-process/index.md @@ -260,7 +260,7 @@ jobs: - uses: actions/checkout@v7 - name: Generate SBOM run: | - docker run --rm -v $(pwd):/project \ + docker run --rm -v "$(pwd)":/project \ anchore/syft:latest /project \ --output cyclonedx-json > sbom.cdx.json - name: License check diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/docker-cicd.md b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/docker-cicd.md index cd69d9c6..57e96d5d 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/docker-cicd.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/docker-cicd.md @@ -17,12 +17,12 @@ This page contains the actual Docker commands for syft and cdxgen, the GitHub Ac ## Running syft with Docker — commands per language/package manager -| Language | Package manager | Command | -| -------- | --------------- | ---------------------------------------------------------------------------------------------------- | -| Java | Maven/Gradle | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | -| Python | pip | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | -| Node.js | npm | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | -| Go | go mod | `docker run --rm -v $(pwd):/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Language | Package manager | Command | +| -------- | --------------- | ------------------------------------------------------------------------------------------------------ | +| Java | Maven/Gradle | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Python | pip | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Node.js | npm | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | +| Go | go mod | `docker run --rm -v "$(pwd)":/src anchore/syft dir:/src -o cyclonedx-json > output/sbom/sbom.cdx.json` | Full command (identical for every language; only the directory changes): @@ -32,7 +32,7 @@ mkdir -p output/sbom # Generate the SBOM with syft docker run --rm \ - -v $(pwd):/src \ + -v "$(pwd)":/src \ anchore/syft \ dir:/src \ -o cyclonedx-json \ @@ -45,7 +45,7 @@ docker run --rm \ ```bash docker run --rm \ - -v $(pwd):/app \ + -v "$(pwd)":/app \ -w /app \ ghcr.io/cyclonedx/cdxgen:latest \ -o /app/output/sbom/sbom-cdxgen.cdx.json \ @@ -102,7 +102,7 @@ Three sample projects are provided for practice: ```bash # Practice with the java-vulnerable sample docker run --rm \ - -v $(pwd)/samples/java-vulnerable:/src \ + -v "$(pwd)"/samples/java-vulnerable:/src \ anchore/syft \ dir:/src -o cyclonedx-json \ > output/sbom/java-vulnerable.cdx.json diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/index.md b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/index.md index 22e9f578..e2b6b161 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/index.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-generation/index.md @@ -108,19 +108,19 @@ For the actual Docker commands, GitHub Actions CI/CD setup, and the sample proje Key field descriptions: -| Field | Description | -| ----------------------------- | --------------------------------------------------------------------------------------------------- | -| `bomFormat`, `specVersion` | CycloneDX format identifier and specification version. Both syft and cdxgen emit 1.7 by default | -| `metadata.timestamp` | When the SBOM was generated | -| `metadata.tools.components[]` | Name and version of the tool that built the SBOM, the "SBOM generation tool" CISA 2026 requires | -| `metadata.lifecycles[]` | Lifecycle phase the SBOM was captured in, the "generation context" | -| `metadata.component` | Information about the software being analyzed | -| `components[].supplier` | Supplier of the component | -| `components[].hashes[]` | Component file hash. The `alg` (SHA-256 and so on) and `content` (hex value) pair proves integrity | -| `components[].licenses[]` | License of the component | -| `components[].purl` | PURL (Package URL, a standard string that uniquely identifies a package) | -| `signature` | Top-level BOM signature in JSON Signature Format (JSF), which proves the SBOM was not tampered with | -| `vulnerabilities[]` | Vulnerability information (if present) | +| Field | Description | +| ----------------------------- | ----------------------------------------------------------------------------------------------------- | +| `bomFormat`, `specVersion` | CycloneDX format identifier and specification version. cdxgen 12.x and syft 1.51+ emit 1.7 by default | +| `metadata.timestamp` | When the SBOM was generated | +| `metadata.tools.components[]` | Name and version of the tool that built the SBOM, the "SBOM generation tool" CISA 2026 requires | +| `metadata.lifecycles[]` | Lifecycle phase the SBOM was captured in, the "generation context" | +| `metadata.component` | Information about the software being analyzed | +| `components[].supplier` | Supplier of the component | +| `components[].hashes[]` | Component file hash. The `alg` (SHA-256 and so on) and `content` (hex value) pair proves integrity | +| `components[].licenses[]` | License of the component | +| `components[].purl` | PURL (Package URL, a standard string that uniquely identifies a package) | +| `signature` | Top-level BOM signature in JSON Signature Format (JSF), which proves the SBOM was not tampered with | +| `vulnerabilities[]` | Vulnerability information (if present) | Hashes, the generation tool name, the generation context, and licenses are the fields that the CISA 2026 minimum elements described in [SBOM Basics: An Introduction to the Software Bill of Materials](../../00-overview/sbom-101.md) @@ -138,6 +138,13 @@ If you need the lifecycle phase or hashes and syft leaves them empty, generate t with cdxgen and compare. To pin the specification version, use `-o cyclonedx-json@1.7` with syft and `--spec-version 1.7` with cdxgen. +:::warning Check the minimum tool versions first +CycloneDX 1.7 requires syft 1.51 or newer. Older syft only goes up to 1.6 and fails with +`unsupported output format` when given `-o cyclonedx-json@1.7`. +grype needs 0.118 or newer to read 1.7; an older grype rejects a 1.7 SBOM with +`sbom format not recognized`. Check with `syft version` and `grype version` before you start. +::: + :::tip MCP servers belong in the SBOM too For how to list MCP (Model Context Protocol, the convention by which an agent calls external tools) servers that AI agents call, see [Agent and MCP Tool Governance](/en/ai-coding/agent-governance). @@ -293,7 +300,7 @@ Confirm all of the items below before moving on to the next step. - [ ] `output/sbom/[project].cdx.json` created - [ ] The `components` array in the SBOM file is not empty -- [ ] `specVersion` is `1.7` (if it is lower, upgrade the tool or pin the specification version and regenerate) +- [ ] `specVersion` is `1.7` (if it is lower, upgrade syft to 1.51 or newer and regenerate) - [ ] `metadata.timestamp` and `metadata.tools` record the generation time and the generating tool - [ ] Every entry in `components[]` has a `purl` - [ ] Component hashes (`hashes`) and licenses (`licenses`) have been checked (they can be empty depending on the tool; if so, compare against cdxgen output) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-management/index.md b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-management/index.md index acff63d2..e6c27171 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-management/index.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/sbom-management/index.md @@ -192,6 +192,11 @@ jobs: This workflow automatically scans for vulnerabilities against the latest SBOM every week. If a high-severity vulnerability is found, the CI build can be failed and the team notified. +:::warning When grype reports `sbom format not recognized` +syft 1.51 and newer emit CycloneDX 1.7, which only grype 0.118 and newer can read. +Upgrade both tools together, or generate at the older version with `syft ... -o cyclonedx-json@1.6`. +::: + --- ## 3. Self-study diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/vulnerability/tools-setup.md b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/vulnerability/tools-setup.md index c80d1b39..d183c1a4 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/vulnerability/tools-setup.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/05-tools/vulnerability/tools-setup.md @@ -98,10 +98,10 @@ The `vulnerability-analyst` agent automatically reads the CycloneDX SBOM files i ## Troubleshooting -| Symptom | Cause | Solution | -| ----------------------------- | ---------------------- | -------------------------------------------------------------------------- | -| Cannot reach Dependency-Track | Still initializing | Wait 3-5 minutes and retry | -| Zero vulnerabilities | NVD data still loading | Wait 10-30 minutes (on the first run) | -| No response from the OSV API | Network problem | Check connectivity with `curl -I https://api.osv.dev` | -| SBOM upload error | File format problem | Verify the CycloneDX JSON format and that the `bomFormat` field is present | -| Agent execution error | SBOM file missing | Check that a `.cdx.json` file exists in `output/sbom/` | +| Symptom | Cause | Solution | +| ----------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Cannot reach Dependency-Track | Still initializing | Wait 3-5 minutes and retry | +| Zero vulnerabilities | NVD data still loading | Wait 10-30 minutes (on the first run) | +| No response from the OSV API | Network problem | Run `curl -sS -X POST https://api.osv.dev/v1/query -H 'Content-Type: application/json' -d '{"package":{"name":"requests","ecosystem":"PyPI"},"version":"2.25.0"}'`. JSON containing a `vulns` array means it works | +| SBOM upload error | File format problem | Verify the CycloneDX JSON format and that the `bomFormat` field is present | +| Agent execution error | SBOM file missing | Check that a `.cdx.json` file exists in `output/sbom/` | diff --git a/website/reference/samples/sbom.md b/website/reference/samples/sbom.md index 42b8ae26..0ae037d3 100644 --- a/website/reference/samples/sbom.md +++ b/website/reference/samples/sbom.md @@ -249,7 +249,7 @@ cyclonedx convert --input-file sbom.cdx.json \ ``` cdxgen은 SBOM 생성 도구로 포맷 간 변환 기능이 없으므로, 변환에는 cyclonedx-cli -(`docker run --rm -v $(pwd):/data cyclonedx/cyclonedx-cli convert ...`) 또는 납품처 지정 도구를 사용한다. +(`docker run --rm -v "$(pwd)":/data cyclonedx/cyclonedx-cli convert ...`) 또는 납품처 지정 도구를 사용한다. --- diff --git a/website/src/css/_search.scss b/website/src/css/_search.scss index 26e8c259..f142b39e 100644 --- a/website/src/css/_search.scss +++ b/website/src/css/_search.scss @@ -106,10 +106,14 @@ color: var(--brand-text); } + /* 활성 행 배경은 밝은 주색이다. 전경을 흰색으로 고정하면 다크에서 흰 글자가 + #8ab4f8 위에 놓여 2.11:1 로 AA 에 미달한다. --brand-fill-fg 는 라이트에서 + #fff, 다크에서 #062330 이라 라이트는 그대로 두고 다크만 7.72:1 로 올린다. + 사이트의 다크 1차 버튼과 같은 처리다. */ &[aria-selected="true"] { span, mark { - color: white; + color: var(--brand-fill-fg); } } @@ -122,6 +126,41 @@ color: var(--brand-text); } +/* 검색 결과 페이지(/search) + 테마의 CSS 모듈 클래스에는 빌드마다 달라지는 해시가 붙으므로 부분 일치로 잡는다. */ + +/* 결과 제목이 h2 인데 테마가 font-weight 400 을 걸어 사이트 굵기 계층을 덮는다. */ +[class*="searchResultItemHeading"] { + font-weight: var(--ifm-heading-font-weight); + font-size: 1.125rem; + line-height: 1.4; +} + +/* 한 화면에 담기는 결과 수를 늘린다. 테마 기본값은 padding 1rem 0 이다. + 행 높이의 대부분은 본문용 제목 패딩(위아래 12px)과 breadcrumb 패딩·여백(합 40px)이 + 차지한다. 결과 목록에서는 둘 다 필요 없다. */ +[class*="searchResultItem"] { + padding: 0.75rem 0; + + [class*="searchResultItemHeading"] { + padding: 0; + } + + .breadcrumbs { + padding: 0; + margin: 0; + } +} + +[class*="searchResultItemSummary"] { + margin: 0.25rem 0 0; +} + +/* 강조 표시가 DocSearch 기본 크림색이라 사이트 토큰 밖이고 다크 대응이 없다. */ +.search-result-match { + background: color-mix(in srgb, var(--ifm-color-primary) 18%, transparent); +} + .DocSearch-Footer { font-size: 14px; border-radius: 0 0 var(--ifm-global-radius) var(--ifm-global-radius); @@ -169,4 +208,9 @@ html[data-theme="dark"] { .DocSearch-Footer { background: none; } + + /* 다크에서는 같은 비율이면 흐려서 조금 더 올린다. */ + .search-result-match { + background: color-mix(in srgb, var(--ifm-color-primary) 26%, transparent); + } }