feat: phase 3 pr #13 — obligations tab + NOTICE generator#6
Merged
Conversation
…nerator)
Adds a fifth project-detail tab that surfaces the per-license obligation
catalog (attribution, source-disclosure, copyleft, etc.) and exposes a
NOTICE attribution download for the project's latest scan.
Backend (new):
- GET /v1/projects/{id}/obligations — kind/category multi-filter, search
(4 ILIKE columns escaped), sort + paging; returns items, per-kind
distribution (known kinds first, unknown alphabetical), total.
- GET /v1/projects/{id}/obligations/{obligation_id} — drawer detail,
project-scoped URL, existence-hide (404) on cross-team, presence check
against latest_scan_id.
- GET /v1/projects/{id}/notice — text/plain or text/markdown body with
optional Content-Disposition: attachment + sanitized filename token;
X-Notice-{Generated-At, License-Count, Obligation-Count} headers.
- read-only domain — no PATCH, no transition matrix, no audit_log writes.
- schema check confirmed Obligation has no status workflow column.
Frontend (new):
- ObligationsTab + ObligationsToolbar + ObligationDrawer mirroring the
PR #12 Licenses pattern. URL search-params (?obligation, ?kind,
?license_category) chosen to not collide with prior tabs' drawer keys.
- useNotice hook drives an imperative Blob download.
- http(s) URL scheme guard on obligation.link + license_reference_url.
Carry-overs from PR #11/#12:
- _escape_like ILIKE escape, cross-team authz logging emitted before
raise, structlog "obligation.service", category ENUM cast for filters,
query-time aggregation only.
Tests:
- backend: 49 unit cases (22 pure-helper pass + 27 DB-gated integration),
10 HTTP integration cases.
- frontend: 18 vitest cases (9 ObligationsTab + 9 ObligationDrawer
including a non-http(s) link XSS guard).
- e2e: 4 @obligations scenarios + 5 PortalPage harness verbs.
- seed CLI: --with-obligations flag attaches a 7-row catalog to the four
e2e seed-licenses (forbidden 2 / conditional 2 / allowed 2 / unknown 1).
i18n: 49 EN+KO keys (264/264 parity); 9 glossary entries.
DoD: ruff/mypy/typecheck/lint clean; vitest 241/241; 22 backend pure-unit
pass; security review PASS-with-follow-ups (1 Low + 3 Info on size caps and
defense-in-depth headers, tracked as a separate hardening PR).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erator Records the read-only catalog domain decision (Obligation has no status column → no mutation patterns), the project-scoped detail URL, the open kind enum strategy, and the security review's PASS-with-follow-ups verdict. Carries forward the size-cap defense-in-depth follow-up (PR #12 Info #1 + PR #13 Low #1) as the leading candidate for the next chore PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`API-MIT` was hardcoded in both `test_licenses_api.py` and `test_obligations_api.py`, causing a UniqueViolationError on the shared `licenses.spdx_id` constraint when the suite runs against a single Postgres instance. While here, applied the same uuid-suffix treatment to the three remaining fixed SPDX literals so the suite is robust to any future cross-module collision.
haksungjang
added a commit
that referenced
this pull request
May 7, 2026
…ects (chore PR #6) (#10) * docs(sessions): archive completed chore PR #5 next-session prompt Move docs/sessions/_next-session-prompt-chore-pr5.md to docs/sessions/archive/next-session-prompts/. The chore PR #5 prompt was fully consumed by PR #9 (merged in b23871b); keeping the file in docs/sessions/ root would clutter the working-tree status with a stale prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(integrations): promote subprocess env scrubbing to shared module Extract the prep-only ``_PREP_ENV_ALLOWLIST`` + ``_scrubbed_env`` helper from ``tasks/scan_source.py`` into a new ``integrations/_subprocess_env.py`` module that exposes three ecosystem-specific builders: * ``scrubbed_env_for_prep()`` — Ruby/Cargo/Go/.NET/Java prep * ``scrubbed_env_for_cdxgen()`` — Node/JVM/Python tooling for cdxgen, plus the ``npm_config_*`` / ``CDXGEN_*`` prefix bands * ``scrubbed_env_for_ort()`` — JVM tooling, plus the ``ORT_*`` prefix band All three share the same base allowlist (PATH/HOME/LANG/LC_ALL/TZ + corporate CA / proxy hints from chore PR #5 L1) and resolve ``os.environ`` at call time per CLAUDE.md rule #11. Prefix bands carry a credential heuristic: any allow-listed key whose name matches ``auth|token|password|passphrase|secret|credential| apikey|api_key|private(_)?key`` is dropped before forwarding. That prevents npm's ``_authToken`` / ``_password`` / ``_auth`` and ``ORT_*_TOKEN`` style keys from sneaking through the prefix gates. ``tasks.scan_source._scrubbed_env`` is preserved as an alias so the existing prep call site and its unit tests keep working unchanged. The cdxgen / ORT integrations adopt the new helpers in follow-up commits. Refs: security-reviewer Medium #1 v2 (chore PR #5 handoff §1.2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(integrations): scrub worker secrets from cdxgen subprocess env cdxgen runs as a Node binary under the worker. Hostile clones can load attacker-controlled ``package.json`` plugin hooks / ``cdxgen.config.json`` rules, and the existing ``env = dict(os.environ)`` inheritance hands them ``DT_API_KEY`` / ``SECRET_KEY`` / ``DATABASE_URL`` / ``*_WEBHOOK_URL`` for free — exfiltrable through plugin telemetry, crash reports, or DNS lookups in error paths (security-reviewer Medium #1 v2, chore PR #5 handoff §1.2). ``_build_cdxgen_env`` now starts from ``scrubbed_env_for_cdxgen()`` instead of ``os.environ``. The Gradle 8 compat-shim path from chore PR #5 Part C is preserved verbatim and a unit test is added pinning that operator-set ``CDXGEN_GRADLE_ARGS`` still wins. Three new tests in ``test_cdxgen_gradle_compat.py`` verify the scrubbed env strips worker secrets, forwards ``NODE_EXTRA_CA_CERTS`` / ``HTTPS_PROXY``, and keeps the existing operator-override semantics intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(integrations): scrub worker secrets from ORT subprocess env The ORT subprocess previously inherited the worker's full env (no ``env=`` argument to ``subprocess.run``). ORT is a JVM tool whose ``--rules-file`` and ``--ort-file`` inputs originate from the worker image / scan workspace, so the inputs themselves are not attacker- controlled — but a malicious ORT plugin or a JVM CVE in the rule evaluator would still see ``DT_API_KEY`` / ``SECRET_KEY`` / ``DATABASE_URL`` and could tunnel them through telemetry, crash reports, or DNS lookups in error paths (security-reviewer Medium #1 v2). ``run_ort`` now passes ``env=scrubbed_env_for_ort()``. The new env forwards JVM toolchain hints (``JAVA_HOME`` / ``JAVA_OPTS`` / ``GRADLE_USER_HOME`` / ``MAVEN_OPTS``) and the operator-override ``ORT_*`` band, while stripping worker secrets and credential-named ``ORT_*`` keys (``ORT_GITHUB_TOKEN`` / ``ORT_NEXUS_PASSWORD`` etc.). A new ``test_ort_env_scrub.py`` exercises the real-binary code path by monkeypatching ``shutil.which`` + ``subprocess.run`` and pinning two regressions: secrets are stripped, and credential-named ``ORT_*`` keys are dropped even though the prefix band is allow-listed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(license-fetcher): disable redirect following on registry clients All four ecosystem fetchers (Maven Central, PyPI, crates.io, pkg.go.dev) now build their default ``httpx.Client`` with ``follow_redirects=False`` (security-reviewer L4, chore PR #5 handoff §1.2). Each registry serves the licence metadata from a single authoritative origin, so a 3xx response is never a legitimate signal — a phishing host inserted via MITM, a misconfigured mirror, or an attacker-poisoned DNS response would otherwise be silently chased. ``base.request_with_retry`` grows an explicit 3xx branch that emits ``license_fetch_unexpected_redirect`` (warning-level, includes the truncated ``Location`` header) and returns ``None`` so the caller registers a negative cache entry. The attacker host is never contacted. Each fetcher gains two unit tests: * a behaviour pin — a 3xx response with a ``Location: attacker.example`` header returns ``None`` and the second URL is never requested; * a contract pin — the production-default ``_client(...)`` factory has ``follow_redirects is False``. Test injections are unaffected: callers can still pass an ``httpx.Client(follow_redirects=True)`` for live integration tests that legitimately need to chase redirects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(integrations): widen credential deny heuristic with session/bearer/cookie Add ``"session"`` / ``"bearer"`` / ``"cookie"`` to ``_CREDENTIAL_DENY_SUBSTRINGS`` so the prefix-band forwarders drop ``*_SESSION`` / ``*_SESSIONID`` / ``*_BEARER`` / ``*_COOKIE`` env variables that cdxgen / ORT plugins occasionally accept for registry login. Bare ``"key"`` is intentionally not added: it would match ``npm_package_keywords`` (npm script context, benign). The existing ``"private_key"`` / ``"privatekey"`` / ``"apikey"`` / ``"api_key"`` substrings already cover the real threat shapes for crypto material. Refs: security-reviewer L1 (chore PR #6 review). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
haksungjang
added a commit
that referenced
this pull request
May 7, 2026
Land the session housekeeping for the chore PR #6 + #7 work (merged in 1d41a94 + 53be9ba): - 2026-05-07-chore-pr6-cdxgen-ort-env-scrub.md - 2026-05-07-chore-pr7-maven-url-uat-revalidation.md - _next-session-prompt-phase4-pr14.md (next session start prompt) Archive the now-completed _next-session-prompt-chore-pr6-pr7.md under docs/sessions/archive/next-session-prompts/. The chore PR #6 + #7 work landed two security-reviewer Mediums (M1 v2 cdxgen/ORT subprocess env scrub, M2 reference_url phishing) plus L1/L4 follow-ups, surfaced and fixed three real chore PR #5 fetcher gaps (OR/WITH compounds, Maven aliases, pkg.go.dev 2026 HTML), and discovered + patched a chore-PR-#5-latent High recursion DoS + Medium charset flaw in normalize_spdx_id during the security review of chore PR #7. Phase 4 entry is now safe — Medium-or-higher backlog at 0. 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
CI typecheck (frontend) flagged 9 test fixtures that construct ScanPublic / ApprovalOut literals without the freshly-added display fields. Local tsc passed only because we ran it from a stale incremental cache that didn't include the tests directory. The fields are semantically optional anyway — list endpoints populate them, but single-row endpoints and test fixtures don't need to — so declare them with `?:` instead of `T | null`. UI call sites already use truthy guards (`item.component_name ? ... : fallback`) so the runtime behaviour is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 26, 2026
…ts (#174) * feat(P1 #5/#6): surface project/component names in Scans + Approvals lists The Scans queue and Approvals queue used to render the first 8 chars of a raw UUID in their Project / Component columns because the list endpoints shipped only the foreign-key columns (`ScanPublic.project_id`, `ApprovalOut.component_id`/`project_id`). Users in real triage couldn't tell rows apart and couldn't click through. BE — Scans (#5): - ScanPublic gains optional `project_name` / `project_slug`. - `list_scans_for_actor` and `list_scans_for_project` now `selectinload(Scan.project)` so the relationship is batch-loaded in one IN(...) lookup; no per-row round-trip. - `ScanPublic.from_scan(scan)` factory splices the two fields off the eagerly-loaded relationship; single-row endpoints keep `model_validate(scan)` and fall back to `null` (callers don't need them yet). BE — Approvals (#6): - ApprovalOut gains optional `component_name`, `component_purl`, `project_name`, `project_slug`. - ComponentApproval intentionally does NOT carry ORM relationships across the auth / scan domains (see model docstring), so the service resolves the display fields with two batched IN(...) lookups against components and projects and returns each row inside a new `ApprovalListRow` dataclass envelope. `ApprovalOut.from_list_row(row)` splices them onto the model-validated approval row. FE: - ScansPage Project column links to `/projects/{id}` with the project name when present, falls back to the legacy 8-char UUID otherwise. - ApprovalsPage Component column shows `component_name` + the purl in a monospace sub-line; Project column links to `/projects/{id}`. Both fall back to the UUID prefix when the BE field is null. The Project link stops click propagation so it doesn't also trip the row's drawer-open handler. - TypeScript interfaces updated in `projectsApi.ts` / `approvalsApi.ts`. Tests: - New integration test `test_list_includes_project_name_and_slug` covers the Scans list response shape. - New unit test `test_list_returns_component_and_project_display_fields` covers the ApprovalListRow envelope. - Existing list_approvals tests updated to read fields via `.approval.*` through the new envelope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(P1 #5/#6): make new display fields optional to keep tests building CI typecheck (frontend) flagged 9 test fixtures that construct ScanPublic / ApprovalOut literals without the freshly-added display fields. Local tsc passed only because we ran it from a stale incremental cache that didn't include the tests directory. The fields are semantically optional anyway — list endpoints populate them, but single-row endpoints and test fixtures don't need to — so declare them with `?:` instead of `T | null`. UI call sites already use truthy guards (`item.component_name ? ... : fallback`) so the runtime behaviour is 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>
4 tasks
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
Jul 3, 2026
…(Phase F) (#452) The scan-compare (release diff) feature landed with #28 (project_diff_service + ComparePage + Releases-tab entry) but had no end-to-end coverage. Close that gap. Seed: new `--scan-count {1|2}` on seed_e2e_user.py. `--scan-count 2` seeds a second succeeded scan on the first project whose SCA posture differs from the first by an exact, deterministic delta — 1 added / 1 removed / 1 version-changed component and 1 introduced / 1 resolved finding (the delta components are suffix-unique so they never collide). Requires --component-count >= 4; implies --with-scan. The newer scan id is returned as `second_scan_id`; the base stays `scan_ids[0]`. `--scan-count 1` (default) is byte-for-byte the old behavior. Verified against a throwaway Postgres 17: diff_release_snapshots reports exactly 1/1/1 components and 1/1 vulnerabilities. Harness: seed.ts gains `scanCount` + `second_scan_id`; PortalPage gains `selectReleasesTab()`. E2E: compare.spec.ts (@compare) — S1 single release keeps the Compare button disabled; S2 two releases enable Compare and the diff renders the injected change sets (added/removed/changed = 1, introduced/resolved >= 1) with base/target pinned to the two seeded scans; S3 the swap control reverses base/target. Each bootstrap uses a test-unique component prefix so the globally-unique component purl never collides across the file's seeds on a persistent dev DB. Docs: user-guide Releases/Compare paragraph (EN/KO) expanded to describe the full diff (changed components, introduced/resolved vulns, risk/gate deltas, swap, shareable ?base=&target= URL). Parity #6 moves to in-progress.
haksungjang
added a commit
that referenced
this pull request
Jul 3, 2026
Fulfil the CLAUDE.md "Excel / PDF reports" commitment, which previously shipped
PDF only. The project vulnerability report is now downloadable as an .xlsx
workbook alongside the PDF.
Backend:
- `services/report_xlsx_service.build_report_xlsx` — a PURE function (like
build_report_html) that turns the SAME read-service dict shapes the PDF path
uses into an openpyxl workbook (bytes), three sheets: Overview (risk score,
severity + license distribution), Components, Vulnerabilities (CVE, CVSS,
EPSS, KEV + due date, affected component). Document metadata is pinned to
`generated_at` so identical inputs produce identical bytes.
- Formula-injection defence (CWE-1236 / OWASP): every string cell starting with
`= + - @` (or a leading tab/CR) is prefixed with `'`, mirroring
admin_audit_service._csv_cell. Component names / summaries come from scanned
third-party metadata, so a hostile package can't ship a live formula.
- `GET /projects/{id}/vulnerability-report.xlsx` — mirrors the PDF endpoint
(IDOR / existence-hide 404, team gate, disconnect guard, run_in_threadpool),
records a `vuln_xlsx` export-history row.
- `report_type_enum += vuln_xlsx` (migration 0037; REPORT_TYPE_VALUES updated).
`format` is a free VARCHAR, so `fmt="xlsx"` needs no migration.
- openpyxl==3.1.5 pinned (pure-Python, no native deps; latest stable, no CVEs).
Frontend:
- `fetchVulnerabilityReportXlsx` + `useVulnReportXlsx` (shares the PDF hook's
download logic) + an "Download Excel" button on the Reports-tab vulnerability
card. EN/KO strings.
Tests:
- Unit: workbook shape, sheet headers, native-typed number/bool cells, KEV date
as ISO, determinism, and the injection defence (=cmd / +SUM / @ neutralised).
- Integration: xlsx happy-path (content-type + PK magic + .xlsx disposition),
401 / 404-existence-hide / super-admin bypass, and one `vuln_xlsx` history row.
None gated on weasyprint — openpyxl always available.
- E2E: reports.spec S2 clicks Excel and asserts a vulnerability-report-*.xlsx
download. OpenAPI golden updated for the new path.
Docs: vulnerabilities guide "Download a report (PDF or Excel)" (EN/KO) + CHANGELOG.
Parity #8 → in-progress; #6 (Phase F) → closed.
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
/v1/projects/{id}/notice) that produces text/plain or text/markdown bodies with sanitized Content-Disposition filenames; the toolbar exposes a one-click download.Obligationhas no status column → read-only domain (no PATCH, no transitions, no audit_log writes), mirroring PR docs: chore PR #6 + #7 session handoffs + Phase 4 PR #14 prompt #12.Test plan
ruff check apps/backendcleanmypy apps/backendclean (117 source files)pytest tests/unit/test_obligation_service.py -k 'not integration'— 22/22 passnpm run lint0 errors (14 pre-existing fast-refresh warnings)npm run typecheckcleannpm run test -- --run(full vitest) — 241/241 pass (+18 new)npx playwright test --list --grep '@obligations'— 4 collect--with-obligationsseed → tab → distribution → row → drawer → cross-link to component → NOTICE downloadFollow-up backlog (separate PR)
affected_components(.limit(500)+truncated: bool) andobligation.text(clamp at 64 KiB).X-Content-Type-Options: nosniffmiddleware (PR Phase 4 PR #13 — Admin Panel: Users & Teams #13 Info chore: harden worker image + restore image-scan hard-fail #2)._escape_like→core/sql_safety.pypromote,_authz_denyshared helper across 5 service modules.@radix-ui/react-tabsswap (5 tabs all active).🤖 Generated with Claude Code