diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 237e6bf29..2fe5fa832 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,6 +4,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
groups:
github-actions:
patterns:
@@ -13,6 +15,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
groups:
docker-base-images:
patterns:
@@ -22,6 +26,8 @@ updates:
directory: "/frontend"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
groups:
frontend-docker-base-images:
patterns:
@@ -31,6 +37,8 @@ updates:
directory: "/backend"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
groups:
backend-python:
patterns:
@@ -40,6 +48,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
groups:
ci-python:
patterns:
@@ -49,6 +59,8 @@ updates:
directory: "/frontend"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
groups:
frontend-npm:
patterns:
@@ -58,13 +70,19 @@ updates:
directory: /connector
schedule:
interval: daily
+ cooldown:
+ default-days: 7
- package-ecosystem: pip
directory: /connector
schedule:
interval: daily
+ cooldown:
+ default-days: 7
- package-ecosystem: npm
directory: /
schedule:
interval: daily
+ cooldown:
+ default-days: 7
diff --git a/.github/workflows/app-ci.yml b/.github/workflows/app-ci.yml
index d6a17f49a..e8f445748 100644
--- a/.github/workflows/app-ci.yml
+++ b/.github/workflows/app-ci.yml
@@ -35,10 +35,12 @@ jobs:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
@@ -84,14 +86,15 @@ jobs:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
-
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Install pnpm
run: corepack enable pnpm
- name: Set up Node.js
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: pnpm
diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml
index 58c486b5b..c5c613c08 100644
--- a/.github/workflows/bandit.yml
+++ b/.github/workflows/bandit.yml
@@ -22,10 +22,12 @@ jobs:
with:
egress-policy: audit
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
@@ -38,7 +40,7 @@ jobs:
- name: Upload SARIF file
if: ${{ always() }}
- uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
+ uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
sarif_file: bandit-results.sarif
category: bandit
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 27fde0dc0..c303d1e61 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -29,7 +29,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 7ea854dc1..8d0ca1ed8 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -20,7 +20,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 54652af73..879b906ec 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -54,7 +54,9 @@ jobs:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
@@ -178,7 +180,9 @@ jobs:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Read release version
id: version
@@ -248,7 +252,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
diff --git a/.github/workflows/mail-smoke.yml b/.github/workflows/mail-smoke.yml
index cfa94f5a0..6a3a3fdc9 100644
--- a/.github/workflows/mail-smoke.yml
+++ b/.github/workflows/mail-smoke.yml
@@ -30,10 +30,12 @@ jobs:
${{ vars.MAIL_SMOKE_ALLOWED_ENDPOINTS }}
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a06003d8f..3e217792b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2718,3 +2718,13 @@
- **Note:** CI opencode-review 잡 실행 중 타임아웃 오류(The action 'Run OpenCode PR Review model pool' has timed out after 350 minutes)가 발생했습니다. 이는 외부 AI 검토 모델 서버(github-models 등)의 응답 지연에 기인한 일시적 인프라 문제로 판단되며, 코드 변경 자체의 결함은 아니므로 그대로 재제출하여 파이프라인 재실행을 시도합니다.
- **Note:** CI opencode-review 잡 실행 중 타임아웃 오류(The action 'Run OpenCode PR Review model pool' has timed out after 350 minutes)가 발생했습니다. 반복되는 외부 인프라 타임아웃 문제를 해결하기 위해, 마지막으로 재제출을 시도합니다.
- **Note:** 추가적인 코드 변경은 없으며, PR 내 자동 분석 커멘트에 대한 답변(CI 실패가 본 PR이 아닌 develop의 기존 이슈임을 인지함)을 남기고 현재 워크플로우를 완료합니다.
+
+- [UX 개선] 캘린더 회의 조율 화면 제안 버튼의 접근성 향상 (스크린 리더 사용자를 위한 sr-only 텍스트 추가 및 불필요한 중복 텍스트 숨김)
+
+### 변경 사항 (Changes)
+
+- `backend/tests/test_release_governance.py` 파일의 394번째 줄에서 `yaml.load` 함수 사용 시 발생하는 Bandit B506 오탐지를 억제하기 위해 `# nosec B506` 주석을 추가했습니다. 해당 코드는 `yaml.SafeLoader`를 상속받은 `UniqueKeyLoader`를 사용하므로 실제로는 안전합니다. 이 변경은 보안 취약점 픽스가 아닌, 정적 분석 툴의 오탐지를 처리하기 위한 조치입니다.
+
+### 문서 (Documentation)
+
+- `yaml.load()`와 관련해 발생한 Bandit B506 항목에 대해 규칙 한정적 오탐지(false-positive) 판정 및 처분 근거(disposition)를 담은 `docs/doctoring/bandit-b506-false-positive-disposition.md` 문서를 추가했습니다. 이는 제품의 실제 취약점 패치가 아니며, PyYAML의 `SafeLoader`를 명시적으로 사용하는 사용자 정의 로더에 대해 오탐지를 억제하는 조건과 롤백 기준을 테스트 증거와 함께 기록한 문서입니다.
diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py
index 56ef2bff4..efe0acd0e 100644
--- a/backend/tests/test_release_governance.py
+++ b/backend/tests/test_release_governance.py
@@ -388,10 +388,28 @@ def construct_mapping(
construct_mapping,
)
+ # Verify that UniqueKeyLoader is strictly a subclass of SafeLoader so that `# nosec B506`
+ # suppression is genuinely justified according to PyYAML safety contracts.
+ assert issubclass(UniqueKeyLoader, yaml.SafeLoader), (
+ "UniqueKeyLoader must inherit from SafeLoader to suppress B506"
+ )
+ # Ensure that Python object instantiation tags (like !!python/object) are safely
+ # rejected rather than executed.
+ with pytest.raises(yaml.constructor.ConstructorError):
+ yaml.load("!!python/object/apply:os.system ['echo pwned']", Loader=UniqueKeyLoader) # nosec B506
+ # Ensure normal valid YAML loading still works
+ assert yaml.load("a: 1\nb: 2", Loader=UniqueKeyLoader) == {"a": 1, "b": 2} # nosec B506
+ # Ensure the duplicate key prevention still works
+ with pytest.raises(AssertionError, match="duplicate mapping key 'a'"):
+ yaml.load("a: 1\na: 2", Loader=UniqueKeyLoader) # nosec B506
+
duplicates: list[str] = []
for workflow_path in governed_workflows:
try:
- yaml.load(workflow_path.read_text(encoding="utf-8"), Loader=UniqueKeyLoader)
+ # We explicitly pass UniqueKeyLoader (which inherits from SafeLoader).
+ # Bandit B506 blindly flags yaml.load() regardless of the Loader argument.
+ # This is a verified false positive.
+ yaml.load(workflow_path.read_text(encoding="utf-8"), Loader=UniqueKeyLoader) # nosec B506
except AssertionError as exc:
duplicates.append(f"{workflow_path.relative_to(REPO_ROOT)}: {exc}")
@@ -716,7 +734,7 @@ def test_docker_publish_validates_pr_images_and_publishes_semver_images_only_on_
== 2
)
assert (
- "docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0"
+ "docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0"
in workflow
)
assert (
diff --git a/docs/doctoring/bandit-b506-false-positive-disposition.md b/docs/doctoring/bandit-b506-false-positive-disposition.md
new file mode 100644
index 000000000..28d0e9099
--- /dev/null
+++ b/docs/doctoring/bandit-b506-false-positive-disposition.md
@@ -0,0 +1,24 @@
+# False Positive Disposition: Bandit B506 (`yaml.load`)
+
+## Context and Evidence
+Bandit reports a Medium severity B506 issue on `yaml.load()` calls because using the default loader can permit the instantiation of arbitrary Python objects, posing a security risk (PyCQA, 2024). However, in `backend/tests/test_release_governance.py`, `yaml.load` is explicitly invoked with `Loader=UniqueKeyLoader`.
+
+The local implementation explicitly defines `UniqueKeyLoader` as a subclass of `yaml.SafeLoader`:
+```python
+class UniqueKeyLoader(yaml.SafeLoader):
+ pass
+```
+
+Because `UniqueKeyLoader` inherits from `yaml.SafeLoader`, it automatically inherits all safety constraints, explicitly rejecting unsafe tags (e.g., `!!python/object/apply`). Tests in `test_release_governance.py` verify that `issubclass(UniqueKeyLoader, yaml.SafeLoader)` is true and that malicious YAML payloads are correctly rejected via `yaml.constructor.ConstructorError` rather than being executed (PyYAML, 2024).
+
+Therefore, this finding is a verified false positive caused by a limitation in Bandit's static analysis, which triggers on the `yaml.load` function name without evaluating the inheritance chain of the provided `Loader` argument.
+
+## Resolution
+The `yaml.load` call has been annotated with `# nosec B506` to suppress the false positive locally. We retain this suppression strictly under the condition that `UniqueKeyLoader` remains a subclass of `yaml.SafeLoader` and is explicitly provided to `yaml.load`.
+
+## Rollback Criteria
+If the YAML loader implementation is modified to inherit from an unsafe loader, or if `yaml.load` is used without explicitly providing the safe custom loader, this disposition must be revoked and the `# nosec B506` annotation removed.
+
+## References
+PyCQA. (2024). *B506: Test for use of yaml load*. Bandit Documentation. https://bandit.readthedocs.io/en/latest/plugins/b506_yaml_load.html
+PyYAML. (2024). *PyYAML Documentation: Loading YAML safely*. https://pyyaml.org/wiki/PyYAMLDocumentation#loading-yaml-safely
diff --git a/docs/doctoring/calendar-a11y.md b/docs/doctoring/calendar-a11y.md
new file mode 100644
index 000000000..a78d17cb2
--- /dev/null
+++ b/docs/doctoring/calendar-a11y.md
@@ -0,0 +1,25 @@
+# Calendar Coordination View Accessibility
+
+`CalendarCoordinationView.tsx` presents numbered meeting proposals with date, time, attendance status, and a visible `제안하기` action label.
+
+## Accessibility problem
+
+A short `aria-label` on the button would replace the descendant-derived accessible name and could omit the date, time, or attendance information needed to distinguish the proposals. Purely visual repetition can also make screen-reader output unnecessarily noisy.
+
+## Implemented pattern
+
+Each button keeps its essential visible text in the accessibility tree and adds a visually hidden contextual prefix such as `1안 제안하기: `. The duplicated visual option badge and trailing action label use `aria-hidden="true"`. The native button role and existing `focus-visible` ring remain intact.
+
+This component pattern aligns with WCAG 2.2 Success Criterion 4.1.2, **Name, Role, Value**, and Success Criterion 2.4.7, **Focus Visible**. This scoped implementation statement does **not** establish conformance of the whole Naruon product.
+
+## Research note
+
+Lazar et al. (2007) studied 100 blind web users and identified confusing screen-reader feedback and poorly designed or unlabeled controls among the leading sources of frustration. The proposal-button pattern therefore preserves task-specific context in the computed accessible name instead of relying on visual grouping alone.
+
+## References
+
+Lazar, J., Allen, A., Kleinman, J., & Malarkey, C. (2007). What frustrates screen reader users on the web: A study of 100 blind users. *International Journal of Human–Computer Interaction, 22*(3), 247–269. https://doi.org/10.1080/10447310709336964
+
+World Wide Web Consortium. (2023a). *Understanding Success Criterion 2.4.7: Focus visible*. https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html
+
+World Wide Web Consortium. (2023b). *Understanding Success Criterion 4.1.2: Name, role, value*. https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html
diff --git a/frontend/src/components/calendar/CalendarCoordinationView.test.tsx b/frontend/src/components/calendar/CalendarCoordinationView.test.tsx
new file mode 100644
index 000000000..97825f4c8
--- /dev/null
+++ b/frontend/src/components/calendar/CalendarCoordinationView.test.tsx
@@ -0,0 +1,64 @@
+/* @vitest-environment jsdom */
+import React, { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { afterEach, describe, expect, it } from "vitest";
+import { CalendarCoordinationView } from "./CalendarCoordinationView";
+
+describe("CalendarCoordinationView", () => {
+ let container: HTMLDivElement | null = null;
+ let root: Root | null = null;
+
+ afterEach(() => {
+ if (root && container) {
+ act(() => {
+ root!.unmount();
+ });
+ container.remove();
+ }
+ container = null;
+ root = null;
+ });
+
+ it("renders buttons with distinct accessible names including date and attendance", () => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+
+ act(() => {
+ root!.render(