Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a3421f3
fix(deps): bump next to 16.3.4 and sharp override to 0.35.4
seonghobae Sep 9, 2026
d76f7b4
test(security): lock frontend patched dependency floors
seonghobae Sep 9, 2026
a5abe04
test(deps): reproduce weak frontend lock validation
seonghobae Sep 9, 2026
e8a54fc
fix(deps): validate frontend lock security floors structurally
seonghobae Sep 9, 2026
b97f42f
fix(deps): raise frontend audit security floors
seonghobae Sep 9, 2026
09cb87a
test(deps): bind js-yaml owner succession structurally
seonghobae Sep 9, 2026
a6715c9
test(deps): reproduce missing Vitest lock-entry gap
seonghobae Sep 9, 2026
21897d8
test(deps): require Vitest lock resolutions
seonghobae Sep 9, 2026
15fecaa
test(security): reproduce Vitest importer drift
seonghobae Sep 9, 2026
17a7618
test(security): bind Vitest importer contract
seonghobae Sep 9, 2026
5487141
fix(deps): bind Vitest importer package records
seonghobae Sep 12, 2026
4e66036
docs(gap): record Vitest lockfile repair evidence
seonghobae Sep 12, 2026
d8327d4
fix(governance): keep dependency PR off canonical gap ledger
seonghobae Sep 12, 2026
8175f7f
test(deps): require patched Nano ID resolution
seonghobae Sep 12, 2026
9d6d1e0
fix(deps): pin patched Nano ID in pnpm owner
seonghobae Sep 12, 2026
ecbeb7a
test(deps): bind PostCSS floor to reviewed manifest
seonghobae Sep 15, 2026
9066c3a
test(deps): reproduce below-floor PostCSS lock drift
seonghobae Sep 16, 2026
ccb18dd
fix(deps): reject stale PostCSS lock resolutions
seonghobae Sep 16, 2026
c2f9480
test(deps): reproduce missing PostCSS importer snapshot
seonghobae Sep 16, 2026
509be4c
fix(deps): bind root PostCSS importer to snapshot
seonghobae Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,10 @@ 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
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.18`로 갱신해 사용자 제공 음수 크기에서 비보안 생성기가 무한 반복될 수 있는 High DoS(`CVE-2026-67214`, `GHSA-28wg-ghj8-5hjv`)를 제거했습니다. lockfile과 release-governance 회귀 테스트가 같은 최초 수정 3.x 버전을 강제합니다.
- PostCSS의 Nano ID 해석을 `3.3.19`로 갱신해 사용자 제공 음수 크기에서 비보안 생성기가 무한 반복될 수 있는 High DoS(`CVE-2026-67214`, `GHSA-28wg-ghj8-5hjv`)와 후속 3.x 보안 floor를 충족합니다. workspace override·lockfile·release-governance 회귀 테스트가 같은 패치 버전을 강제합니다.
- 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로 계속 지원합니다.
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ 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
Expand Down
88 changes: 83 additions & 5 deletions backend/tests/test_container_dependency_pin_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
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:
Expand Down Expand Up @@ -83,6 +86,13 @@ 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"))
Expand All @@ -94,6 +104,7 @@ 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"
Expand All @@ -116,31 +127,98 @@ 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": "8.5.24", "version": "8.5.24"}
assert postcss_resolution == {
"specifier": reviewed_postcss,
"version": reviewed_postcss,
}
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": "8.5.24",
"postcss": reviewed_postcss,
"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 (
"postcss@8.5.24",
f"postcss@{reviewed_postcss}",
"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()
Loading
Loading