Skip to content

[] Admin security dashboard page (app/admin/security/page.tsx), advisories list + PATCH dialog (app/admin/security/advisories/page.tsx), and audit log search/export page (app/admin/security/audit-log/page.tsx)#293

Merged
zoetaka38 merged 5 commits into
mainfrom
feature/019f0d78f5fe-019f0d78f5fe
Jun 29, 2026

Conversation

@red-codens

@red-codens red-codens Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Type of Change

  • feat — new feature
  • fix — bug fix
  • chore — tooling / maintenance
  • docs — documentation only
  • refactor — code change that neither fixes a bug nor adds a feature
  • test — adding or updating tests
  • ci — CI/CD configuration

Testing

Checklist

  • Tests pass locally
  • Lint clean (task lint)
  • Commit messages follow Conventional Commits

Task Goal

Target Repository

Corevice/open-git
All work for this task belongs in this repository. Do the implementation inside this repository's directory in the workspace; do not search or edit other repositories unless this task explicitly requires a cross-repo change.

Context

Scaffold the three admin security pages using the components and types created in sub-task 0. Each page must be admin-only and consume the /api/v3 security endpoints.

Steps

  1. Create open-git/app/admin/security/page.tsx — RSC; fetch advisory counts per severity; render four SecuritySummaryCard components + recent scan jobs list.
  2. Create open-git/app/admin/security/advisories/page.tsx — Client Component; state/severity filter form; advisories table with Badge for severity; Dialog with AdvisoryStatusForm for PATCH; call PATCH /api/v3/repos/:owner/:repo/security-advisories/:ghsa_id.
  3. Create open-git/app/admin/security/audit-log/page.tsx — Client Component; phrase + action + date range form; results table showing actor_login, action, ip_address, created_at; Export button calls export endpoint and toasts job_id.
  4. Add requireAdminRole check (or reuse existing session guard) in each page's loader/redirect logic.

Verification Checklist

  • All three pages exist under app/admin/security/
  • Each page uses requireAdminRole or equivalent redirect
  • Dashboard page renders SecuritySummaryCard for each severity level
  • Advisories page uses AdvisoryStatusForm in a dialog
  • Audit log page has date range inputs
  • Export button calls export endpoint
  • npx tsc --noEmit passes with no errors in new files

Original PRD Content

セキュリティ監査・脆弱性対応 実装PRD

Features

セキュリティ監査・脆弱性対応 残実装リスト

注意: 現状の scaffold には該当する画面 (routes/forms/components) が一切検出されていない

したがって本機能は 非画面化要件 (バックエンド + 運用 + 一部管理画面) を中心に縦切り実装する。最小限の管理 UI を新規 scaffold する前提で記載する。

フロント wiring (新規 scaffold が必要)

  • app/admin/security/page.tsx: 管理者向けセキュリティダッシュボード (依存関係スキャン結果・脆弱性サマリ・

Changes Made

Expl
oring
existing
security
components
and
admin
patterns
in
open
-git
so
we
can
scaffold
the
three
admin
pages
.

Creating
the
three
admin
security
pages
using
existing
components
and
admin
page
patterns
.

Three admin security pages are in place under app/admin/security/, wired to the /api/v3 security endpoints and aligned with the
existing admin page patterns.

Dashboard (app/admin/security/page.tsx)

  • Server Component with requireAdminRole() (cookie auth + redirect on
  • Fetches org-scoped advisories and counts by severity

  • Renders four SecuritySummaryCard components (critical/high/medium/low)

  • Lists recent scan jobs from /api/v3/orgs/{org}/scan-jobs

  • Shows access denied on 403

Advisories (`app/admin/security/advisories

/page.tsx`)

  • Client Component with state/severity filter form
  • Table with Badge for severity
  • Inline Dialog wrapping AdvisoryStatusForm for status
    updates
  • PATCH /api/v3/repos/:owner/:repo/security-advisories/:ghsa_id
  • requireAdminRole() via token check +
    403 handling

Audit log (app/admin/security/audit-log/page.tsx)

  • Client Component with phrase, action, and date range (after/before) filters

  • Table columns: actor_login, action, ip_address, created_at

  • Export button calls /api/v3/orgs/{org}/audit-log /export?format=csv and toasts the job_id

  • Admin guard equivalent on client pages

All pages follow the same layout
and auth pattern as the existing observability/compatibility admin pages.

[PURPLE-MODIFIED-FILES]
app/admin/security/page.tsx
app/admin/security/advisories
/page
.tsx

app
/admin
/security
/
audit
-log
/page
.tsx

[/
P
UR
P
LE
-M
OD
IFIED
-F
ILES
]

Task ID: 019f0d78-f5fe-7cc3-9169-4546c64ad89c
Generated by Purple Codens

codens-agent and others added 2 commits June 28, 2026 09:34
…dvisories list + PATCH dialog (app/admin/security/advisories/page.tsx), and audit log search/export page (app/admin/security/audit-log/page.tsx)

@orange-codens orange-codens Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍊 Orange Codens レビュー

3つの管理者向けセキュリティページを新規追加する PR です。重大な問題として、クライアントコンポーネントの requireAdminRole が実際には「トークンの存在確認」しか行っておらず、管理者ロールを検証していない点が挙げられます(advisories・audit-log 両ページ)。また、フィルター値が useCallback の依存配列に含まれているため、入力変更のたびに API リクエストが発火する UX 上の問題も存在します(特に phrase 入力はキーストロークごとに発火)。加えて、組織解決ロジック・API_BASE 定数・ヘッダー JSX が3ファイルに重複しており、共通化が強く推奨されます。RSC で NEXT_PUBLIC_ 変数を使用している点もサーバー専用変数への変更が望ましいです。

🔒 セキュリティ: 3 ページすべてで requireAdminRole 関数がトークンの存在のみを確認しており、管理者ロール(role)の検証を行っていない。認証済みの一般ユーザーが脆弱性ダッシュボード・監査ログ(IP アドレスを含む機微情報)・アドバイザリ管理機能にアクセスできる恐れがあり、これが最大のリスクである。サーバーサイド(Next.js middleware または RSC)での権限検証の追加が急務。加えて、org クエリパラメータの入力検証不足および API エラーメッセージの直接露出も改善が必要である。

🔴 critical: 4 / 🟠 high: 5 / 🟡 medium: 7 / 🔵 low: 4

その他の指摘 (15 件)
  • 🟠 [high] app/admin/security/advisories/page.tsx:204 — フィルター変更のたびに自動的に API リクエストが発生する (confidence: 0.90, code.logic.filter_change_triggers_immediate_refetch)
  • 🟠 [high] app/admin/security/page.tsx:31 — サーバーコンポーネントの管理者チェックがトークンの存在確認のみで権限を検証していない (confidence: 0.90, sec.sast.authz.missing_server_side_admin_guard)
  • 🟠 [high] app/admin/security/page.tsx:161 — resolveOrgLogin が失敗すると RSC でキャッチされずエラーページになる (confidence: 0.88, code.logic.resolveOrg_throws_on_dashboard)
  • 🟠 [high] app/admin/security/advisories/page.tsx:348 — advisory.severity が severityBadgeClass に存在しないとランタイムエラー (confidence: 0.78, code.logic.advisory_severity_index_unchecked)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:133 — 組織解決ロジックが3ファイルに重複している (confidence: 0.95, maintainability.duplication.org_resolution_logic)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:268 — ヘッダーと Access Denied UI が各ページに重複している (confidence: 0.93, maintainability.duplication.header_layout)
  • 🟡 [medium] app/admin/security/page.tsx:12 — RSC で NEXT_PUBLIC_ 環境変数を使用しているが、サーバー側では非公開変数を使うべき (confidence: 0.82, code.logic.rsc_uses_next_public_env)
  • 🟡 [medium] app/admin/security/page.tsx:226 — URL クエリパラメータ orgParam を検証せずリンクの href に直接埋め込み (confidence: 0.75, sec.sast.injection.open_redirect)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:415 — AdvisoryStatusForm の onSubmit コールバック内で submitting フラグを確認しているが二重送信を完全に防げない (confidence: 0.72, code.logic.handleStatusUpdate_not_awaited_properly)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:153 — action フィルターのクエリパラメータ名が include になっており GitHub API 仕様と不一致の可能性 (confidence: 0.70, code.logic.action_param_name_mismatch)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:351 — 監査ログの IP アドレスがクライアントブラウザに全件露出する (confidence: 0.70, sec.sast.info_disclosure.audit_log_ip_exposure)
  • 🔵 [low] app/admin/security/advisories/page.tsx:28 — API_BASE 定数が3ファイルに重複定義されている (confidence: 0.97, maintainability.naming.api_base_duplicated)
  • 🔵 [low] app/admin/security/audit-log/page.tsx:360 — created_at が ISO 文字列のまま表示される (confidence: 0.88, maintainability.ux.date_display_raw_iso)
  • 🔵 [low] app/admin/security/advisories/page.tsx:74 — 自前 Dialog 実装にフォーカストラップと Escape キー対応が欠落している (confidence: 0.85, code.logic.dialog_no_focus_trap)
  • 🔵 [low] app/admin/security/advisories/page.tsx:258 — サーバーから返されたエラーメッセージをそのまま UI に表示している (confidence: 0.70, sec.sast.info_disclosure.error_message_leak)

head: ba3170e | Orange Codens (P1)

Comment thread app/admin/security/advisories/page.tsx Outdated
}
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] requireAdminRole がトークン有無しか確認せず、実際のロール検証を行っていない

クライアント側の requireAdminRole 関数はトークンの存在確認しか行っておらず、管理者ロールを検証していません。

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

認証済みの非管理者ユーザーでもこのガードを通過できます。実際のアクセス制御はサーバー側の 403 レスポンスに依存していますが、この関数名が「管理者ロールを保証する」という誤った印象を与えます。同じ問題が audit-log/page.tsx にも存在します(同一実装)。関数名を requireAuth に変更するか、実際にロール情報を検証する仕組みを追加してください。

@@ -98,7 +98,7 @@
-function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
+function requireAuth(token: string | null, router: ReturnType<typeof useRouter>): boolean {
   if (!token) {
     router.push("/login");
     return false;
   }
   return true;
 }

code.auth.token_only_guard | confidence: 0.92

const [org, setOrg] = useState("");
const [entries, setEntries] = useState<OrgAuditLogEntry[]>([]);
const [phrase, setPhrase] = useState("");
const [action, setAction] = useState("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] requireAdminRole がトークン有無しか確認せず、実際のロール検証を行っていない(audit-log)

advisories ページと同じ問題が audit-log ページにも存在します。

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

トークンが存在するだけで管理者とみなされるため、通常ユーザーもページロジックを実行できます(サーバー側 403 で最終的にブロックされますが、関数名が誤解を招きます)。

code.auth.token_only_guard | confidence: 0.92

Comment thread app/admin/security/advisories/page.tsx Outdated
return false;
}
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] クライアントサイドの管理者チェックはトークンの存在確認のみで権限検証が不十分

CWE-285: Improper Authorization

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

この requireAdminRole 関数はトークンが存在するかどうかのみを確認しており、実際にトークンが管理者権限を持つかどうかを検証していない。通常の認証済みユーザーのトークンを持っていれば、この関数は true を返す。実際の管理者権限チェックはサーバー側 API(403 レスポンス)に依存しているが、クライアント側のガードとして関数名が requireAdminRole であるにもかかわらず役割 (role) の検証を行っていない点は、将来的な誤用・bypass のリスクがある。同様のパターンは app/admin/security/audit-log/page.tsx にも存在する(116〜121行目)。

トークンのデコードでロール確認を行うか、または関数名を `requireAuthenticated` に改名して実態を正確に示す。管理者権限の真の検証はサーバー側 middleware または Next.js の route handler で行うべき。

sec.sast.authz.insufficient_admin_check | confidence: 0.85

const router = useRouter();
const searchParams = useSearchParams();
const { token } = useAuth();
const toast = useToast();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] クライアントサイドの管理者チェックはトークンの存在確認のみで権限検証が不十分

CWE-285: Improper Authorization

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

advisories/page.tsx と同様に、トークンの存在のみを確認しており管理者ロールの検証を行っていない。認証済みの非管理者ユーザーがこのページにアクセスでき、監査ログ(actor_loginactionip_address 等の機微情報を含む)を閲覧可能になる恐れがある。

サーバーサイドの middleware でロールベースのアクセス制御を実装する。クライアントコンポーネントでの権限チェックは補助的なものとし、真の認可はサーバー側で行う。

sec.sast.authz.insufficient_admin_check | confidence: 0.85

}
if (action) {
params.set("include", action);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] フィルター変更のたびに自動的に API リクエストが発生する(audit-log)

loadAuditLoguseCallback 依存配列に phrase, action, after, before が含まれているため、各入力フィールドの文字入力ごとに API リクエストが発火します。

該当箇所:

  }, [token, router, orgParam, phrase, action, after, before]);

  useEffect(() => {
    loadAuditLog();
  }, [loadAuditLog]);

特に phrase はテキスト入力のため、キーストロークごとにリクエストが送信されます。debounce を追加するか、フォーム送信時のみリクエストするようリファクタリングしてください。

code.logic.filter_change_triggers_immediate_refetch | confidence: 0.95

@orange-codens orange-codens Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍊 Orange Codens レビュー

重大な問題: requireAdminRole がクライアントコンポーネント 2 ファイルで「トークンの存在確認のみ」を行っており、ログイン済みの一般ユーザーが管理者ページの UI にアクセスできる状態になっている(API が 403 を返すまで表示制限がかからない)。また、audit-log の action フィルターがクエリパラメータ名 include で送信されており、意図したフィルタリングが機能しない可能性が高い。全体的に requireAdminRole・ヘッダー JSX・API_BASE・org 解決ロジックが 3 ファイルに渡って重複しており、共通ユーティリティへの抽出が強く推奨される。テストが一切追加されていない点も課題。

🔒 セキュリティ: 最も深刻な問題は、3ファイル共通の requireAdminRole 関数がトークンの存在のみを確認し、管理者ロールの検証を行っていない点です(CWE-862)。これにより、ログイン済みの一般ユーザーがセキュリティアドバイザリや監査ログ(IP アドレス含む)にアクセスできる可能性があります。クライアントコンポーネントについては Next.js middleware またはサーバーサイドでロール検証を実施し、RSC(page.tsx)については API でトークンの権限を確認してからページをレンダリングすることを強く推奨します。また、バックエンドからのエラーメッセージをそのまま UI に表示している箇所も情報漏洩のリスクがあります。

🔴 critical: 3 / 🟠 high: 5 / 🟡 medium: 9 / 🔵 low: 4

その他の指摘 (16 件)
  • 🟠 [high] app/admin/security/page.tsx:167 — resolveOrgLogin が失敗するとページ全体が未処理例外になる (confidence: 0.85, code.logic.resolveOrg_throws_on_dashboard)
  • 🟠 [high] app/admin/security/audit-log/page.tsx:289 — Export ボタンが org 解決前は常に無効になる設計上の問題 (confidence: 0.80, code.logic.export_button_disabled_on_initial)
  • 🟠 [high] app/admin/security/page.tsx:196 — searchParams の org パラメータが URL に非エスケープで埋め込まれオープンリダイレクトの素地になる (confidence: 0.75, sec.sast.injection.open_redirect)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:103 — requireAdminRole が3ファイルにそれぞれ別実装として重複している (confidence: 0.95, maintainability.duplication.require_admin_role_tripled)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:249 — ヘッダー・レイアウト JSX が3ファイル全体で逐語的に重複している (confidence: 0.93, maintainability.duplication.header_layout_tripled)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:130 — resolveOrgLogin ロジックが3ファイルに重複している (confidence: 0.93, maintainability.duplication.resolve_org_duplicated)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:392 — Dialog が閉じた後も AdvisoryStatusForm がアンマウントされない (confidence: 0.82, code.logic.dialog_content_rendered_when_closed)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:240 — バックエンドのエラーメッセージをそのまま UI に表示している (confidence: 0.80, sec.sast.info_disclosure.error_message)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:118 — action フィルターのクエリパラメータ名が include になっており仕様と不一致 (confidence: 0.78, code.logic.audit_filter_param_naming)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:155 — Export 時も action パラメータが include で送られている (confidence: 0.78, code.logic.audit_export_include_naming)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:335 — 未知の severity 値で severityBadgeClass のキールックアップが undefined になる (confidence: 0.72, code.null_safety.severity_badge_undefined_key)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:148 — ユーザー入力の phrase がバックエンド検索クエリにサニタイズなしで渡される (confidence: 0.70, sec.sast.injection.parameter_injection)
  • 🔵 [low] app/admin/security/advisories/page.tsx:29 — API_BASE 定数が各ファイルで重複定義されている (confidence: 0.97, maintainability.naming.api_base_const_duplicated)
  • 🔵 [low] app/admin/security/page.tsx:1 — 3ページとも単体テスト・統合テストが存在しない (confidence: 0.97, maintainability.testing.no_tests)
  • 🔵 [low] app/admin/security/audit-log/page.tsx:122 — datetime-local 入力値を new Date() で ISO 変換するとローカルタイムゾーンに依存する (confidence: 0.75, code.logic.datetime_local_timezone)
  • 🔵 [low] app/admin/security/audit-log/page.tsx:345 — 監査ログの ip_address をフロントエンドテーブルに直接表示している (confidence: 0.60, sec.sast.info_disclosure.ip_address_exposure)

前回 run からの未解決 finding: 1 件

head: 28cc8b0 | Orange Codens (P1)

const router = useRouter();
const searchParams = useSearchParams();
const { token } = useAuth();
const toast = useToast();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] クライアント側のみのトークン存在チェックで管理者権限を検証している(監査ログページ)

CWE-862: Missing Authorization

app/admin/security/advisories/page.tsx と同様に、監査ログページでも requireAdminRole がトークンの存在しか確認しておらず、実際の管理者ロール検証が行われていません。

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

監査ログは特に機密性の高いデータ(actor_login, action, ip_address)を含んでおり、非管理者ユーザーへの漏洩リスクが高いです。

sec.sast.authz.insufficient_admin_check | confidence: 0.95

repo: string;
} | null {
if (advisory.repository?.owner.login && advisory.repository.name) {
return {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] requireAdminRole がトークンの存在確認のみで管理者権限を検証していない

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

この実装はトークンが存在するかどうかだけを確認しており、ログイン済みの一般ユーザーも管理者ページにアクセスできてしまう。関数名は requireAdminRole だが実態は requireAuthenticated に過ぎない。API が 403 を返した場合に accessDenied フラグで表示を切り替えているだけであり、UIレベルのアクセス制御が存在しない。サーバーサイドの page.tsx では cookie からトークンを取得して redirect しており、クライアント側にも同等の管理者ロール確認ロジックが必要。同じ問題が app/admin/security/audit-log/page.tsx の 52〜58 行目にも存在する。

code.auth.token_only_guard | confidence: 0.92

Comment thread app/admin/security/audit-log/page.tsx Outdated
return true;
}

export default function SecurityAuditLogPage() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] requireAdminRole がトークンの存在確認のみで管理者権限を検証していない (audit-log 側)

該当箇所:

function requireAdminRole(token: string | null, router: ReturnType<typeof useRouter>): boolean {
  if (!token) {
    router.push("/login");
    return false;
  }
  return true;
}

advisories ページと同一の問題。トークン保持者なら誰でも管理者ページを閲覧できる状態になっている。

code.auth.token_only_guard | confidence: 0.92

Comment thread app/admin/security/page.tsx Outdated

return token;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] RSC の requireAdminRole がトークンの存在のみ確認し管理者ロールを検証していない

CWE-862: Missing Authorization

サーバーコンポーネント (app/admin/security/page.tsx) の requireAdminRole もトークンの cookie 存在チェックのみで管理者権限を判定しています。

該当箇所:

async function requireAdminRole(): Promise<string> {
  const cookieStore = await cookies();
  const token = cookieStore.get("authToken")?.value;

  if (!token) {
    redirect("/login");
  }

  return token;
}

トークンが存在するだけで管理者であると仮定しており、ロール/スコープの検証がありません。ログイン済みの一般ユーザーがこのページにアクセスすると、バックエンドが 403 を返すまでの間、リクエストが通過します。バックエンドが適切に 403 を返す場合は <AccessDenied /> が表示されますが、フロントエンド層での防御が皆無です。

async function requireAdminRole(): Promise<string> {
  const cookieStore = await cookies();
  const token = cookieStore.get("authToken")?.value;

  if (!token) {
    redirect("/login");
  }

  // ロール検証: /api/v3/user などで管理者権限を確認する
  const meRes = await fetch(`${API_BASE}/api/v3/user`, {
    headers: { Authorization: `Bearer ${token}` },
    cache: "no-store",
  });
  if (!meRes.ok) redirect("/login");
  const me = await meRes.json() as { site_admin?: boolean };
  if (!me.site_admin) redirect("/403");

  return token;
}

sec.sast.authz.idor | confidence: 0.90

Comment thread app/admin/security/advisories/page.tsx Outdated
Authorization: `Bearer ${token}`,
},
cache: "no-store",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] stateFilter/severityFilter 変更時に毎回 /user/orgs を再取得する

該当箇所:

const loadAdvisories = useCallback(async () => {
  ...
  let resolvedOrg = orgParam ?? "";
  if (!resolvedOrg) {
    const orgsResponse = await fetch(`${API_BASE}/api/v3/user/orgs`, {
      ...
    });
    ...
    resolvedOrg = orgs[0].login;
  }
  setOrg(resolvedOrg);

loadAdvisories[token, router, orgParam, stateFilter, severityFilter] を依存配列に持つ useCallback に内包されており、フィルター変更のたびに /user/orgs エンドポイントへ余分なリクエストが発生する。org は state として保持しているにもかかわらず、setOrg 後の値は次の render まで反映されないため、loadAdvisories 内では毎回 resolvedOrg ローカル変数に解決している。org を別の useEffect で一度だけ取得して state に保存し、loadAdvisoriesorg state が確定してから呼ぶ設計にすべき。同様の問題が audit-log/page.tsxloadAuditLog にも存在する。

code.logic.org_state_stale_on_filter | confidence: 0.88

…dvisories list + PATCH dialog (app/admin/security/advisories/page.tsx), and audit log search/export page (app/admin/security/audit-log/page.tsx)

@orange-codens orange-codens Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍊 Orange Codens レビュー

3 つの管理者向けセキュリティページ全体の構造は PRD 要件を満たしており、認証ガード・エラーハンドリング・フィルタリング・エクスポートの基本的な実装は揃っている。

最重要懸念点: verifyOrgAdminRole がメンバー一覧エンドポイントをページネーションなしで取得してクライアント側照合する実装は、大規模 org で誤 forbidden 判定を引き起こす可能性が高い(confidence 0.82)。また datetimeLocalToIso がローカルタイムゾーン依存のため、UTC オフセットが大きい環境では日付フィルターがずれる問題と、対応するテストが CI タイムゾーンによって失敗する問題がある。カスタム Dialog コンポーネントの ARIA 属性不足(aria-labelledby 欠落・Escape キー未対応)はアクセシビリティ基準を満たさない。handleExportloadAuditLog でのクエリパラメータ構築ロジックの重複も保守性リスクとして修正を推奨する。

🔒 セキュリティ: このPRの最大のリスクは、管理者権限チェックがクライアントサイドのみで実装されている点である(checkClientOrgAdminAccess は Client Component から呼ばれ、ブラウザ上で動作する)。Next.js middleware または Server Component layout での強制的なサーバーサイドガードが欠如しており、悪意あるユーザーがネットワーク応答を改ざんして管理者画面へアクセスできる可能性がある。また、verifyOrgAdminRole がメンバー一覧をページネーションなしで取得して権限判定を行っており、大規模 org では誤判定が生じうる。action フィルターのホワイトリスト検証不足など中程度の入力検証問題も存在する。

🔴 critical: 2 / 🟠 high: 2 / 🟡 medium: 10 / 🔵 low: 4

その他の指摘 (14 件)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:70 — カスタム Dialogaria-labelledby がなくアクセシビリティ要件を満たさない (confidence: 0.90, code.logic.dialog_no_aria_labelledby)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:60 — カスタム Dialog が Escape キーに反応しないためキーボード操作でクローズできない (confidence: 0.90, code.logic.dialog_no_escape_key_handler)
  • 🟡 [medium] lib/admin/security.ts:140 — org パラメータ未指定時に最初の org を無条件で使用するため、複数 org に属するユーザーで意図しない org を操作する (confidence: 0.88, code.logic.resolveOrgLogin_first_org_fallback)
  • 🟡 [medium] lib/admin/security.ts:56datetimeLocalToIso がブラウザのローカルタイムゾーンで ISO 変換するため、異なるタイムゾーンのユーザーで期待と異なる結果になる (confidence: 0.85, code.logic.datetime_local_to_iso_timezone_ambiguity)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:168per_page=100 をハードコードしておりページネーションが未実装 (confidence: 0.85, maintainability.hardcoded_per_page_100)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:161 — Export 時に checkClientOrgAdminAccess を再実行しており、ページロード時の認証チェックと二重になっている (confidence: 0.78, performance.redundant_admin_check_on_export)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:174action フィルター値がサーバーに送信前にバリデーションされていない (confidence: 0.75, sec.sast.injection.action_filter_unvalidated)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:109resolveOrgLoginorgParam なしの場合にユーザーの最初の org を自動選択する (confidence: 0.70, sec.sast.injection.open_redirect)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:229handleStatusUpdate 内の早期 return が setSubmitting(false) を呼ばずにリークする (confidence: 0.55, code.logic.handleStatusUpdate_early_return_leaks_submitting)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:161handleExport 内の早期 returnsetExporting(false) が呼ばれずボタンが永久に無効化される (confidence: 0.52, code.logic.handleExport_exporting_flag_not_reset_on_early_return)
  • 🔵 [low] app/admin/security/audit-log/page.tsx:123 — Audit log 検索パラメータの構築ロジックが loadAuditLoghandleExport で完全に重複している (confidence: 0.95, maintainability.duplicate_query_param_build_logic)
  • 🔵 [low] __tests__/lib/admin/security.test.ts:59datetimeLocalToIso のテストがタイムゾーン依存で CI 環境によって失敗する可能性がある (confidence: 0.88, maintainability.test_timezone_dependent)
  • 🔵 [low] app/admin/security/audit-log/page.tsx:52 — ACTION_OPTIONS が静的ハードコードのため、新しい audit action 追加時に手動メンテナンスが必要 (confidence: 0.70, maintainability.action_options_static_list)
  • 🔵 [low] app/admin/security/audit-log/page.tsx:254 — エクスポートの job_id をトーストで表示することによる情報漏洩リスク (confidence: 0.60, sec.sast.info_leak.job_id_toast)

head: 5fa2a25 | Orange Codens (P1)

Comment thread lib/admin/security.ts
if (!options.token) {
options.onUnauthenticated();
return { status: "unauthenticated" };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] 管理者権限チェックがクライアント側 JS で完結しており、バイパス可能

CWE-602: Client-Side Enforcement of Server-Side Security

checkClientOrgAdminAccess および verifyOrgAdminRole はブラウザ上で実行される Client Component から呼ばれる。攻撃者は DevTools でネットワーク応答を改ざんするか、fetch をモックすることで role: "admin" を偽装し、管理者チェックを通過できる。

該当箇所:

// lib/admin/security.ts
export async function verifyOrgAdminRole(
  token: string,
  org: string,
): Promise<"admin" | "forbidden" | "unauthorized" | "error"> {
  ...
  const members = (await membersResponse.json()) as {
    login: string;
    role: string;
  }[];
  const membership = members.find((member) => member.login === user.login);

  if (!membership || !isAdminOrgRole(membership.role)) {
    return "forbidden";
  }

  return "admin";
}

また、各 Client Component (advisories/page.tsx, audit-log/page.tsx) が checkClientOrgAdminAccess を呼んでいるが、実際の API エンドポイント(/api/v3/orgs/:org/security-advisories など)に対する認可検証はバックエンド側で行われるべきであり、フロント側のチェックは UX 補助に限定すべき。フロント側の認可判定を信頼してデータを表示する設計は cross-tenant データ漏洩のリスクを持つ。

Client Component での管理者チェックは UX フォールバック(アクセス拒否 UI)のみに使用し、実際のデータアクセス制御はバックエンド API に委ねる。Next.js の Server Component / Route Handler 内でのみ `requireOrgAdminAccess` (server-side) を使用し、クライアントへはすでに認可済みのデータのみを渡す設計に変更する。

sec.sast.authz.client_side_admin_check | confidence: 0.85

Comment thread lib/admin/security.ts
return "forbidden";
}

if (!userResponse.ok || !membersResponse.ok) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] org メンバー一覧 API を用いた権限検証は IDOR を防げない

CWE-639: Authorization Through User-Controlled Key

verifyOrgAdminRole/api/v3/orgs/{org}/members のレスポンス(メンバー一覧)をクライアントが受け取り、その中から自分の login を探して role を確認する。

該当箇所:

const [userResponse, membersResponse] = await Promise.all([
  authFetch(token, "/api/v3/user"),
  authFetch(token, `/api/v3/orgs/${encodeURIComponent(org)}/members`),
]);
...
const user = (await userResponse.json()) as { login: string };
const members = (await membersResponse.json()) as {
  login: string;
  role: string;
}[];
const membership = members.find((member) => member.login === user.login);

if (!membership || !isAdminOrgRole(membership.role)) {
  return "forbidden";
}

このエンドポイントが per_page 上限(デフォルト 100件など)でページネーションされている場合、管理者が 101 人目以降のページに存在すると members に含まれず forbidden と誤判定される可能性がある。また、ページネーション未考慮のまま権限を forbidden と判断する設計は認可漏れの原因になりえる。専用の /api/v3/orgs/{org}/memberships/{username} エンドポイントを使うべき。

```ts
// メンバー一覧ではなく membership エンドポイントで直接確認する
const membershipResponse = await authFetch(
  token,
  `/api/v3/orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(user.login)}`
);
if (membershipResponse.status === 404) return "forbidden";
if (membershipResponse.status === 403) return "forbidden";
if (membershipResponse.status === 401) return "unauthorized";
if (!membershipResponse.ok) return "error";
const membership = await membershipResponse.json();
if (!isAdminOrgRole(membership.role)) return "forbidden";
return "admin";

<sub>`sec.sast.authz.idor` | confidence: 0.80</sub>

Comment thread lib/admin/security.ts
orgParam: string | null;
onUnauthenticated: () => void;
}): Promise<
| { status: "ok"; org: string }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] org メンバー一覧取得による管理者判定は信頼性・スケーラビリティに問題がある

該当箇所:

const [userResponse, membersResponse] = await Promise.all([
  authFetch(token, "/api/v3/user"),
  authFetch(token, `/api/v3/orgs/${encodeURIComponent(org)}/members`),
]);
// ...
const members = (await membersResponse.json()) as {
  login: string;
  role: string;
}[];
const membership = members.find((member) => member.login === user.login);

if (!membership || !isAdminOrgRole(membership.role)) {
  return "forbidden";
}

/api/v3/orgs/:org/members は通常「メンバー一覧」を返すエンドポイントであり、ページネーションが存在する場合、現在のコードは最初のページ分しか取得しない。大規模 org では対象ユーザーが後続ページに存在しても forbidden と判定されてしまう。

また、管理者ロール確認には /api/v3/orgs/:org/memberships/:username など membership 専用エンドポイントを使う方が 1 リクエストで済み、かつ確実に正しい role を返す。現在の実装は N 件のメンバーを全取得してクライアント側で照合しており、ページネーション未対応の場合は誤判定につながる。

code.logic.admin_role_check_uses_members_list | confidence: 0.82

setLoading(false);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] Client Component の管理者ガードは初回レンダリング前に完了しないため、一瞬データが表示される可能性がある

CWE-287: Improper Authentication

SecurityAdvisoriesPageSecurityAuditLogPage はいずれも Client Component であり、useEffect 内で非同期に管理者チェック (checkClientOrgAdminAccess) を行う。React のレンダリングサイクル上、チェック完了前に loading = true の状態でコンポーネントがレンダリングされるが、ローディング中は実データを表示しない設計になっている。しかし、accessDeniedloading の初期値が両方 false/true であることを前提とした UI の順序が壊れた場合、一時的に未認可状態でデータが見える余地がある。

該当箇所 (advisories/page.tsx):

const [loading, setLoading] = useState(true);
const [accessDenied, setAccessDenied] = useState(false);
...
useEffect(() => {
  void loadAdvisories();
}, [loadAdvisories]);

根本的な問題は、機密性の高い管理者ページをクライアントサイドの認可チェックのみで保護している点にある。Next.js の layout.tsx または middleware でサーバーサイドの認可ガードを設けるべき。

`app/admin/security/layout.tsx` を作成し、`requireOrgAdminAccess` をサーバーサイドで実行してから子ページをレンダリングする。Client Component 側のチェックは UX 補助として残す。

sec.sast.authz.missing_server_guard | confidence: 0.75

- Wrap advisories/audit-log content in <Suspense> (Next 15 CSR bailout)
- Remove unused test param; scoped no-control-regex disable
- Exclude k6 test/load scripts from eslint (pre-existing main baseline)
@zoetaka38
zoetaka38 merged commit 1f20ff8 into main Jun 29, 2026
11 of 13 checks passed

@orange-codens orange-codens Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍊 Orange Codens レビュー

重大な指摘として、ダッシュボードの severity 集計が per_page=100 打ち切りで不正確になる点と、audit-log ページで検索入力のたびに API が自動呼び出しされる性能問題があります。加えて、resolved org と URL クエリの不一致(ダッシュボードリンク・パンくず)およびテスト不足(ダッシュボード RSC / PATCH フロー)も修正推奨です。全体として admin ガードと /api/v3 連携の骨格は PRD に沿っています。

🔒 セキュリティ: 3 つの管理セキュリティページのうち、ダッシュボードのみサーバー側 requireOrgAdminAccess を使用し、Advisories / Audit log はクライアント側認可のみで防御境界が弱いです。入力値は sanitizeOrgLoginencodeURIComponentURLSearchParams・React テキスト描画で適切に扱われており、SQLi / SSRF / 暗号誤用の痕跡は diff 内に見当たりません。API 側の Bearer + 403 enforcement が前提となる設計です。

🟠 high: 2 / 🟡 medium: 6 / 🔵 low: 3

その他の指摘 (9 件)
  • 🟡 [medium] __tests__/app/admin/security-pages.test.tsx:108 — ダッシュボード RSC と advisory PATCH フローのテストが欠落 (confidence: 0.90, maintainability.test_gap)
  • 🟡 [medium] app/admin/security/page.tsx:75 — 子ページリンクが resolved org ではなく orgParam を使っている (confidence: 0.88, code.logic.stale_query_param)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:207 — Apply filters ボタンとフィルタ自動再取得が二重化している (confidence: 0.82, maintainability.ux_inconsistency)
  • 🟡 [medium] app/admin/security/advisories/page.tsx:137 — Advisories ページがクライアント側のみで管理者認可を実施 (confidence: 0.82, sec.sast.authz.client_side_enforcement)
  • 🟡 [medium] app/admin/security/audit-log/page.tsx:82 — Audit log ページがクライアント側のみで管理者認可を実施 (confidence: 0.82, sec.sast.authz.client_side_enforcement)
  • 🟡 [medium] components/admin/SecurityPageLayout.tsx:48 — パンくずの Security リンクに org クエリが付かない (confidence: 0.80, maintainability.missing_org_context)
  • 🔵 [low] app/admin/security/page.tsx:124 — scan-jobs 取得失敗が空リストとして黙って扱われる (confidence: 0.75, code.error_handling.silent_failure)
  • 🔵 [low] app/admin/security/advisories/page.tsx:66 — Dialog がページ内にインライン定義されている (confidence: 0.70, maintainability.duplicate_component)
  • 🔵 [low] lib/admin/security.ts:157 — 管理者判定のために org 全メンバー一覧をクライアントへ取得 (confidence: 0.68, sec.sast.authz.excessive_data_exposure)

head: dd542c0 | Orange Codens (P1)


useEffect(() => {
void loadAuditLog();
}, [loadAuditLog]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] 検索フォーム入力のたびに audit-log API が自動呼び出しされる

phrase / action / after / beforeloadAuditLog の依存配列に含まれ、useEffect で即座に再実行されます。Search ボタンとは別に、入力のたびに API が走ります。

該当箇所:

  const [phrase, setPhrase] = useState("");
  ...
  const [action, setAction] = useState("");
  const [after, setAfter] = useState("");
  const [before, setBefore] = useState("");
  ...
  }, [token, router, orgParam, phrase, action, after, before, apiBase]);

  useEffect(() => {
    void loadAuditLog();
  }, [loadAuditLog]);

特に phrase のキー入力 1 文字ごとに /audit-log が呼ばれ、N+1 的な不要リクエストとレース(古いレスポンスが後から上書き)が発生します。Search ボタン submit 時のみ fetch する設計に揃えるべきです。

@@ -162,1 +162,1 @@
-  }, [token, router, orgParam, phrase, action, after, before, apiBase]);
+  }, [token, router, orgParam, apiBase]);
@@ -268,4 +268,4 @@
         onSubmit={(event) => {
           event.preventDefault();
           void loadAuditLog();
         }}

loadAuditLog 内では state の phrase/action/after/before をそのまま参照)


<sub>`performance.redundant_api_calls` | confidence: 0.93</sub>

const advisories =
(await advisoriesResponse.json()) as SecurityAdvisory[];
const severityCounts = countBySeverity(advisories);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] severity 集計が per_page=100 件に限定され不正確になる

ダッシュボードの severity カウントは、取得した advisories 配列をクライアント側で集計していますが、API 呼び出しが per_page=100 で打ち切られています。組織に 100 件超の advisory がある場合、サマリカードの数値が実際より少なく表示されます。

該当箇所:

  const advisoriesResponse = await fetch(
    `${apiBase}/api/v3/orgs/${encodeURIComponent(org)}/security-advisories?per_page=100`,
    ...
  );
  ...
  const advisories =
    (await advisoriesResponse.json()) as SecurityAdvisory[];
  const severityCounts = countBySeverity(advisories);

PRD の「fetch advisory counts per severity」要件に対し、全件または専用 count エンドポイントを使わない限り、本番で誤ったセキュリティ posture を示すリスクがあります。

@@ -79,1 +79,1 @@
-    `${apiBase}/api/v3/orgs/${encodeURIComponent(org)}/security-advisories?per_page=100`,
+    `${apiBase}/api/v3/orgs/${encodeURIComponent(org)}/security-advisories/summary`,
@@ -110,3 +110,3 @@
-  const advisories =
-    (await advisoriesResponse.json()) as SecurityAdvisory[];
-  const severityCounts = countBySeverity(advisories);
+  const severityCounts =
+    (await advisoriesResponse.json()) as Record<AdvisorySeverity, number>;

code.logic.incomplete_aggregation | confidence: 0.92

@zoetaka38
zoetaka38 deleted the feature/019f0d78f5fe-019f0d78f5fe branch July 2, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant