diff --git a/.jules/palette.md b/.jules/palette.md index bdb0a4bbd..0c0fa9b82 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -80,3 +80,6 @@ ## 2025-05-19 - Dynamic ARIA labels and robust disabled states for sidebar actions **Learning:** Hardcoded ARIA labels in mockups (like "출시 회의 일정 삭제") are often left intact during implementation, leading to incorrect screen reader announcements when different items are selected. In addition, action buttons that depend on selection state often lack correct visual and functional disabled states. **Action:** When implementing detail views or sidebars, always replace hardcoded mockup ARIA labels with dynamic data (e.g. `${event.title} 삭제`), and ensure action buttons are explicitly disabled (both functionally via `disabled` and visually via `opacity-50 cursor-not-allowed`) when their prerequisites (like a selected item or specific properties like location) are unmet. +## 2024-05-18 - 버튼의 aria-disabled 안티패턴 수정 +**Learning:** 네이티브 `disabled` 속성과 `aria-disabled` 속성을 함께 사용하는 것은 스크린 리더 등에서 혼란을 줄 수 있는 접근성 안티패턴입니다. 네이티브 `disabled` 속성만으로 상태를 전달하고, 비동기 작업 시에는 `aria-busy`를 사용해야 합니다. +**Action:** 다음 작업부터는 비동기 작업이 있는 버튼에 `disabled`와 `aria-disabled`를 중복해서 사용하지 않도록 유의하여 검토합니다. diff --git a/AGENTS.md b/AGENTS.md index 0d0fd6abc..9104dd1f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -678,10 +678,6 @@ in this repo. backend/tests/test_release_governance.py backend/tests/test_runtime_config_api.py -q`, `corepack pnpm@11.5.3 --dir frontend test --runInBand` when frontend behavior changes, and a Docker build of the affected image. -- A pnpm importer entry is only valid when both records it names exist: the - base-version key in `packages` and the complete peer-qualified key in - `snapshots`. Security-floor tests must reject a lock that retains another - compliant version while dropping the importer's own base package record. - GHCR publishing evidence for the combined `naruon` image must include the exact image name, tag, local image ID, push result, and registry verification from GitHub Packages or an equivalent manifest/API query. Publish the package diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb2dc28c..7ec84c36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - OIDC token endpoint는 운영 환경에서 서버 전용 `OIDC_ALLOWED_HOSTS` 정확 호스트 allowlist를 필수로 적용합니다. hostname의 모든 DNS 결과가 공인 주소인지 검증한 뒤 해당 주소 집합을 native HTTP(S) 연결의 `lookup`에 고정하고, 원래 issuer hostname은 Host/TLS SNI로 유지해 사설 주소 해석과 DNS rebinding 사이의 TOCTOU를 차단합니다. 실패 로그는 입력 URL·token 대신 고정된 configuration/DNS·transport/response/backend-verification reason code만 남깁니다. - Trivy 2026-07-26 DB에서 새로 확인된 Next.js High 4건·Medium 5건(`CVE-2026-64641`–`CVE-2026-64649`)과 PostCSS High 1건(`GHSA-r28c-9q8g-f849`)을 제거하기 위해 Next.js/`eslint-config-next`를 `16.2.11`, PostCSS를 `8.5.18`로 갱신했습니다. 이후 2026-08-04 DB가 `8.5.18`에서 추가 탐지한 PostCSS Medium(`CVE-2026-69153`, 최초 수정 `8.5.23`)도 제거하도록 manifest·workspace override·lock을 `8.5.24`로 동기화했으며 저장소의 release-age 정책을 우회하지 않습니다. - `pnpm audit`가 개발 도구 체인에서 추가 탐지한 `brace-expansion <=5.0.7` High DoS(`GHSA-mh99-v99m-4gvg`)와 이후 `5.0.8`까지 영향을 주는 우회형 High DoS(`GHSA-rgw5-rvv9-x895`)는 `5.0.9` 전역 override로 제거했습니다. CommonJS default export를 기대하는 legacy `minimatch 3.1.5`에는 `expand` named export도 수용하는 최소 pnpm 패치를 적용해 ESLint/glob 동작을 보존합니다. 같은 감사에서 확인된 `undici 7.28.0`의 High 1건·Moderate 4건(`GHSA-4cwx-7wf7-3272` 등)은 `jsdom 30.0.1` 및 release-age 정책을 통과하는 `undici 8.9.0`으로 갱신했습니다. -- PostCSS의 Nano ID 해석을 `3.3.19`로 갱신해 사용자 제공 음수 크기에서 비보안 생성기가 무한 반복될 수 있는 High DoS(`CVE-2026-67214`, `GHSA-28wg-ghj8-5hjv`)와 후속 3.x 보안 floor를 충족합니다. workspace override·lockfile·release-governance 회귀 테스트가 같은 패치 버전을 강제합니다. +- PostCSS의 Nano ID 해석을 `3.3.18`로 갱신해 사용자 제공 음수 크기에서 비보안 생성기가 무한 반복될 수 있는 High DoS(`CVE-2026-67214`, `GHSA-28wg-ghj8-5hjv`)를 제거했습니다. lockfile과 release-governance 회귀 테스트가 같은 최초 수정 3.x 버전을 강제합니다. - root·frontend Docker build의 frozen install 계층이 pnpm manifest와 함께 `frontend/patches`를 먼저 복사하도록 수정해, 이미지 검증에서도 lockfile의 patched dependency를 동일하게 재현합니다. - Scorecard SARIF normalizer는 고정 workspace artifact로 정규화되는 `./scorecard-results.sarif`와 절대 경로를 동일하게 허용하면서 symlink·workspace 이탈은 계속 거부합니다. 도구 실행 실패 API는 CR/LF·제어 문자를 escape하고 500자로 제한하며, 로그에는 raw 도구 코드·예외 text 대신 SHA-256 기반 코드·traceback 상관 식별자만 기록합니다. - 백엔드 origin 보안 경계를 `frontend/src/lib/backend-url.ts`의 단일 생성기로 통합해 API proxy·session·OIDC callback이 같은 검증을 사용합니다. UI smoke의 새 `NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE` 이름은 실제 selector 의미를 드러내며, 기존 `..._SCREENSHOT_DIR`은 호환 alias로 계속 지원합니다. diff --git a/CLAUDE.md b/CLAUDE.md index 197a38766..be67bc80c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,11 +55,6 @@ pnpm run dev npm run test:e2e -- tests/e2e/dashboard-branding.spec.ts # Playwright (set LIVE_BASE_URL for live stacks) ``` -For pnpm security-floor checks, bind each root importer to its base-version -`packages` entry and its complete peer-qualified `snapshots` entry. A different -patched version elsewhere in the lockfile is not evidence for the importer's -declared resolution. - ### Whole-repo verification ```bash diff --git a/backend/tests/test_container_dependency_pin_contract.py b/backend/tests/test_container_dependency_pin_contract.py index e2352ea2a..fdd4f6620 100644 --- a/backend/tests/test_container_dependency_pin_contract.py +++ b/backend/tests/test_container_dependency_pin_contract.py @@ -13,15 +13,12 @@ import re from pathlib import Path -import pytest import yaml REPO_ROOT = Path(__file__).resolve().parents[2] _HASH_PATTERN = re.compile(r"--hash=sha256:([0-9a-f]{64})") _EXACT_PIN_PATTERN = re.compile(r"^([A-Za-z0-9_.-]+)==([^\\\s]+)") -_EXACT_SEMVER_PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") -POSTCSS_SECURITY_FLOOR = (8, 5, 24) def read_repo_text(relative_path: str) -> str: @@ -86,13 +83,6 @@ def importer_resolution(importer_section: dict[str, object], group: str, name: s return resolution -def exact_semver(value: str) -> tuple[int, int, int]: - """Return one exact three-part semantic version for security-floor comparison.""" - match = _EXACT_SEMVER_PATTERN.fullmatch(value) - assert match is not None, f"expected exact semantic version, got {value!r}" - return tuple(int(part) for part in match.groups()) - - def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None: """Keep backend, Strix, and frontend dependency floors reviewable together.""" backend_pins = exact_requirement_pins(read_repo_text("backend/requirements.txt")) @@ -104,7 +94,6 @@ def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None read_repo_text("requirements-strix-ci-hashes.txt") ) frontend_package = json.loads(read_repo_text("frontend/package.json")) - frontend_workspace = yaml.safe_load(read_repo_text("frontend/pnpm-workspace.yaml")) frontend_lock = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml")) assert backend_pins["cryptography"] == "50.0.0" @@ -127,98 +116,31 @@ def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None for digest in strix_records[pin] ) - reviewed_postcss = frontend_package["devDependencies"]["postcss"] - assert isinstance(reviewed_postcss, str) - assert exact_semver(reviewed_postcss) >= POSTCSS_SECURITY_FLOOR - assert frontend_package["overrides"]["postcss"] == reviewed_postcss - assert frontend_workspace["overrides"]["postcss"] == reviewed_postcss - root_importer = frontend_lock["importers"]["."] postcss_resolution = importer_resolution( root_importer, "devDependencies", "postcss" ) jsdom_resolution = importer_resolution(root_importer, "devDependencies", "jsdom") - assert postcss_resolution == { - "specifier": reviewed_postcss, - "version": reviewed_postcss, - } + assert postcss_resolution == {"specifier": "8.5.24", "version": "8.5.24"} assert jsdom_resolution == {"specifier": "^30.0.1", "version": "30.0.1"} + assert frontend_package["devDependencies"]["postcss"] == "8.5.24" assert frontend_package["devDependencies"]["jsdom"] == "^30.0.1" + assert frontend_package["overrides"]["postcss"] == "8.5.24" assert frontend_package["overrides"]["brace-expansion"] == "5.0.9" assert frontend_package["overrides"]["undici"] == "8.9.0" assert frontend_lock["overrides"] == { **frontend_lock["overrides"], - "postcss": reviewed_postcss, + "postcss": "8.5.24", "brace-expansion": "5.0.9", "undici": "8.9.0", } - for section_name in ("packages", "snapshots"): - section_records = frontend_lock[section_name] - assert isinstance(section_records, dict) - postcss_entries = [ - key for key in section_records if key.startswith("postcss@") - ] - assert postcss_entries, f"{section_name} must contain postcss" - for package_key in postcss_entries: - resolved_postcss = package_key.removeprefix("postcss@") - assert ( - exact_semver(resolved_postcss) >= POSTCSS_SECURITY_FLOOR - ), f"{section_name} contains postcss below the reviewed security floor" - - postcss_snapshot_key = f"postcss@{postcss_resolution['version']}" - assert ( - postcss_snapshot_key in frontend_lock["snapshots"] - ), "root importer postcss snapshot must exist" - package_records = frontend_lock["packages"] for exact_lock_entry in ( - f"postcss@{reviewed_postcss}", + "postcss@8.5.24", "jsdom@30.0.1", "brace-expansion@5.0.9", "undici@8.9.0", ): assert exact_lock_entry in package_records - - -@pytest.mark.parametrize("section_name", ("packages", "snapshots")) -def test_postcss_security_floor_rejects_below_floor_lock_entry( - monkeypatch: pytest.MonkeyPatch, - section_name: str, -) -> None: - """Reject a stale transitive PostCSS resolution even when the direct pin is valid.""" - original_read_repo_text = read_repo_text - lock = yaml.safe_load(original_read_repo_text("frontend/pnpm-lock.yaml")) - lock[section_name]["postcss@8.5.23"] = {} - mutated_lock_text = yaml.safe_dump(lock, sort_keys=False) - - def read_mutated_repo_text(relative_path: str) -> str: - if relative_path == "frontend/pnpm-lock.yaml": - return mutated_lock_text - return original_read_repo_text(relative_path) - - monkeypatch.setitem(globals(), "read_repo_text", read_mutated_repo_text) - with pytest.raises(AssertionError, match=f"{section_name} contains postcss below"): - test_container_provenance_dependency_pins_match_reviewed_manifests() - - -def test_postcss_root_importer_requires_exact_snapshot( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Reject a lockfile whose root PostCSS resolution has no matching snapshot.""" - original_read_repo_text = read_repo_text - lock = yaml.safe_load(original_read_repo_text("frontend/pnpm-lock.yaml")) - postcss_version = lock["importers"]["."]["devDependencies"]["postcss"]["version"] - lock["snapshots"].pop(f"postcss@{postcss_version}") - lock["snapshots"].setdefault("postcss@8.5.25", {}) - mutated_lock_text = yaml.safe_dump(lock, sort_keys=False) - - def read_mutated_repo_text(relative_path: str) -> str: - if relative_path == "frontend/pnpm-lock.yaml": - return mutated_lock_text - return original_read_repo_text(relative_path) - - monkeypatch.setitem(globals(), "read_repo_text", read_mutated_repo_text) - with pytest.raises(AssertionError, match="root importer postcss snapshot"): - test_container_provenance_dependency_pins_match_reviewed_manifests() diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py deleted file mode 100644 index e200c5ead..000000000 --- a/backend/tests/test_frontend_framework_security_floor.py +++ /dev/null @@ -1,353 +0,0 @@ -"""Fail closed when frontend framework/image dependencies regress below patched floors.""" - -from __future__ import annotations - -import json -import re -from pathlib import Path -from typing import Any - -import pytest -import yaml - - -REPO_ROOT = Path(__file__).resolve().parents[2] -FRONTEND_ROOT = REPO_ROOT / "frontend" -NEXT_SECURITY_FLOOR = (16, 3, 3) -SHARP_SECURITY_FLOOR = (0, 35, 4) -JS_YAML_SECURITY_FLOOR = (4, 3, 2) -VITEST_SECURITY_FLOOR = (4, 1, 11) - - -def _exact_version(value: str) -> tuple[int, int, int]: - """Return a three-part exact version, rejecting ranges and prereleases.""" - - match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value) - assert match is not None, f"expected exact semantic version, got {value!r}" - return tuple(int(part) for part in match.groups()) - - -def _resolved_version(value: str) -> tuple[int, int, int]: - """Return the exact version prefix from a pnpm peer-qualified resolution.""" - - version = value.split("(", 1)[0] - return _exact_version(version) - - -def _package_key_version(package_key: str, package_name: str) -> tuple[int, int, int]: - """Return the version encoded by one pnpm package/snapshot key.""" - - prefix = f"{package_name}@" - assert package_key.startswith(prefix), ( - f"expected {package_name!r} lock key, got {package_key!r}" - ) - return _resolved_version(package_key[len(prefix) :]) - - -def _assert_lock_contract( - lock: dict[str, Any], - next_value: str, - eslint_next_value: str, - sharp_value: str, -) -> None: - """Validate root resolution identity and every locked Next.js/sharp security floor.""" - - importer = lock["importers"]["."] - next_import = importer["dependencies"]["next"] - assert next_import["specifier"] == next_value, ( - "root importer must preserve the package.json Next.js specifier" - ) - assert _resolved_version(str(next_import["version"])) == _exact_version(next_value), ( - "root importer must resolve the reviewed Next.js release" - ) - assert f"next@{next_import['version']}" in lock["snapshots"], ( - "root importer Next.js resolution must reference an existing snapshot" - ) - - eslint_next_import = importer["devDependencies"]["eslint-config-next"] - assert eslint_next_import["specifier"] == eslint_next_value, ( - "root importer must preserve the eslint-config-next specifier" - ) - assert _resolved_version(str(eslint_next_import["version"])) == _exact_version( - eslint_next_value - ), "root importer must resolve the reviewed eslint-config-next release" - assert f"eslint-config-next@{eslint_next_import['version']}" in lock["snapshots"], ( - "root importer eslint-config-next resolution must reference an existing snapshot" - ) - - assert str(lock["overrides"]["sharp"]) == sharp_value, ( - "lockfile sharp override must match the reviewed workspace override" - ) - - expected_next = _exact_version(next_value) - expected_sharp = _exact_version(sharp_value) - for section_name in ("packages", "snapshots"): - section = lock[section_name] - next_keys = [key for key in section if key.startswith("next@")] - sharp_keys = [key for key in section if key.startswith("sharp@")] - - assert next_keys, f"{section_name} must contain a Next.js resolution" - assert sharp_keys, f"{section_name} must contain a sharp resolution" - assert any( - _package_key_version(key, "next") == expected_next for key in next_keys - ), f"{section_name} must contain the reviewed Next.js release" - assert any( - _package_key_version(key, "sharp") == expected_sharp for key in sharp_keys - ), f"{section_name} must contain the reviewed sharp release" - - for package_key in next_keys: - assert _package_key_version(package_key, "next") >= NEXT_SECURITY_FLOOR, ( - f"{section_name} contains Next.js below the reviewed security floor: " - f"{package_key}" - ) - for package_key in sharp_keys: - assert _package_key_version(package_key, "sharp") >= SHARP_SECURITY_FLOOR, ( - f"{section_name} contains sharp below the reviewed security floor: " - f"{package_key}" - ) - - -def _frontend_security_inputs() -> tuple[str, str, str, dict[str, Any]]: - """Load the manifest, workspace override, and generated lock contract.""" - - package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) - next_value = package["dependencies"]["next"] - eslint_next_value = package["devDependencies"]["eslint-config-next"] - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - return next_value, eslint_next_value, sharp_value, lock - - -def test_frontend_framework_and_image_security_floors() -> None: - """Keep manifests and every generated lock resolution at reviewed patched releases.""" - - next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() - - assert _exact_version(next_value) >= NEXT_SECURITY_FLOOR, ( - "Next.js must include the fixes for CVE-2026-75604 and " - "GHSA-2xp9-vwfh-vxw4" - ) - assert eslint_next_value == next_value, ( - "eslint-config-next must stay on the same reviewed release as Next.js" - ) - assert _exact_version(sharp_value) >= SHARP_SECURITY_FLOOR, ( - "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" - ) - _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) - - -def test_js_yaml_security_floor_covers_every_lock_resolution() -> None: - """Keep every js-yaml resolution above the reviewed denial-of-service floor.""" - - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - for section_name in ("packages", "snapshots"): - js_yaml_keys = [ - key for key in lock[section_name] if key.startswith("js-yaml@") - ] - for package_key in js_yaml_keys: - assert ( - _package_key_version(package_key, "js-yaml") - >= JS_YAML_SECURITY_FLOOR - ), f"{section_name} contains js-yaml below the reviewed security floor" - - -def test_vitest_security_floor_covers_manifest_and_lock() -> None: - """Keep Vitest and its coverage package above the reviewed traversal floor.""" - - package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - importer = lock["importers"]["."]["devDependencies"] - for package_name in ("vitest", "@vitest/coverage-v8"): - declared_value = package["devDependencies"][package_name] - assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR - importer_entry = importer[package_name] - assert importer_entry["specifier"] == declared_value, ( - f"root importer must preserve the package.json {package_name} specifier" - ) - assert _resolved_version(str(importer_entry["version"])) == _exact_version( - declared_value - ), f"root importer must resolve the reviewed {package_name} release" - resolved_version = str(importer_entry["version"]) - base_version = resolved_version.split("(", 1)[0] - assert f"{package_name}@{base_version}" in lock["packages"], ( - f"root importer {package_name} resolution must reference an existing package record" - ) - assert f"{package_name}@{importer_entry['version']}" in lock["snapshots"], ( - f"root importer {package_name} resolution must reference an existing snapshot" - ) - for section_name in ("packages", "snapshots"): - package_keys = [ - package_key - for package_key in lock[section_name] - if package_key.startswith(f"{package_name}@") - ] - assert package_keys, ( - f"{section_name} must contain a {package_name} resolution" - ) - for package_key in package_keys: - assert ( - _package_key_version(package_key, package_name) - >= VITEST_SECURITY_FLOOR - ), f"{section_name} contains {package_name} below the reviewed floor" - - -@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) -@pytest.mark.parametrize("section_name", ["packages", "snapshots"]) -def test_vitest_security_floor_rejects_missing_lock_resolution( - monkeypatch: pytest.MonkeyPatch, - package_name: str, - section_name: str, -) -> None: - """Reject a regenerated lock section that drops an expected Vitest resolution.""" - - package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - lock[section_name] = { - key: value - for key, value in lock[section_name].items() - if not key.startswith(f"{package_name}@") - } - lock_text = yaml.safe_dump(lock) - original_read_text = Path.read_text - - def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: - if path == FRONTEND_ROOT / "package.json": - return package_text - if path == FRONTEND_ROOT / "pnpm-lock.yaml": - return lock_text - return original_read_text(path, *args, **kwargs) - - monkeypatch.setattr(Path, "read_text", _read_text) - with pytest.raises(AssertionError): - test_vitest_security_floor_covers_manifest_and_lock() - - -@pytest.mark.parametrize("field", ["specifier", "version"]) -def test_security_floor_rejects_root_importer_drift(field: str) -> None: - """Reject a partially regenerated lock whose root Next.js importer drifts.""" - - next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() - lock["importers"]["."]["dependencies"]["next"][field] = "16.3.2" - - with pytest.raises(AssertionError): - _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) - - -@pytest.mark.parametrize( - ("section_name", "package_key"), - [("packages", "next@16.3.2"), ("snapshots", "sharp@0.35.3")], -) -def test_security_floor_rejects_every_below_floor_lock_entry( - section_name: str, package_key: str -) -> None: - """Reject any stale vulnerable Next.js or sharp package/snapshot entry.""" - - next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() - lock[section_name][package_key] = {} - - with pytest.raises(AssertionError): - _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) - - -@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) -@pytest.mark.parametrize("field", ["specifier", "version"]) -def test_vitest_security_floor_rejects_root_importer_drift( - monkeypatch: pytest.MonkeyPatch, - package_name: str, - field: str, -) -> None: - """Reject a root Vitest importer that no longer matches the reviewed manifest.""" - - package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - lock["importers"]["."]["devDependencies"][package_name][field] = "4.1.12" - lock_text = yaml.safe_dump(lock) - original_read_text = Path.read_text - - def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: - if path == FRONTEND_ROOT / "package.json": - return package_text - if path == FRONTEND_ROOT / "pnpm-lock.yaml": - return lock_text - return original_read_text(path, *args, **kwargs) - - monkeypatch.setattr(Path, "read_text", _read_text) - with pytest.raises(AssertionError): - test_vitest_security_floor_covers_manifest_and_lock() - - -@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) -def test_vitest_security_floor_rejects_missing_root_snapshot( - monkeypatch: pytest.MonkeyPatch, - package_name: str, -) -> None: - """Reject a root Vitest resolution whose exact peer-qualified snapshot vanished.""" - - package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - resolution = str( - lock["importers"]["."]["devDependencies"][package_name]["version"] - ) - snapshot_key = f"{package_name}@{resolution}" - snapshot = lock["snapshots"].pop(snapshot_key) - lock["snapshots"][f"{package_name}@4.1.12"] = snapshot - lock_text = yaml.safe_dump(lock) - original_read_text = Path.read_text - - def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: - if path == FRONTEND_ROOT / "package.json": - return package_text - if path == FRONTEND_ROOT / "pnpm-lock.yaml": - return lock_text - return original_read_text(path, *args, **kwargs) - - monkeypatch.setattr(Path, "read_text", _read_text) - with pytest.raises(AssertionError): - test_vitest_security_floor_covers_manifest_and_lock() - - -@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) -def test_vitest_security_floor_rejects_missing_root_package( - monkeypatch: pytest.MonkeyPatch, - package_name: str, -) -> None: - """Reject a root Vitest resolution whose base package record vanished.""" - - package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - resolution = str( - lock["importers"]["."]["devDependencies"][package_name]["version"] - ) - package_key = f"{package_name}@{resolution.split('(', 1)[0]}" - package_record = lock["packages"].pop(package_key) - lock["packages"][f"{package_name}@4.1.12"] = package_record - lock_text = yaml.safe_dump(lock) - original_read_text = Path.read_text - - def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: - if path == FRONTEND_ROOT / "package.json": - return package_text - if path == FRONTEND_ROOT / "pnpm-lock.yaml": - return lock_text - return original_read_text(path, *args, **kwargs) - - monkeypatch.setattr(Path, "read_text", _read_text) - with pytest.raises(AssertionError): - test_vitest_security_floor_covers_manifest_and_lock() diff --git a/backend/tests/test_frontend_nanoid_security.py b/backend/tests/test_frontend_nanoid_security.py index 66bc5b499..f80b23a01 100644 --- a/backend/tests/test_frontend_nanoid_security.py +++ b/backend/tests/test_frontend_nanoid_security.py @@ -8,7 +8,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] FRONTEND_LOCK = REPO_ROOT / "frontend" / "pnpm-lock.yaml" -PATCHED_NANOID_VERSION = "3.3.19" +PATCHED_NANOID_VERSION = "3.3.18" def test_frontend_lock_resolves_only_patched_nanoid_3x() -> None: diff --git a/backend/tests/test_js_yaml_dependency_security.py b/backend/tests/test_js_yaml_dependency_security.py deleted file mode 100644 index 11f1bc3ad..000000000 --- a/backend/tests/test_js_yaml_dependency_security.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Keep the generated frontend dependency graph on the reviewed js-yaml floor.""" - -from pathlib import Path - -import yaml - - -FRONTEND_ROOT = Path(__file__).resolve().parents[2] / "frontend" -JS_YAML_PATCHED_RELEASE = "4.3.2" - - -def _resolved_version(package_key: str) -> tuple[int, int, int]: - """Return the semantic version from one peer-qualified js-yaml lock key.""" - - prefix = "js-yaml@" - assert package_key.startswith(prefix) - version = package_key[len(prefix) :].split("(", 1)[0] - return tuple(int(part) for part in version.split(".")) - - -def test_js_yaml_override_lock_and_eslint_consumer_share_patched_release() -> None: - """Bind workspace policy, generated lock identity, and the ESLint consumer together.""" - - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - - assert str(workspace["overrides"]["js-yaml"]) == JS_YAML_PATCHED_RELEASE - assert str(lock["overrides"]["js-yaml"]) == JS_YAML_PATCHED_RELEASE - - floor = (4, 3, 2) - for section_name in ("packages", "snapshots"): - keys = [key for key in lock[section_name] if key.startswith("js-yaml@")] - assert keys, f"{section_name} must contain a js-yaml resolution" - assert {_resolved_version(key) for key in keys} == {floor} - - eslint_snapshots = [ - value - for key, value in lock["snapshots"].items() - if key.startswith("@eslint/eslintrc@") - ] - assert eslint_snapshots, "lock must retain the ESLint configuration snapshot" - assert any( - str(snapshot.get("dependencies", {}).get("js-yaml")) - == JS_YAML_PATCHED_RELEASE - for snapshot in eslint_snapshots - ), "ESLint must consume the reviewed js-yaml release" diff --git a/docs/doctoring/settings-native-disabled-accessibility.md b/docs/doctoring/settings-native-disabled-accessibility.md deleted file mode 100644 index d717e84ac..000000000 --- a/docs/doctoring/settings-native-disabled-accessibility.md +++ /dev/null @@ -1,54 +0,0 @@ -# Settings native-disabled accessibility decision - -Date: 2026-09-17 -Owner lane: `SettingsLayout` native disabled semantics (#1676) - -## Problem - -Two Settings actions already use native HTML `