feat: phase 3 pr #10 — project detail (overview + components tabs)#3
Merged
Conversation
…ignore policy Records the 3-round (8dca1fa → a1e2e14 → ac39bf0) Trivy 33 → 0 path: Round 1: apt upgrade + cdxgen 11→12 → 33 → 27 (FAIL, security-reviewer) Round 2: npm 11.13 self-upgrade + .trivyignore for ORT 3 jars → 27 → 12 (FAIL — cdxgen-plugins-bin postinstall defeats --omit=optional) Round 3: .trivyignore policy expanded to category (3) "fixed-upstream- but-bundled-in-dev-tooling-runtime-unreached" with 12 cdxgen- plugins-bin gobinary entries (CRITICAL grpc CVE-2026-33186 + 11 HIGH) → 0 findings, image-scan hard-fail green. Documents the empirical finding that `npm install --omit=optional` is ineffective on cdxgen 12.x (postinstall script downloads plugins-bin independently of the optionalDependencies key) so future contributors don't repeat the same investigation cycle. Phase 8 hardening backlog logged: carve plugins-bin out at image build once cdxgen self-test compatibility is verified, base digest pin, non-root USER, NodeSource curl|bash → signed-by, npm audit signatures. Routed onto the same branch (feature/phase3-pr10-project-detail) as the Phase 3 work that follows so the handoff lands on main with that PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 PR #10 — Project Detail API + UI per docs/v2-execution-plan.md §3.4 tables 3.1 / 3.2 / 3.3. Backend (apps/backend/): 3 new endpoints under /v1, all auth-gated (developer role) with IDOR guard + RFC 7807 problem+json on 4xx/5xx: - GET /v1/projects/{id}/overview — risk score, severity + license distributions, recent 5 scans (concurrent SELECTs via asyncio.gather; uses ix_scans_project_created_at + existing scan_components / vulnerability_findings / license_findings indexes per db-designer's EXPLAIN review — no migration needed). - GET /v1/projects/{id}/components — 1만-row paginated list (limit ≤ 500, offset ≥ 0); search (ILIKE), severity / license_ category multi-filter, sort by name|severity|license × asc|desc. Items + count run concurrently. Cross-team component existence is hidden (404, not 403) — deliberate hide-existence. - GET /v1/components/{id} — drawer payload (vuln list + raw_data jsonb + license). schemas/project_detail.py (185 LOC) — ProjectOverviewResponse, ScanSummary, ComponentSummary, ComponentListResponse, VulnerabilityRef, ComponentDetailResponse. Severity / license category as Literal so OpenAPI shows allowed values exactly. services/project_detail_service.py (723 LOC) — query-time aggregation off project.latest_scan_id (the actual schema does not denormalize severity_max / license_category onto components — both come from vulnerability_findings → vulnerabilities and license_findings → licenses joins). _can_access_team mirrors services/project_service. scripts/seed_e2e_user.py — new flags --with-scan, --component-count, --component-prefix for e2e fixture data. Tests (37 new, ruff + mypy --strict clean): - unit/test_risk_score.py (7 pure-unit) - unit/test_project_detail_service.py (19 service-DB integration) - integration/test_project_detail_api.py (11 HTTP + RFC 7807) Frontend (apps/frontend/): ProjectDetailPage with shadcn-style Tabs (4 tabs; Vulnerabilities / Licenses are disabled placeholders for PR #11/#12). Hand-rolled minimal Tabs primitive (no @radix-ui/react-tabs dep) — same shape so a future swap is non-breaking. OverviewTab — Risk gauge (semicircular SVG, no recharts) + severity & license stacked bars + recent-5 scans table. All charts are pure SVG / Tailwind divs (recharts deliberately not added — bundle size + v1 XSS surface avoidance). ComponentsTab — react-virtuoso TableVirtuoso for 10k-row virtual scroll, infinite-scroll via TanStack Query useInfiniteQuery; inline toolbar (search debounced 300ms, severity / license multi-select filters, sort key + order); URL search-params sync (deep-linking for ?tab, ?search, ?severity, ?license_category, ?sort, ?order, ?drawer). Row click opens right-slide Sheet drawer; drawer fetches detail lazily on open. ComponentDrawer — vuln list (CVE id + severity badge + CVSS + fixed_in), raw_data accordion (collapsible JSON viewer via <pre> + JSON.stringify; React text-node escapes — no innerHTML). Tests (41 new, vitest): - 9 spec files; coverage 93.41% lines / 85.29% branches / 87.09% funcs (above 80% gate). i18n: project_detail namespace with 84 keys, both EN + KO complete (i18n-specialist mapped against existing common.risk + projects.status conventions; new domain terms — Severity, Forbidden/Conditional/ Allowed, Cancelled, Source/Container, Fixed in, Breadcrumb, Unknown — logged for docs/glossary.md follow-up). Router: /projects/:id added (RequireAuth-guarded) between /projects and the catch-all. ProjectListPage row name now navigates via Link. E2E (apps/frontend/tests/): PortalPage harness extended with 8 verbs: openProjectDetail, selectTab, filterComponentsBySeverity, searchComponents, openComponentDrawer, assertRiskScore + 2 ergonomic helpers. All event-driven waits — no page.waitForTimeout. project_detail.spec.ts — 6 scenarios: 1. Open detail → Overview default render (4 cards + risk-gauge) 2. Components endReached pagination growth (250 rows) 3. Drawer open + URL sync 4. Search debounce + clear restores total 5. Severity multi-filter + URL sync 6. Sort key + order toggle (severity desc/asc) Scenario 2 seeds 250 components (not 10k — keeps suite under 30s/test CI budget). The CLI still supports --component-count 10000 for ad-hoc perf testing; 60fps validation deferred to Lighthouse measurement pre-GA. Security review (security-reviewer): PASS, 0 blockers, 1 Medium (raw_data redaction — same-team exposure surface introduced; mitigated by 256 KiB JSONB size guard from PR #8; logged as Phase 8 follow-up PR), 4 Low + 3 Info — all logged as separate follow-up PRs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…functions
CI run 25430060584 backend test job failed with:
asyncpg.exceptions.UndefinedFunctionError:
operator does not exist: vuln_severity = character varying
HINT: No operator matches the given name and argument types.
You might need to add explicit type casts.
Postgres ENUM types do not implicitly compare with varchar. The CASE
expression form `case({literal("foo"): N, ...}, value=Column)` compiles
to `CASE Column WHEN 'foo' THEN N END`, where 'foo' is bound as varchar
— which Postgres refuses to compare with the vuln_severity / license_
category ENUM columns. The error surfaces only at execute time
(asyncpg prepare phase), so unit + integration suites both went red.
Fix: cast the LHS column to text inside the CASE so dict-key string
literals compare cleanly. No schema change — purely a query rewrite.
case({literal("critical"): 5, ...}, value=Vulnerability.severity)
→ case({literal("critical"): 5, ...},
value=cast(Vulnerability.severity, String))
Same fix applied to _license_rank_case() (LicenseModel.category is
the license_category ENUM).
Both functions ship inline comments explaining the cast so future
contributors don't strip it.
Refs: PR #10 (feature/phase3-pr10-project-detail), CI run 25430060584,
job 74593651095.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 25430459893 backend test job failed with:
asyncpg.exceptions.UniqueViolationError: duplicate key value violates
unique constraint "uq_licenses_spdx_id"
FAILED tests/unit/test_project_detail_service.py::
test_component_detail_returns_drawer_payload_with_vulns
The unit-test DB fixture is module-scoped (`_migrate_once` runs alembic
upgrade head exactly once per module, then commits from each test
survive into the next). Multiple tests in this file legitimately ask
for the same hardcoded spdx_id (MIT for both `test_overview_aggregates_
severity_and_license_distributions` and `test_component_detail_returns_
drawer_payload_with_vulns`), so the second INSERT hits
`uq_licenses_spdx_id`.
Two paths considered:
A. Make every test use unique spdx_ids (`MIT-{suffix}` etc).
Rejected — the readability of `MIT`/`GPL-3.0`/`AGPL-3.0`/
`Apache-2.0` in test code is intentional (they exercise category
classification at the spdx-id level), and any future test adding
a new hardcoded spdx_id would silently break too.
B. Make `_make_license` idempotent (SELECT first, INSERT when absent).
Adopted. The fixture now treats `spdx_id` as a stable handle —
the first call inserts, subsequent calls in any test reuse the
existing row. `category` of an existing row wins; callers that
need a different category must pick a different spdx_id (a
constraint documented in the new docstring).
Refs: PR #10 (feature/phase3-pr10-project-detail), CI run 25430459893,
job 74595050764.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
haksungjang
added a commit
that referenced
this pull request
May 7, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 7, 2026
…_like import Two CI failures from the previous chore-pr3 commit: 1. typecheck (backend) — `tests/unit/test_license_service.py` imported `_escape_like` from `services.vulnerability_service`. After chore PR #3 promoted the helper to `core.sql_safety`, the vulnerability_service re-export became a renamed alias which mypy strict refuses to count as "explicitly exported." The test now imports `escape_like as _escape_like` directly from `core.sql_safety` — same call site alias, no in-tree underscore re-export. 2. test (backend) — the four `/notice` integration tests (text inline, markdown, download, UTF-8 round-trip) all returned 500 because slowapi's `@limiter.limit("10/minute")` wrapper inherits slowapi's own module as its `__globals__`. With `from __future__ import annotations` enabled, FastAPI calls `typing.get_type_hints()` on the wrapper and Pydantic raises "TypeAdapter is not fully defined" for the `uuid.UUID` path parameter. Mirror the workaround already in `apps/backend/api/v1/auth.py` for the login endpoint: patch the names the wrapper needs (`uuid`, `AsyncSession`, `Request`, `Response`, `Depends`, `Query`, `CurrentUser`) into the wrapper's `__globals__` after the decorator applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 7, 2026
CI image build (run 25475555213, jobs `e2e (scan-flow)` + `image-scan
(worker)`) failed at worker stage 12/15 with:
sha256sum: 'standard input': no properly formatted checksum lines found
Root cause: `https://go.dev/dl/<file>.sha256` returns an HTML refresh
page (the human download UI), not the raw 64-hex string. The previous
`expected_sha="$(curl ...)"` captured the HTML and fed it to
`sha256sum -c -`, which then choked on `<!DOCTYPE html>` as a
non-checksum line. The tarball curl on the same path worked because
it follows the redirect to dl.google.com transparently with `-L`.
Fix:
* Switch the Go tarball URL to `https://dl.google.com/go/<file>`
(where Go's CDN serves both the tarball and a raw `.sha256`).
* Pin the per-arch Go SHA-256 as a build-time `ENV` constant
(`GO_SHA256_AMD64` / `GO_SHA256_ARM64`) — same pattern Debian's
`golang` image uses. Bumping the version now requires bumping the
hash deliberately. This also addresses chore PR #4
security-reviewer Medium #3 (TOFU same-origin hash → pinned
constant), so we collapse both items into one fix instead of
carrying it to chore PR #5.
* Same treatment for Gradle: pin `GRADLE_SHA256` as ENV constant
rather than fetching the `.sha256` companion at build time. The
Gradle URL did work (Gradle's CDN serves a raw checksum), but the
consistency win + the security-reviewer recommendation outweigh
the "fetch dynamically" ergonomics.
Verified by manually fetching the published hashes against pinned
versions:
curl https://dl.google.com/go/go1.22.7.linux-amd64.tar.gz.sha256
curl https://dl.google.com/go/go1.22.7.linux-arm64.tar.gz.sha256
curl https://services.gradle.org/distributions/gradle-8.10.2-bin.zip.sha256
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 7, 2026
…ation (chore PR #5) (#9) * chore(scan): scrub worker secrets from prep subprocesses + dt_resync source guard Part A — security-reviewer Medium #1 follow-up. `_run_prep` previously inherited the full worker env (DT_API_KEY / SECRET_KEY / DATABASE_URL credentials / *_WEBHOOK_URL) into `bundle lock` / `cargo generate-lockfile` / `go mod tidy` / `dotnet restore`. A hostile clone (malicious NuGet feed, Go `replace` directive) could exfiltrate those through resolver telemetry. Now we hand each prep subprocess a `_scrubbed_env` built from a documented allowlist (PATH/HOME/LANG + per-language config vars only) and seed `.NET` telemetry-opt-out by default. Part E.1 — `_upsert_vulnerability` previously called `raw.get("source", {}) .get("name")` to derive `external_id`, which raises AttributeError on DT 4.13's plain-string `source` shape. The fallback through `vulnId` masked it most of the time, but the asymmetry with the line 110-118 `source` resolution was inconsistent. Both reads now share the same isinstance-guarded shape. Tests: - `test_run_prep_passes_only_allowlisted_env` pins that DT_API_KEY / SECRET_KEY / DATABASE_URL / SLACK_WEBHOOK_URL are NOT inherited. - `test_run_prep_seeds_dotnet_telemetry_optout` pins the default telemetry-opt-out injection. - 37 prep tests stay green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(settings): formalize git push + gh pr merge allow rules Carry-over from chore PR #3. The user's policy update (2026-05-06) authorized Claude to run `git push` and `gh pr merge` directly; the working-tree change had been sitting on main since then. This is the user's edit verbatim — no further modification by Claude. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps,trivy): bump python-multipart 0.0.27 + ignore 4 Maven UNREACHED HIGHs chore PR #4 introduced a Maven 3.9 bundle in the worker image so we can run `mvn dependency:tree` for Java pre-cdxgen prep. Trivy's next scan flagged five new HIGH CVEs that did not affect chore PR #4 itself (soft-fail). This commit triages all five: - python-multipart 0.0.22 → 0.0.27 — direct dep, picks up the CVE-2026-42561 boundary-parsing DoS fix. - 4 Maven JARs (netty-codec, netty-codec-http, bcpg-jdk18on, plexus-utils) — vendored alongside `mvn`. We only ever invoke `mvn -B -q -DskipTests dependency:tree`; none of the vulnerable code paths (HTTP server frames, OpenPGP packet parsing, untrusted XML XXE) are reached by a resolver-only invocation. Added to .trivyignore as Category (3) UNREACHED with per-CVE reach analysis matching the policy header. Trivy stays soft-fail until Phase 8 GA hardening — this commit is about keeping the residual list explicit, not about gating the build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(docs): archive completed chore PR #3 / #4 next-session prompts Both prompts were used to start their respective sessions and are now historical record only. Moving them out of the live `docs/sessions/` root keeps `git status` clean for subsequent sessions and preserves the original briefings under `docs/sessions/archive/next-session-prompts/` for traceability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(authz): migrate project / project_detail / vulnerability services to assert_team_access Part D of chore PR #5 (carry-over from chore PR #3). Replaces 11 remaining `if not _can_access_team(...): raise XxxForbidden/NotFound(...)` call sites across project_service (3), project_detail_service (3), and vulnerability_service (3 + 2 redundant log.warning blocks) with the centralized `assert_team_access(actor, team_id, log=..., resource=..., resource_id=..., deny=lambda: ExistingException(...))` helper. Behavior preserved: same exception class, same message, same 403/404 existence-hide policy. The `authz.cross_team_attempt` warning event now flows through a single shape, matching license_service / obligation_service which were migrated in chore PR #3. The vulnerability_service duplicate inline `log.warning("authz.cross_team_attempt", ...)` blocks before the raise are removed (the helper emits the same event). scan_service.py keeps its local `_can_access_team` for now and is tracked as a separate carry-over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(scan): silence S108 on _scrubbed_env HOME default with rationale Inline noqa + comment per security-reviewer Medium #1 implementation review. The `/tmp` value is a HOME hint for resolver config caches (`~/.cargo`, `~/.dotnet`), not a tempfile-creation site — S108's collision / symlink-race rationale doesn't apply, and the workspace is wiped after every scan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(ui): swap stand-in tabs primitive for @radix-ui/react-tabs Replace the hand-rolled Tabs stand-in (PR #10) with the canonical shadcn/ui component built on @radix-ui/react-tabs@1.1.0. The exported API (Tabs / TabsList / TabsTrigger / TabsContent) and the visual styling (border-b strip, h-9 trigger, primary underline on the active tab) are preserved 1:1 — existing call sites and Playwright harness selectors (role="tab", data-state, data-testid) keep working unchanged. Active-state styling now uses radix's data-[state=active] selector instead of a conditional className, matching the upstream shadcn pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(integrations): add multi-ecosystem license fetcher (Maven/PyPI/crates/pkg.go.dev) The 2026-05-07 UAT matrix flagged 91/39/164/29 unknown licenses on the pilot-java-maven / pilot-python / pilot-rust / pilot-go scans because cdxgen does not always extract declared licences for transitive deps. This adds a post-process fetcher that hits each ecosystem's authoritative registry to fill those gaps with SPDX ids, caches the answer (positive or negative) in a new license_fetch_cache table with a 24h TTL, and emits the result as a "concluded" LicenseFinding so the downstream UI can tell registry-derived licences apart from cdxgen's "declared" ones. Module layout: integrations/license_fetcher/ base.py — LicenseFetchResult, request_with_retry helper, SPDX normalization (free-text → canonical id), per-host throttle (crates.io 1 req/sec, others 0.1-0.25s). maven.py — repo1.maven.org/<g>/<a>/<v>/<a>-<v>.pom XML parser. pypi.py — pypi.org/pypi/<n>/<v>/json, prefers Trove classifiers. crates.py — crates.io/api/v1/crates/<n>/<v>, SPDX expression (compound expressions yield None — same policy as _extract_spdx_ids). pkggo.py — pkg.go.dev HTML scrape of the UnitMeta-license block. __init__.py — dispatch by purl prefix + cache layer (UPSERT keyed on purl; positive + negative cache share TTL). Schema (forward-only, Alembic 0004): license_fetch_cache purl TEXT PK, spdx_id TEXT NULL, reference_url TEXT NULL, source TEXT NOT NULL, is_negative BOOL NOT NULL, fetched_at TIMESTAMPTZ. scan_source._persist_component_licenses now falls back to the fetcher when cdxgen returned zero SPDX ids for the component; cdxgen wins when it has metadata so the cost-saver path is preserved. Tests (104 new): unit/integrations/license_fetcher/ test_base.py — normalize_spdx_id (10 happy + 7 reject), request_with_retry (8 cases — 2xx/404/5xx-with- retry/429/persistent-5xx/4xx-no-retry/transport- error/no-retry-on-403). test_maven.py — POM parsing, 404, no-licences, unmapped free-text, 5xx retry, foreign purl prefix. test_pypi.py — Trove classifier preferred, free-text fallback, 404, invalid JSON, URL shape pin. test_crates.py — SPDX expression direct, compound rejected, missing field, 404, 429 retry. test_pkggo.py — primary anchor, fallback anchor, missing block, 404, fallback-to-normalized free-text. test_dispatch.py — prefix routing, cache miss/hit positive/hit negative/expired/unsupported-ecosystem/empty-purl, TTL env override. integration/scan/test_license_fetcher_integration.py — real license_fetch_cache UPSERT, TTL refresh, "cdxgen has license → fetcher not called" pin. Coverage: 88% lines on changed integrations/license_fetcher/* code. ruff + mypy clean. 697 unit + 141 integration tests green. Refs: docs/sessions/_next-session-prompt-chore-pr5.md Part B docs/sessions/2026-05-07-uat-multi-ecosystem-matrix.md §4.1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(integrations): cdxgen Gradle 8 init.gradle compatibility The 2026-05-07 UAT found pilot-java-gradle returned 0 components: cdxgen v11.x's bundled init.gradle calls allprojects { ... } at root scope, which Gradle 8 removed (the build aborts with "Could not get unknown property 'allprojects' for root project" before cdxgen can enumerate configurations). Fix: when run_cdxgen detects a Gradle build root (build.gradle{,.kts} / settings.gradle{,.kts}) it writes a small Gradle 8 compat init script into the cdxgen output dir and exposes it via CDXGEN_GRADLE_ARGS= "--init-script <path>". cdxgen passes the args through to gradle, the shim runs first, re-binds the missing allprojects closure to rootProject.allprojects, and cdxgen's component enumeration completes normally. The shim is a no-op when an operator already set CDXGEN_GRADLE_ARGS in the worker env (explicit override wins). Strategy choice (per the prompt): Option 1 (--no-init-gradle CLI flag) — not available across all cdxgen 11.x point releases shipped in the worker image. Skipped to avoid version-coupling. Option 2 (env-var-driven init script) — chosen. Works on every cdxgen 11.x build that honours CDXGEN_GRADLE_ARGS, which is the canonical knob in the cdxgen 11 line. Trivially reverted (delete the helper) when cdxgen 12 lands with native Gradle 8 support. Option 3 (separate cdxgen-java11 image) — heavyweight, deferred. Tests (15 cases): test_cdxgen_gradle_compat.py - _is_gradle_project: each marker file (build.gradle / .kts / settings.gradle / .kts) flips the detector True; package.json / empty dir → False. - _write_gradle_compat_init: file exists, lives under output_dir, content matches _GRADLE8_COMPAT_INIT, parent dir is created lazily. - _build_cdxgen_env: Gradle root → CDXGEN_GRADLE_ARGS injected and script written; non-Gradle root → no env var, no script; operator-supplied CDXGEN_GRADLE_ARGS wins (no clobber). Pilot validation is out of scope for CI — actual scan-result re-check (component count ≥ 30 on pilot-java-gradle) lands in the next UAT round. Refs: docs/sessions/_next-session-prompt-chore-pr5.md Part C docs/sessions/2026-05-07-uat-multi-ecosystem-matrix.md §4.1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(scan,settings): adopt security-reviewer L1 + I1 follow-ups Two findings from the chore PR #5 Producer-Reviewer pass that are small enough to land inline: - L1 (Low) — `_PREP_ENV_ALLOWLIST` was dropping corporate CA / proxy hints (SSL_CERT_FILE / SSL_CERT_DIR / REQUESTS_CA_BUNDLE / NODE_EXTRA_CA_CERTS / HTTP[S]_PROXY / NO_PROXY in both upper- and lower-case). On-prem evaluators behind a TLS-intercepting proxy saw `go mod tidy` / `dotnet restore` fail silently with x509 errors, surfacing only in `prep_failed` log lines. These vars are operator-chosen — a hostile clone cannot influence them — so they are safe to forward. - I1 (Info) — `Bash(git push *)` allow rule (commit 2965283) accepts destructive push variants. Per memory `feedback_push_pr_authorized` the user authorized push/PR-create direct execution but reserved force-push for explicit approval. Adding deny rules for `--force` / `-f` / `--force-with-lease` / `--delete` / `--mirror` to align settings.json with documented policy. Two reviewer Mediums (M1 cdxgen/ORT scope gap, M2 Maven reference_url phishing surface) are flagged as immediate follow-up chore PRs in the session handoff — neither is a regression of the in-scope work and both deserve their own focused Producer-Reviewer pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(sessions): chore PR #5 security follow-up + license fetcher handoff 11 commits, 35 files (+4199 / -275 LoC), security-reviewer PASS with conditions (0 Critical / 0 High / 2 Medium / 4 Low / 3 Info). L1 + I1 absorbed inline (4d2c619). M1 (cdxgen/ORT env scope gap) and M2 (Maven reference_url phishing) deferred to focused follow-up PRs with their own Producer-Reviewer pass. Three options for the next session: A. Phase 4 entry — notification system PR #14 (model + API + RBAC). B. Medium #1 v2 + L4 — cdxgen/ORT subprocess env scrub + license-fetcher follow_redirects=False. C. Medium #2 + UAT re-validation — drop fetcher-derived reference_url + re-run 5-pilot ecosystem matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(tests): apply ruff import-order fix to license_fetcher conftest Auto-fix from `ruff check --fix` that was generated locally during the chore PR #5 verification but missed inclusion in the original 9a00b86 commit. CI lint job caught it. Pure import reorder, no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tasks
4 tasks
haksungjang
added a commit
that referenced
this pull request
May 11, 2026
Marathon bundle 8 (L1). Splits PostgreSQL into a migration-owning role
(``trustedoss`` — DDL) and a runtime DML-only role (``trustedoss_app``)
so a runtime RCE cannot DROP the audit_logs immutable trigger and
mutate audit_logs. The audit-log append-only contract is now enforced
at three layers:
1. Trigger (migration 0012) — row-level DDL guard.
2. REVOKE UPDATE/DELETE/TRUNCATE on audit_logs (migration 0014) —
even if the trigger is dropped, the runtime cannot mutate.
3. Trigger ownership barrier — the runtime can't drop the trigger
because it doesn't own the table.
Backend:
- alembic/versions/0014_app_role_grants.py — conditional GRANT block.
No-op when trustedoss_app role doesn't exist (legacy / dev / single-
role deployments). Also REVOKEs CREATE on schema public for
defence-in-depth on PG <15.
- alembic/env.py — uses database_url_owner_sync() so DDL has the
necessary privileges. Falls back to DATABASE_URL when
DATABASE_URL_OWNER unset.
- core/config.py — splits database_url() (runtime, reads
DATABASE_URL_APP first) from database_url_owner() (migrations).
- main.py lifespan — logs the connected role at boot. In APP_ENV=prod
with DATABASE_URL_APP set, refuses to start when the runtime ended
up connecting as a non-app role (security-reviewer Medium #2 —
fail loud on env-wiring drift instead of silent regression).
Infra:
- docker-compose.yml — backend / worker / beat ONLY see the runtime
DSN (DATABASE_URL_APP fallback to DATABASE_URL). Owner DSN never
flows into long-running containers (security-reviewer Critical fix).
- scripts/install.sh — alembic exec uses an explicit
``-e DATABASE_URL=$DATABASE_URL_OWNER`` override so the DDL run
uses the owner role exactly once. Generates app password +
POSTGRES_APP_PASSWORD env.
- scripts/postgres-init.sh — first-boot psql script that creates the
trustedoss_app role from POSTGRES_APP_PASSWORD. Uses psql
``--variable`` interpolation (security-reviewer Medium M4 —
password injection safety; single-quote / backslash in password no
longer breaks SQL parse).
- .env.example — documents DATABASE_URL_OWNER / DATABASE_URL_APP /
POSTGRES_APP_PASSWORD with the legacy single-role fallback noted.
Tests added (7 new):
- tests/integration/test_role_separation.py — fixture creates a
randomized trustedoss_app_test_<hex> role, applies the same grants
as migration 0014, then verifies:
* INSERT on audit_logs allowed.
* UPDATE / DELETE / TRUNCATE on audit_logs denied.
* DROP TRIGGER audit_logs_immutable_trigger denied (ownership).
* Normal-table DML (projects) still allowed.
* CREATE TABLE denied on PG 15+.
- Fixture returns (conn, role_name) tuple explicitly (security-reviewer
High #2: avoids reaching into asyncpg's _params internal which a
future driver bump could rename).
- test_alembic_upgrade head bumped to 0014.
Verification:
- pytest 12/12 green (7 role_separation + 5 alembic_upgrade).
- Manual end-to-end: created trustedoss_app + applied GRANTs →
audit_logs has INSERT=t / UPDATE=f / DELETE=f / TRUNCATE=f, while
projects keeps full DML.
- ruff + mypy clean. shellcheck clean (no warnings).
security-reviewer Producer-Reviewer = CHANGES REQUESTED, fixes
applied:
- Critical: docker-compose env wiring (compose anchor + install.sh
alembic exec) → fixed.
- High #2: test fixture asyncpg internal → fixed (tuple return).
- Medium M4: postgres-init.sh password injection → fixed (psql
--variable).
- Medium #2: silent env-mismatch in prod → fixed (lifespan refuses).
- Low #1: audit_logs ACL clean shape → fixed (REVOKE ALL + GRANT
SELECT, INSERT).
- High #3: deny-by-default for FUTURE tables → applied (ALTER
DEFAULT PRIVILEGES grants only SELECT, INSERT — future audit-style
tables auto-inherit append-only posture).
Backlog (deferred to follow-up chore — security-reviewer findings):
- Existing-deployment upgrade path (upgrade.sh hook + admin health
card + runbook) — Medium #5 below.
- Strengthen test for default-privileges idempotency across grantor
rotation — High #3 partial.
- Migration that creates the literal trustedoss_app role and runs
0014 against it (currently the integration test mirrors the GRANTs
rather than exercising 0014 directly) — Low #2.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 22, 2026
haksungjang
added a commit
that referenced
this pull request
May 22, 2026
Addresses the residual security-reviewer findings on the scan-pipeline-gap surface (Medium #1/#2 were already fixed in #107/#109). - Low #3: report_service._safe_href rejects URLs with embedded control chars (CR/LF/TAB) before scheme parsing — closes a latent CRLF-injection vector if the helper is reused in a header context. - Low #4: obligation list + NOTICE endpoints now existence-hide cross-team reads as 404 (were 403), uniform with the detail / report / source-tree / SBOM endpoints — a non-member can no longer enumerate project existence. - Low #5: the inline HTML NOTICE response carries a restrictive Content-Security-Policy (default-src 'none'; style-src 'unsafe-inline') so a future escaping regression cannot execute against the API origin. Tests: _safe_href CR/LF/NUL cases; obligation list/notice cross-team 404 (unit + integration); NOTICE CSP present for html / absent for text. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 24, 2026
…-b1) (#157) * feat(backend): GitHub App credential storage + token-minting foundation (v2.2-b1) Lay down the credential STORAGE + token-minting foundation for the D1 GitHub App flow (b2/b3 build PR creation on top — not implemented here). - core/crypto.py: reversible Fernet encryption (encrypt_secret/decrypt_secret) with runtime key resolution — GITHUB_APP_ENCRYPTION_KEY, else deterministically derived from SECRET_KEY with a structured WARNING. Pin cryptography explicitly. - models/github_app.py: github_app_credentials (team-scoped, Fernet-encrypted PEM private key + webhook secret, soft-delete) and github_app_installations (per-project opt-in). Migration 0019 (expand-only, forward-only). - core/audit.py: mask private_key_encrypted / webhook_secret_encrypted. - schemas/github_app.py: create/out/list + installation shapes; key plaintext accepted only inbound, never returned; adversarial PEM/app_id/repo validation. - services/github_app_service.py: register/list/get/revoke + installation link/list/unlink (cross-team opt-in blocked) + mint_installation_token (RS256 App JWT, iat clock-skew clamp, exp ≤10min, injectable httpx client so tests mock GitHub). Never logs the PEM, JWT, or installation token. - api/v1/github_app.py: /v1/github-app-credentials CRUD + installations, JWT auth, team_admin/member RBAC in the service, RFC 7807 errors, no key leak. - Tests: crypto (both key paths, rotation), schema adversarial, service (encrypt-at-rest, masked audit, RBAC, mint w/ real RSA + mocked GitHub), endpoint RBAC/audit/RFC7807. New-code coverage 94%. - Docs: admin-guide/github-app (EN + KO); .env.example entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(github-app): security-reviewer follow-ups — redirect-disable, GITHUB_API_URL allow-list, installation_id revalidation, prod fail-closed key, FK-vs-dup status Medium #1: mint_installation_token POST now passes follow_redirects=False so the App JWT can never follow a 3xx to an attacker host. Medium #2: core.config.github_api_url() validates the value at call time (rule #11) — in prod it requires https:// and rejects loopback, link-local (incl. 169.254.169.254 metadata), and RFC-1918 hosts via urlparse + ipaddress. Non-prod stays permissive for tests / local GHES. New GitHubAppConfigError. Medium #3: mint_installation_token re-validates installation_id against ^[0-9]{1,32}$ at the trust boundary before any DB load or URL interpolation; non-numeric / CRLF / path-traversal values raise before any HTTP attempt. Low #1: core.crypto fails closed in prod when GITHUB_APP_ENCRYPTION_KEY is unset/blank (raises SecretEncryptionError instead of deriving from SECRET_KEY). Derive-from-secret remains the non-prod fallback with its WARNING. Low #4: register_credential distinguishes the FK violation (non-existent team_id → 404) from the unique violation (genuine duplicate → 409) via SQLSTATE 23503 with a message-substring fallback. Low #2/#3: docs/comments — .env.example documents the GITHUB_API_URL prod policy and that encryption-key rotation requires re-registration (MultiFernet rolling rotation tracked as follow-up); App-JWT builder notes the NTP-sync clock assumption. Tests: new test_github_api_url_config.py (prod allow-list + non-prod permissive); crypto prod fail-closed cases; service tests for bad installation_id (never reaches HTTP), follow_redirects=False, no-3xx-follow, and FK-vs-duplicate status. Endpoint surface (OpenAPI) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 25, 2026
haksungjang
added a commit
that referenced
this pull request
May 26, 2026
Sweep the user and admin guides (EN + KO) so the prose matches what shipped to main between W1 and the current ops-triage batch. The PR is doc-only — no apps/* changes. User guide - new user-guide/dashboard.md (EN + KO) — covers the post-login `/` landing page introduced by W1; sidebar entry as position 0. - projects.md: project-list meta row (`scan_count`/`release_count`/ `last_scan_at` from #30), detail-page tab table refreshed to the post-v2.3 layout (11 tabs incl. Releases/Reports/Source moved between Reports and Remediation per P2 #3), new Project info card section (P2 #1), new "Releases tab" section incl. ?scan= snapshot pin behaviour, new "Reports tab" section (#32 — generate cards + export history table), risk-score rewritten as the two-axis model (#34, Security + License); KO mirror in parallel. - scans.md: "only one scan at a time per project" callout + troubleshooting entry (P1 #10), per-stage log panel description (P2 #8b), Project-name column note on the global queue (P1 #5), Finalizing-spinner regression troubleshooting (P1 #11); KO mirror. - vulnerabilities.md: "Vulnerability data unavailable" banner section (#35), bulk-transition section with the `:bulk-transition` endpoint contract (#33b); KO mirror. - components-and-licenses.md: Components table columns updated to include Type (Direct/Transitive) and Usage (Required/Optional) with the matching toolbar filters and drawer meta (W2 #31); KO mirror. - approvals.md: list-row columns updated to the resolved component / project names + click-through links (P1 #6), batched IN(...) lookups note; KO mirror. Admin guide - users-and-teams.md: new "Recovering a deactivated super-admin" section that documents the ensure-active recovery path in scripts/create_super_admin.py (#14); KO mirror. Docs site - sidebars.ts: added user-guide/dashboard ahead of user-guide/projects. - KO projects.md: explicit anchor `{#build-gate-verdict-overview-tab}` added so the dashboard's KO link to the build-gate section resolves (the auto-derived Korean slug was the previous breakage point). - KO vulnerabilities.md: fixed pre-existing stale anchor target `#epss--악용-가능성` → `#epss--악용-확률`. Stale assets flagged (not removed) - Walkthrough video on projects.md still captures the pre-post-v2.3 four-tab layout. Marked in a `<!-- -->` comment; refresh after merge. - Projects-list and global-scans-queue screenshots predate the scan-meta row and Project-name column. Marked in `<!-- -->` comments; refresh after merge. Not covered (deferred until on main) - P2 #4 (Dashboard KPI deep links), P2 #3/#1/#2 (visible in feature branches but not yet merged to main). Verification - `npm run build` under docs-site exits 0 with no warnings on either locale. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 26, 2026
…s→Components / file input i18n (#3/#1/#2/#9) (#177) * feat(P2): UI consistency pass — #3 / #1 / #2 / #9 bundle Four small UI defects reported in 2026-05-26 운영 트리아지 grouped into one PR because they all touch the project detail surface and are best verified together by a walkthrough. #3 — Source tab order The Source (raw artifact) tab is the cognitive predecessor of Licenses (classified output of that artifact), so Source now sits to the left of Licenses. Tab key / route is unchanged — deep links and tests keep working. #1 — Overview "Project info" card Description, repository URL (with external-link affordance), default branch, and visibility are now surfaced at the top of the Overview tab. Data comes from the already-fetched ProjectPublic (parent threads it through OverviewTab), so this is a zero-extra-fetch addition. EN + KO translations added under `overview.info_card.*`. #2 — Releases row → Components tab Clicking a row in the Releases table used to pin the snapshot and land on Overview; "a release IS a component snapshot," so the natural landing surface is Components. The header ReleaseSwitcher keeps the original Overview target — that affordance is about a quick header- level snapshot pin, not a deep-dive. Implemented by splitting `handleViewSnapshot` into a parameterised `pinSnapshotAndGoToTab` core with two thin wrappers. #9 — File input localisation `<input type="file">` renders its own button label in the OS locale (macOS Korean = "파일 선택") regardless of app i18n. Both the source- upload + folder-pick inputs in SourceSelectDialog and the VEX import input in VexImportDialog now hide the native control (sr-only) and expose a translated Button + filename strip we own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(P2 #9): add missing i18n keys for file/folder picker labels CI i18n drift check failed: the new translated Button labels in SourceSelectDialog + VexImportDialog referenced keys that didn't exist in locale json: - en/ko scans.json: source.upload.{choose_file,no_file_chosen} source.folder.{choose_folder,no_folder_chosen,files_count} - en/ko project_detail.json: vulnerabilities.vex.{choose_file,no_file_chosen} defaultValue kept the UI rendering during dev (i18next falls through) but i18n:check scans every t() call site and requires both EN + KO entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
haksungjang
added a commit
that referenced
this pull request
May 26, 2026
Add a tool-log streaming side-channel on the existing /ws/scans/<id> WebSocket so the scan drawer renders cdxgen / scancode stdout + stderr lines as they arrive, instead of only progress events. Backend: - New `integrations/_line_streamer.py` wraps `subprocess.Popen` with two drain threads that decode each line and forward to an optional callback. Fallback to `subprocess.run` when no callback is set, preserving the prior contract for callers that don't need streaming. - cdxgen / scancode adapters accept a `line_callback` kwarg and route to the new helper. Full captured stdout/stderr is preserved on the returned CompletedProcess so existing error inspection still works. - `tasks/_progress.publish_log(scan_id, stage, stream, line)` publishes log frames on the same Redis channel as progress frames. Per-scan line budget (SCAN_LOG_MAX_LINES_PER_SCAN, default 5000) + per-line truncation (SCAN_LOG_LINE_MAX_LEN, default 2000) cap broker volume + memory. - Progress frames now carry `type: "progress"` discriminator; log frames carry `type: "log"`. Older clients that ignore `type` are unaffected (missing-field interpretation is "progress"). - `scan_source.py` passes `_make_line_callback(scan_uuid, stage=...)` to cdxgen + scancode call sites. Per-scan log counter resets at task entry so a Celery re-execution starts with a fresh budget. Frontend: - `useScanWebSocket` discriminates frames by `type` and surfaces a new `logMessages: ScanLogMessage[]` (5000-cap ring buffer) alongside the existing `messages` progress buffer. Buffer clears on scanId change. - `ScanProgress` log panel renders tool stdout / stderr color-coded by stage (cdxgen=blue, scancode=amber) with stderr tinted critical-red and an `err` badge. Falls back to the prior per-step trace when no tool lines have arrived (older worker / mock backend). - EN/KO `progress.tool_log_toggle` keys added. Tests: - 9 unit tests for `_line_streamer` (no-callback fallback, in-order stdout/stderr delivery, callback-exception isolation, timeout with partial output, non-zero exit, blank-line drop, invalid utf-8). - 10 unit tests for `publish_log` + the discriminator on `publish_progress` (channel, schema, stream normalisation, line truncation, per-scan budget, kill switch, Redis-error isolation, scan isolation). - 3 unit tests for `scan_source._make_line_callback` (stage curry, publisher-error isolation, multi-stage independence). - 2 unit tests for `build_log_frame`. - 1 integration test (`test_ws_forwards_published_tool_log_line`) and `type: "progress"` assertion added to the existing progress forwarding test. - 5 useScanWebSocket tests for log-frame parsing + dispatch. - 4 ScanProgress tests for the new tool-log panel rendering. CLAUDE.md compliance: env reads via getters at call time (rule #11); PostgreSQL is unaffected (the publisher cap is in-process); Celery async pipeline preserved (rule #3); DT breaker untouched (rule #4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 27, 2026
#185) * feat(P2 #8c): stream cdxgen + scancode stdout to scan drawer log panel Add a tool-log streaming side-channel on the existing /ws/scans/<id> WebSocket so the scan drawer renders cdxgen / scancode stdout + stderr lines as they arrive, instead of only progress events. Backend: - New `integrations/_line_streamer.py` wraps `subprocess.Popen` with two drain threads that decode each line and forward to an optional callback. Fallback to `subprocess.run` when no callback is set, preserving the prior contract for callers that don't need streaming. - cdxgen / scancode adapters accept a `line_callback` kwarg and route to the new helper. Full captured stdout/stderr is preserved on the returned CompletedProcess so existing error inspection still works. - `tasks/_progress.publish_log(scan_id, stage, stream, line)` publishes log frames on the same Redis channel as progress frames. Per-scan line budget (SCAN_LOG_MAX_LINES_PER_SCAN, default 5000) + per-line truncation (SCAN_LOG_LINE_MAX_LEN, default 2000) cap broker volume + memory. - Progress frames now carry `type: "progress"` discriminator; log frames carry `type: "log"`. Older clients that ignore `type` are unaffected (missing-field interpretation is "progress"). - `scan_source.py` passes `_make_line_callback(scan_uuid, stage=...)` to cdxgen + scancode call sites. Per-scan log counter resets at task entry so a Celery re-execution starts with a fresh budget. Frontend: - `useScanWebSocket` discriminates frames by `type` and surfaces a new `logMessages: ScanLogMessage[]` (5000-cap ring buffer) alongside the existing `messages` progress buffer. Buffer clears on scanId change. - `ScanProgress` log panel renders tool stdout / stderr color-coded by stage (cdxgen=blue, scancode=amber) with stderr tinted critical-red and an `err` badge. Falls back to the prior per-step trace when no tool lines have arrived (older worker / mock backend). - EN/KO `progress.tool_log_toggle` keys added. Tests: - 9 unit tests for `_line_streamer` (no-callback fallback, in-order stdout/stderr delivery, callback-exception isolation, timeout with partial output, non-zero exit, blank-line drop, invalid utf-8). - 10 unit tests for `publish_log` + the discriminator on `publish_progress` (channel, schema, stream normalisation, line truncation, per-scan budget, kill switch, Redis-error isolation, scan isolation). - 3 unit tests for `scan_source._make_line_callback` (stage curry, publisher-error isolation, multi-stage independence). - 2 unit tests for `build_log_frame`. - 1 integration test (`test_ws_forwards_published_tool_log_line`) and `type: "progress"` assertion added to the existing progress forwarding test. - 5 useScanWebSocket tests for log-frame parsing + dispatch. - 4 ScanProgress tests for the new tool-log panel rendering. CLAUDE.md compliance: env reads via getters at call time (rule #11); PostgreSQL is unaffected (the publisher cap is in-process); Celery async pipeline preserved (rule #3); DT breaker untouched (rule #4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(P2 #8c): restore line_callback / _make_line_callback after main merge The prior merge commit (94313c0) accidentally reset scan_source.py to the main version, dropping the four PR #185 changes: - import publish_log + reset_log_counter - reset_log_counter(scan_uuid) at task entry - line_callback=_make_line_callback(stage="cdxgen") on cdxgen call - line_callback=_make_line_callback(stage="scancode") on scancode call - _make_line_callback helper This commit re-applies all five changes on top of the main merge so the P2 #8c tests (test_scan_source_log_callback) and the rest of the WS log streaming chain compile and run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(P2 #8c): fake_run_cdxgen accepts **kwargs for line_callback W6 integration tests (test_scan_source_pipeline_mock / test_scan_source_dependency_graph) define _fake_run_cdxgen with a strict (source_dir, output_dir) signature. PR #185 added a line_callback kwarg to the real cdxgen_adapter.run_cdxgen signature, which makes scan_source.py pass that kwarg into the patched fake — TypeError: unexpected keyword argument 'line_callback'. Accept **_kwargs in both fakes so the W6 tests survive the P2 #8c signature extension without coupling the fixtures to the streaming callback contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(P2 #8c): more fake cdxgen/scancode patches accept line_callback The earlier 012f080 only fixed the two named _fake_run_cdxgen functions; this round-trip caught four more fixtures that the same TypeError chain breaks: - test_scan_source_dependency_graph._skip_scancode (scancode adapter patch) needs **_kwargs for line_callback parity with cdxgen. - test_scan_source_trivy three lambda cdxgen patches (lines 281, 358, 414) each take a strict (source_dir, output_dir) signature; widen them with **_kwargs. The W6 scan_source pipeline now passes line_callback into both adapters on every dispatch, so every monkeypatch on those names must accept the kwarg even when the test does not exercise the streaming path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
Jul 2, 2026
…ines (#443) BomLens parity item #3 (Phase B). The obligation catalog has always declared license_text_inclusion_required, but the NOTICE we generate only linked to spdx.org — the product did not satisfy its own obligation. It now does. License texts: - services/license_texts/ vendors the full text for all 32 catalog licenses (21 from BomLens, Apache-2.0; 11 from the official SPDX license-list-data repo) with a loader that allowlists the id shape, resolves only against the real file-stem set, and caches by file snapshot. A catalog contract test locks catalog_spdx_ids() == bundled_spdx_ids(). - All three NOTICE formats append a 'License Texts' section: unique compound-expression operands (obligation-catalog splitter, so the section shares vocabulary with the obligation rows), spdx asc, verbatim text — html escaped in <pre>, markdown wrapped in a dynamic fence longer than any backtick run in the text. Unbundled ids render a pointer line (own 500 cap); bundled texts cap at 200 with the established omitted footer. Copyright: - Component lines now carry Copyright: <value> from the scanned SBOM, clamped to 500 chars IN SQL (func.left inside jsonb_agg — an authenticated hostile SBOM cannot materialize multi-hundred-MB aggregate rows), control-char sanitized, and escaped per format. Missing holders fall back to 'holders not captured in SBOM — see source (<registry url>)' — never blank. _purl_source_url ports the BomLens purl_src registry map (9 types, https only) and rejects '..'/'%' in the purl body (traversal-shaped links). Security: Producer-Reviewer gate — CHANGES REQUESTED (Medium: SQL-side clamp; Low: plaintext divider forging via scanned spdx_id; two Infos) then PASS on re-review. The ingest-side spdx_id sanitize root fix is tracked with the existing persist-NUL follow-up. Docs: SBOM user guide gains 'Copyright lines' / 'License texts' sections (EN/KO), obligations page cross-link, CHANGELOG entry. Parity tracker moves #3 to in-progress and records the third BomLens baseline review (522735a — G7 registry v2 sync follow-up registered).
haksungjang
added a commit
that referenced
this pull request
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 PR #10 — Project Detail (Overview + Components 탭).
docs/v2-execution-plan.md§3.4 표 3.1 / 3.2 / 3.3 산출.3개 신규 backend 엔드포인트 + ProjectDetailPage UI + 4개 탭 (Overview / Components 활성, Vulnerabilities / Licenses 는 PR #11/#12 placeholder) + 1만 행 가상 스크롤 드로어 + EN/KO 번역 + e2e 6 시나리오.
Backend (apps/backend/)
3 신규 엔드포인트, 모두
Depends(get_current_user)+ IDOR 가드 + RFC 7807 problem+json:GET /v1/projects/{id}/overview— risk score (0~100), severity_distribution, license_distribution, recent_scans (5). asyncio.gather 로 집계 + 스캔 이력 병렬 SELECT. p95 < 200ms 달성 (db-designer EXPLAIN 검증, 신규 마이그레이션 불필요).GET /v1/projects/{id}/components— 1만 행 페이지네이션 (limit ≤ 500). search ILIKE / severity multi-filter / license_category multi-filter / sort by name|severity|license × asc|desc. cross-team component existence 는 404 (hide-existence — 의도적).GET /v1/components/{id}— 드로어 페이로드 (vulnerabilities + raw_data + license).중요 schema 결정: 실제 DB 모델은
severity_max/license_category컬럼이 components 또는 scan_components 에 없음.project → latest_scan_id → scan → scan_components → component_versions → components → vulnerability_findings/license_findings join으로 query-time aggregation. 현 1만 행 cap 에서 적합. 100k+ 행 도달 시 denormalized cache 또는 materialized view 검토 — Phase 3+ follow-up.37 신규 테스트 (7 pure-unit + 19 service-DB + 11 HTTP-integration).
ruff+mypy --strictclean.Frontend (apps/frontend/)
ProjectDetailPage— 4 탭 + breadcrumb + 헤더 RiskGauge.OverviewTab— Risk gauge (semicircular SVG) + severity / license stacked bars + recent-5 scans 테이블. recharts 의도적 미사용 (bundle 200kB + v1 XSS surface 회피).ComponentsTab— react-virtuosoTableVirtuoso1만 행 가상 스크롤 + 인라인 toolbar (search 디바운스 300ms / severity / license / sort) + URL search-params 동기화 (deep-linking) + 우측 슬라이드 Sheet 드로어.ComponentDrawer— vuln 리스트 + raw_data accordion (<pre>+JSON.stringifytext node,dangerouslySetInnerHTML미사용).@radix-ui/react-tabs미추가 — 동일 shape 라 미래 swap 무중단)./projects/:id추가, ProjectListPage row name →<Link>.41 신규 vitest 케이스. coverage 93.41% lines / 85.29% branches / 87.09% funcs (≥ 80% 게이트 통과).
i18n (EN + KO)
project_detailnamespace, 84 키 양쪽 완성. 도메인 용어는 기존common.risk(치명/높음/중간/낮음/정보) +projects.status(대기 중/실행 중/성공/실패) 와 정합.신규 도메인 용어 8개 —
docs/glossary.md정식 등재 후보:E2E
PortalPage하네스에 8 verb 추가 (6 사용자 요청 + 2 ergonomic). 모두 event-driven wait —page.waitForTimeout미사용.project_detail.spec.ts— 6 시나리오:시나리오 2 의 250 rows 는 CI 30s/test 예산 준수. seed CLI 는
--component-count 10000도 지원 (ad-hoc perf 테스트).seed 스크립트 확장:
--with-scan/--component-count/--component-prefix신규 플래그 (배치 100 rows, severity / license_category round-robin 분포).Security review (security-reviewer)
평결: PASS (블로커 0, Medium 1, Low 4, Info 3).
raw_dataredaction 누락 — cdxgen/DT 원본의 잠재적 시크릿 (npm_auth, git tokens) 가 same-team developer 에게 노출%/_미escape (UX 이슈, SQL 주입 아님 — bind parameter)--component-count상한 부재 (운영자 CLI 전용)Test plan
lint× 2 /typecheck× 2 /test× 2 /image-scan(hard-fail) /frontend-bundle-audit/e2e) 모두 success.image-scanhard-fail 통과 (PR chore: harden worker image + restore image-scan hard-fail #2 의.trivyignore정책 + npm 11.13 fix 가 본 PR 에서도 유효 — 본 PR 은 이미지 변경 없음).@project-detail태그). 시나리오 2 의 endReached 트리거 확인.Follow-up backlog (별도 PR)
raw_dataredaction 레이어 — security-reviewer Medium chore(deps): bump fastapi/starlette/multipart/pyasn1/jose for HIGH CVEs + restore image-scan hard-fail #1.docs/glossary.md등재 — i18n-specialist 가 식별한 신규 도메인 용어 8개.Refs:
docs/v2-execution-plan.md§3.4 +docs/sessions/2026-05-06-chore-worker-image-hardening.md(chore PR #2 핸드오프, 본 브랜치 첫 commit4e886d9로 동행).🤖 Generated with Claude Code