diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f561f69..bd38a7a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,10 +17,12 @@ jobs: echo "This marker exposes github/codeql-action usage to Scorecard without uploading SARIF." - name: Checkout repository for manual diagnostics - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Initialize CodeQL for manual diagnostics - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: javascript-typescript @@ -30,6 +32,6 @@ jobs: - name: Document advanced CodeQL analyze action without running it if: ${{ false }} - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: "/language:javascript-typescript" diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1847a..f17bef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ## [Unreleased] +- **CI 공급망 보안**: 수동 CodeQL 진단 워크플로의 `actions/checkout`을 불변 v7.0.1 커밋으로 갱신하고 인증 정보 지속을 비활성화했으며, CodeQL `init`·`analyze` 참조를 하나의 검증된 v4.37.6 커밋으로 통합했습니다. - **UX/접근성 개선**: 프로젝트 카드의 클릭 영역을 카드 전체로 확장하여 사용자 편의성을 높였습니다. 태그를 확장하는 대신 가상 요소(pseudo-element) 겹침 방식을 사용하여 스크린 리더 접근성을 유지했습니다. - **보안 개선**: 컴포넌트 갤러리의 인라인 스크립트와 스타일을 외부 파일로 분리하고, 엄격한 Content-Security-Policy를 적용해 XSS 방어를 강화했습니다. - **성능 회귀 복원**: 오프스크린 `.section` 렌더링을 `content-visibility: auto`로 지연하고, 일반 섹션은 600px·콘텐츠가 큰 DIKW/projects 섹션은 1000px의 `contain-intrinsic-size` placeholder를 유지해 초기 렌더링 비용과 스크롤바 이동을 함께 줄였습니다. diff --git a/docs/doctoring/codeql-action-pin-consolidation.md b/docs/doctoring/codeql-action-pin-consolidation.md new file mode 100644 index 0000000..aaa128e --- /dev/null +++ b/docs/doctoring/codeql-action-pin-consolidation.md @@ -0,0 +1,56 @@ +# CodeQL action pin consolidation + +## Decision + +The repository's manual CodeQL marker workflow uses one reviewed immutable release identity for both `github/codeql-action/init` and the disabled documentation-only `github/codeql-action/analyze` step: + +- `github/codeql-action` v4.37.6: `5595ccaf912efad79be6eef63a5619ff05969be3`; +- `actions/checkout` v7.0.1: `3d3c42e5aac5ba805825da76410c181273ba90b1`. + +The checkout step explicitly sets `persist-credentials: false`. The workflow remains manually dispatched, read-only, and does not upload SARIF. GitHub CodeQL default setup or the organization-owned required checks continue to own analysis publication. + +## Why this consolidation is necessary + +Three independent dependency pull requests proposed overlapping updates to checkout, CodeQL initialization, and the disabled analyze marker. Keeping those changes separate would leave temporary version skew and multiple competing merge paths. The consolidated change applies the newest reviewed CodeQL release to both CodeQL action references and includes the checkout credential hardening already validated in the predecessor branch. + +The CodeQL v4.37.6 tag resolves exactly to commit `5595ccaf912efad79be6eef63a5619ff05969be3`. The checkout v7.0.1 release commit is `3d3c42e5aac5ba805825da76410c181273ba90b1`. Full-length commit pins prevent a mutable tag or branch from changing the code executed by the workflow without a reviewed repository change. + +## Trust and behavior boundaries + +This change does not: + +- enable the disabled `analyze` step; +- transfer SARIF ownership from default or central setup; +- add write permissions, tokens, model credentials, caches, or artifacts; +- execute pull-request-controlled code with retained Git credentials; or +- alter branch protection, review identities, or required status contexts. + +The manual workflow still receives the ordinary runner environment. Disabling persisted checkout credentials removes automatic authenticated Git access from later steps, but it does not remove every GitHub-provided environment variable. Repository permissions therefore remain explicitly read-only. + +## Test-first evidence + +Commit `496474275cb3fdb8f8a54dee02a4767acdb0b8f8` introduced the permanent regression contract before the production workflow changed. Against the protected-base workflow, that contract fails because checkout is still v7.0.0, credential persistence is not disabled, and CodeQL is still v4.37.0. The implementation commit then updates only the reviewed action references and credential setting required to satisfy the contract. + +## Verification + +For each candidate exact head: + +1. Run the complete repository test suite, including `tests/test_codeql_workflow_security.py`. +2. Confirm the workflow contains each required action SHA exactly where expected. +3. Confirm `persist-credentials: false` remains attached to the checkout step. +4. Confirm repository security, static-analysis, and policy checks complete successfully on that exact head. +5. Confirm all actionable review threads are resolved and an independent non-last-pusher approval applies to that exact head before merge. + +Queued, pending, skipped-required, cancelled, absent, failed, or predecessor-head evidence is not accepted. + +## Rollback + +Rollback requires a reviewed commit that replaces both CodeQL action references together and updates the regression contract. Do not move one CodeQL component independently or replace a full commit SHA with a tag. If v4.37.6 causes a verified regression, pin both CodeQL references to the last known-good immutable commit, retain `persist-credentials: false`, rerun the exact-head checks, and document the incident in this record and the changelog. + +## References + +Actions. (2026, July 17). *Checkout v7.0.1* [Software release]. GitHub. https://github.com/actions/checkout/releases/tag/v7.0.1 + +GitHub. (2026, August 4). *CodeQL Action v4.37.6* [Software release]. GitHub. https://github.com/github/codeql-action/releases/tag/v4.37.6 + +GitHub. (n.d.). *Secure use reference*. GitHub Docs. Retrieved August 7, 2026, from https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions diff --git a/tests/test_codeql_workflow_security.py b/tests/test_codeql_workflow_security.py new file mode 100644 index 0000000..048ad6d --- /dev/null +++ b/tests/test_codeql_workflow_security.py @@ -0,0 +1,128 @@ +"""Security contracts for the repository's manual CodeQL marker workflow.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +CODEQL_WORKFLOW = ROOT / ".github" / "workflows" / "codeql.yml" +CHECKOUT_V7_0_1_SHA = "3d3c42e5aac5ba805825da76410c181273ba90b1" +CODEQL_V4_37_6_SHA = "5595ccaf912efad79be6eef63a5619ff05969be3" + + +def _parse_scalar(raw_value: str) -> str | bool: + """Parse the small set of YAML scalars used by the workflow contract.""" + value = raw_value.split(" #", 1)[0].strip() + if value == "false": + return False + if value == "true": + return True + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + return value[1:-1] + return value + + +def _workflow_structure() -> dict[str, Any]: + """Parse the workflow subset needed to bind permissions and action steps.""" + permissions: dict[str, str | bool] = {} + jobs: dict[str, dict[str, Any]] = {} + current_job: dict[str, Any] | None = None + current_step: dict[str, Any] | None = None + current_section: str | None = None + in_step_with = False + + for raw_line in CODEQL_WORKFLOW.read_text(encoding="utf-8").splitlines(): + if not raw_line.strip() or raw_line.lstrip().startswith("#"): + continue + indent = len(raw_line) - len(raw_line.lstrip(" ")) + line = raw_line.strip() + + if indent == 0: + current_job = None + current_step = None + in_step_with = False + current_section = line[:-1] if line.endswith(":") else None + continue + + if current_section == "permissions" and indent == 2: + key, separator, value = line.partition(":") + assert separator, f"malformed permissions entry: {line!r}" + permissions[key] = _parse_scalar(value) + continue + + if current_section != "jobs": + continue + + if indent == 2 and line.endswith(":"): + job_name = line[:-1] + current_job = {"steps": []} + jobs[job_name] = current_job + current_step = None + in_step_with = False + continue + + if current_job is None: + continue + + if indent == 6 and line.startswith("- "): + current_step = {} + current_job["steps"].append(current_step) + in_step_with = False + first_key, separator, value = line[2:].partition(":") + if separator: + current_step[first_key] = _parse_scalar(value) + continue + + if current_step is None: + continue + + if indent == 8: + key, separator, value = line.partition(":") + if not separator: + continue + if key == "with" and not value.strip(): + current_step["with"] = {} + in_step_with = True + else: + current_step[key] = _parse_scalar(value) + in_step_with = False + continue + + if indent == 10 and in_step_with: + key, separator, value = line.partition(":") + assert separator, f"malformed step input: {line!r}" + current_step["with"][key] = _parse_scalar(value) + + return {"permissions": permissions, "jobs": jobs} + + +def _single_action_step(workflow: dict[str, Any], action: str) -> dict[str, Any]: + """Return the sole workflow step whose action belongs to ``action``.""" + steps = [ + step + for job in workflow["jobs"].values() + for step in job["steps"] + if str(step.get("uses", "")).startswith(f"{action}@") + ] + assert len(steps) == 1, f"expected exactly one {action} step, found {len(steps)}" + return steps[0] + + +def test_codeql_workflow_uses_current_immutable_action_pins() -> None: + """Bind each reviewed action and security control to its exact YAML step.""" + workflow = _workflow_structure() + + assert workflow["permissions"] == {"contents": "read"} + + checkout = _single_action_step(workflow, "actions/checkout") + assert checkout["uses"] == f"actions/checkout@{CHECKOUT_V7_0_1_SHA}" + assert checkout.get("with", {}).get("persist-credentials") is False + + init = _single_action_step(workflow, "github/codeql-action/init") + assert init["uses"] == f"github/codeql-action/init@{CODEQL_V4_37_6_SHA}" + + analyze = _single_action_step(workflow, "github/codeql-action/analyze") + assert analyze["uses"] == f"github/codeql-action/analyze@{CODEQL_V4_37_6_SHA}" + assert analyze.get("if") == "${{ false }}"