Skip to content

Commit 0cfbb11

Browse files
committed
chore: setup CI/CD workflows with security hardening
Summary: Added comprehensive CI workflows for Backend (FastAPI) and Frontend (Next.js) with security-first approach based on Issue #3. Backend CI (.github/workflows/backend.yml): - Poetry-based dependency management - Ruff linting (fast Rust-based linter) - Pytest with proper exit code handling (only tolerate exit 5 for no tests) - Python 3.13 support Frontend CI (.github/workflows/frontend.yml): - npm ci for reproducible installs - ESLint + Next.js build validation - Mock environment variables for CI builds Security Gatekeeper (.github/workflows/security.yml): - Dependency-only triggers (pyproject.toml, package.json, Dockerfiles) - TruffleHog secret scanning (pinned to v3.82.13) - SBOM generation + Grype vulnerability scan (Critical only) - Support for main/develop branches (Git Flow compatible) - Managed exceptions via .grype.yaml with documented justification - Emergency bypass via skip-security label (admin-only) Security Improvements: - Pin all dependencies to version ranges (no wildcards) - Event-specific SHA refs for accurate diff scanning - Proper pytest exit code handling to catch real failures - Tool versions pinned to prevent supply-chain attacks Documentation: - ADR 001: CI Strategy & Tool Selection - Vulnerability exception config (.grype.yaml) Rationale: - Unpinned dependencies pose supply-chain risk - Early security detection at develop branch saves time - Dependency-only scans optimize CI cost - Documented exceptions enable realistic security posture Close #3
1 parent f114a0b commit 0cfbb11

File tree

6 files changed

+330
-2
lines changed

6 files changed

+330
-2
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# ADR 001: CI Strategy & Tool Selection
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
プロジェクトの初期段階において、Backend (FastAPI) と Frontend (Next.js) のコード品質とデプロイ可能性を保証するためのCIパイプラインが必要でした。
10+
特に、Python環境におけるリンター/フォーマッターの選定と、Next.jsビルド時の環境変数依存性が課題となっていました。
11+
12+
## Decision
13+
14+
### 1. Backend: Ruff & Pytest
15+
16+
- **Decision**: Pythonのリンター/フォーマッターとして `Ruff` を採用し、テストランナーとして `pytest` を採用する。
17+
- **Reason**:
18+
- `Ruff` は Rust製で極めて高速であり、従来の `Flake8`, `Black`, `isort` などの機能を単体でカバーできるため、CI時間を短縮し設定を簡素化できる。
19+
- `pytest` はPythonエコシステムのデファクトスタンダードであり、将来的な拡張性が高い。
20+
21+
### 2. Frontend: npm ci & Mock Env
22+
23+
- **Decision**: 依存関係インストールに `npm ci` を使用し、ビルド時にダミーの環境変数を注入する。
24+
- **Reason**:
25+
- `npm ci``package-lock.json` に厳密に基づいたインストールを行うため、CI環境での再現性が保証される。
26+
- Next.jsのビルドプロセス(Static Generation等)でAPI URLなどの環境変数が参照される可能性があるため、CI上ではダミー値を設定してビルドエラーを防ぐ戦略をとる。
27+
28+
### 3. Workflow Separation
29+
30+
- **Decision**: `backend.yml``frontend.yml` にワークフローを分離し、`paths` フィルタを設定する。
31+
- **Reason**:
32+
- モノレポ構成において、関連しない変更(例:Frontendのみの修正)でBackendのCIが走ることを防ぎ、リソース消費とフィードバック時間を最適化するため。
33+
34+
### 4. Dependency Version Pinning (Supply Chain Security)
35+
36+
- **Decision**: Python依存関係を `*` (any version) から具体的なバージョン範囲 (例: `^0.115.0`) に固定する。
37+
- **Reason**:
38+
- `*` 指定では、PyPIから常に最新版が取得されるため、パッケージが乗っ取られた場合(supply-chain attack)、悪意あるコードがCI環境や本番環境で実行されるリスクがある。
39+
- バージョン範囲を固定することで、依存関係の更新を意図的・管理的に行い、セキュリティリスクを低減する。
40+
41+
### 5. GitHub Actions Version Pinning
42+
43+
- **Decision**: `trufflesecurity/trufflehog@main` をタグバージョン(例: `@v3.82.13`)に固定する。
44+
- **Reason**:
45+
- `@main` ブランチ参照は上流の変更によってCI動作が予期せず変わる可能性があり、サプライチェーンリスクが高い。
46+
- タグやコミットSHAに固定することで、CI実行内容の不変性と再現性を担保する。
47+
48+
### 6. Test Exit Code Handling
49+
50+
- **Decision**: `pytest || echo ...` の代わりに、exit code 5(テスト未検出)のみを許容し、実際のテスト失敗は検知する。
51+
- **Reason**:
52+
- `|| echo` は全てのエラーを握りつぶしてしまい、テストが実際に失敗してもCIが成功扱いになる。
53+
- exit code を判定することで、「テストがまだない状態」と「テストが失敗した状態」を正確に区別できる。
54+
55+
### 7. Event-Specific Git References in Security Scan
56+
57+
- **Decision**: `github.head_ref` / `github.event.repository.default_branch` の代わりに、イベント種別に応じた適切なSHA参照を使用する。
58+
- **Reason**:
59+
- `github.head_ref` は push イベントでは空文字になるため、差分スキャンが成立しない。
60+
- PR時は `github.event.pull_request.{base,head}.sha`、push時は `github.event.before` / `github.sha` を使用することで、正確な差分スキャンを実現する。
61+
62+
### 8. Dependency-Only Security Scans
63+
64+
- **Decision**: セキュリティスキャンは依存関係ファイル変更時のみ実行する(pyproject.toml, package.json, Dockerfile等)。
65+
- **Reason**:
66+
- SBOM生成と脆弱性スキャンはリソース消費が大きい。
67+
- アプリケーションコードの変更では依存関係の脆弱性状況は変わらないため、実行不要。
68+
- CI時間とコストの最適化。
69+
- **Branch Strategy**: `main``develop` の両方で実行し、Git Flow運用に対応。早期検出を優先。
70+
71+
### 9. Vulnerability Exception Management
72+
73+
- **Decision**: `.grype.yaml` でCritical脆弱性の除外設定を許可する。
74+
- **Reason**:
75+
- すべての脆弱性が実際のリスクとなるわけではない(未使用機能、誤検知等)。
76+
- 修正が存在しない場合や、他の制御で緩和されている場合の対応が必要。
77+
- 除外には文書化された正当な理由(notes)を必須とし、四半期ごとにレビューする運用を前提とする。
78+
79+
### 10. Emergency Admin Bypass
80+
81+
- **Decision**: `skip-security` ラベルをPRに付与することで、管理者が緊急時にセキュリティチェックをバイパス可能にする。
82+
- **Reason**:
83+
- 本番障害などの緊急対応時、セキュリティスキャンで修正がブロックされる状況を回避する必要がある。
84+
- ラベル付与はGitHub権限管理で制御可能(管理者のみ)。
85+
- バイパスは監査ログに記録され、事後レビューが可能。
86+
87+
## Consequences
88+
89+
- Backend開発者はローカルでも `ruff` を使用してコード規約を遵守する必要がある。
90+
- FrontendビルドがCIで成功しても、実行時エラー(環境変数設定ミスなど)は検知できないため、別途E2Eテストなどの検討が必要になる可能性がある。
91+
- 依存関係のバージョンを固定したため、定期的なアップデート戦略(Dependabotなど)が必要になる。
92+
- TruffleHogなどのツールバージョンを固定したため、新機能や修正を取り込むには手動更新が必要。
93+
- セキュリティスキャンを依存関係変更時のみに限定したため、アプリケーションコード変更でのCI時間が短縮される。
94+
- `.grype.yaml` での除外管理には厳格な運用ルール(文書化、定期レビュー)が必須。
95+
- 緊急バイパス機能は慎重に使用し、事後の振り返りと恒久対策の実施が必要。

.github/workflows/backend.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Backend CI
2+
3+
on:
4+
push:
5+
branches: ["main", "develop"]
6+
paths:
7+
- "backend/**"
8+
- ".github/workflows/backend.yml"
9+
pull_request:
10+
types: [opened, synchronize, reopened]
11+
paths:
12+
- "backend/**"
13+
- ".github/workflows/backend.yml"
14+
15+
defaults:
16+
run:
17+
working-directory: ./backend
18+
19+
jobs:
20+
check:
21+
name: Lint & Test (Python)
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
28+
- name: Install Poetry
29+
run: pipx install poetry
30+
31+
- name: Set up Python 3.13
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: "3.13"
35+
# Note: cache removed temporarily until poetry.lock is committed
36+
# Decision: Generate lock file during CI to avoid commit noise
37+
# Reason: In early development, lockfile can be generated on-the-fly
38+
39+
- name: Install Dependencies
40+
# Decision: Allow poetry to generate lock file if missing
41+
# Reason: Simplifies workflow when lock file is not yet committed
42+
run: poetry install --no-interaction --no-root
43+
44+
# Decision: Use Ruff for fast linting
45+
# Reason: Ruff is significantly faster than Flake8/Black and covers both roles.
46+
- name: Run Lint (Ruff)
47+
run: poetry run ruff check .
48+
49+
# Decision: Run Pytest even if no tests exist yet (ensure setup works)
50+
# Reason: Validates that the test runner environment is correctly configured.
51+
# Security: Only tolerate exit code 5 (no tests found), fail on actual test failures
52+
- name: Run Tests
53+
run: |
54+
poetry run pytest || EXIT_CODE=$?
55+
if [ "${EXIT_CODE:-0}" -eq 5 ]; then
56+
echo "No tests found, but runner works"
57+
exit 0
58+
elif [ "${EXIT_CODE:-0}" -ne 0 ]; then
59+
echo "Tests failed with exit code $EXIT_CODE"
60+
exit $EXIT_CODE
61+
fi

.github/workflows/frontend.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Frontend CI
2+
3+
on:
4+
push:
5+
branches: ["main", "develop"]
6+
paths:
7+
- "frontend/**"
8+
- ".github/workflows/frontend.yml"
9+
pull_request:
10+
types: [opened, synchronize, reopened]
11+
paths:
12+
- "frontend/**"
13+
- ".github/workflows/frontend.yml"
14+
15+
defaults:
16+
run:
17+
working-directory: ./frontend
18+
19+
jobs:
20+
build:
21+
name: Build & Lint (Next.js)
22+
runs-on: ubuntu-latest
23+
24+
env:
25+
# Decision: Mock environment variables for CI build
26+
# Reason: Next.js build phases might access env vars.
27+
# CI should not depend on real production secrets unless necessary for e2e.
28+
NEXT_PUBLIC_API_URL: "http://localhost:8000"
29+
30+
steps:
31+
- name: Checkout code
32+
uses: actions/checkout@v4
33+
34+
- name: Set up Node.js
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: "20"
38+
cache: "npm"
39+
cache-dependency-path: frontend/package-lock.json
40+
41+
- name: Install Dependencies
42+
# Decision: Use `npm ci` instead of `npm install`
43+
# Reason: Ensures clean, reproducible installs based on lockfile.
44+
run: npm ci
45+
46+
- name: Run Lint
47+
run: npm run lint
48+
49+
- name: Run Build
50+
# This checks for type errors and build capability
51+
run: npm run build

.github/workflows/security.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Security Gatekeeper
2+
3+
on:
4+
pull_request:
5+
branches: ["main", "develop"]
6+
# Decision: Run on both main and develop branches for Git Flow compatibility
7+
# Reason: Detect security issues early (at develop) rather than late (at main)
8+
# Decision: Only run on dependency file changes to optimize CI resources
9+
# Reason: Security scans are expensive; limit to supply-chain relevant changes
10+
paths:
11+
- "backend/pyproject.toml"
12+
- "backend/poetry.lock"
13+
- "frontend/package.json"
14+
- "frontend/package-lock.json"
15+
- "backend/Dockerfile"
16+
- "frontend/Dockerfile"
17+
- "docker-compose.yml"
18+
- ".grype.yaml"
19+
- ".github/workflows/security.yml"
20+
push:
21+
branches: ["main", "develop"]
22+
paths:
23+
- "backend/pyproject.toml"
24+
- "backend/poetry.lock"
25+
- "frontend/package.json"
26+
- "frontend/package-lock.json"
27+
- "backend/Dockerfile"
28+
- "frontend/Dockerfile"
29+
- "docker-compose.yml"
30+
- ".grype.yaml"
31+
- ".github/workflows/security.yml"
32+
33+
jobs:
34+
security-check:
35+
name: Supply Chain & Secret Scan
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 5
38+
# Decision: Allow admin bypass via label for emergency situations
39+
# Reason: Critical production issues may require temporary security bypass
40+
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-security') }}
41+
42+
steps:
43+
# Decision: Fail on Critical vulnerabilities, allow exceptions via .grype.yaml
44+
# Reason: Critical CVEs pose immediate risk; exceptions require documented justification
45+
- name: Vulnerability Scan
46+
uses: anchore/scan-action@v5
47+
with:
48+
sbom: sbom.spdx.json
49+
fail-build: true
50+
severity-cutoff: critical
51+
# Decision: Support exception configuration for justified ignores
52+
# Reason: Some vulnerabilities may not apply to our use case or have no fix available
53+
only-fixed: false
54+
add-cpes-if-none: falsexed version ensures immutability
55+
- name: Secret Scan
56+
uses: trufflesecurity/trufflehog@v3.82.13
57+
with:
58+
path: ./
59+
# Decision: Use event-specific refs for proper diff scanning
60+
# Reason: github.head_ref is empty on push events; use conditional expressions
61+
base: ${{ github.event.pull_request.base.sha || github.event.before }}
62+
head: ${{ github.event.pull_request.head.sha || github.sha }}
63+
extra_args: --only-verified
64+
65+
# 2. SBOM Generation (Syft)
66+
- name: Generate SBOM
67+
uses: anchore/sbom-action@v0
68+
with:
69+
path: .
70+
format: spdx-json
71+
output-file: sbom.spdx.json
72+
73+
# 3. Vulnerability Scan (Grype) - Critical Only
74+
- name: Vulnerability Scan
75+
uses: anchore/scan-action@v5
76+
with:
77+
sbom: sbom.spdx.json
78+
fail-build: true
79+
severity-cutoff: critical # Critical以外は通す(ハッカソン用)

.grype.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Grype Vulnerability Scan Configuration
2+
# Purpose: Define exceptions for known vulnerabilities with documented justification
3+
# Decision: Allow exclusion of vulnerabilities with valid business/technical reasons
4+
# Reason: Not all CVEs apply to our context; some may have no available fix
5+
6+
# Format:
7+
# ignore:
8+
# - vulnerability: CVE-YYYY-XXXXX
9+
# fix-state: unknown|fixed|not-fixed|wont-fix
10+
# package:
11+
# name: package-name
12+
# version: version-pattern
13+
# notes: |
14+
# Justification for ignoring this vulnerability.
15+
# Example reasons:
16+
# - Not exploitable in our usage pattern
17+
# - Mitigated by other controls (WAF, network isolation)
18+
# - Awaiting upstream fix (link to issue)
19+
# - False positive (explain why)
20+
21+
# Example:
22+
# ignore:
23+
# - vulnerability: CVE-2024-12345
24+
# fix-state: not-fixed
25+
# package:
26+
# name: example-package
27+
# version: "1.2.3"
28+
# notes: |
29+
# This vulnerability affects a feature we do not use (admin panel).
30+
# Our application only uses the public API, which is not vulnerable.
31+
# Tracking upstream fix: https://github.com/example/example-package/issues/123
32+
33+
# Decision Log: All exceptions must be reviewed quarterly and removed when fixed
34+
ignore: []

backend/pyproject.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@ authors = ["Your Name <you@example.com>"]
66

77
[tool.poetry.dependencies]
88
python = "^3.13"
9-
fastapi = "*"
10-
uvicorn = {extras = ["standard"], version = "*"}
9+
# Decision: Pin dependencies to specific version ranges (not "*")
10+
# Reason: Mitigates supply-chain risk from hijacked package updates
11+
fastapi = "^0.115.0"
12+
uvicorn = {extras = ["standard"], version = "^0.32.0"}
13+
14+
[tool.poetry.group.dev.dependencies]
15+
# Decision: Pin dev dependencies to prevent CI instability
16+
# Reason: Unpinned versions can introduce breaking changes unexpectedly
17+
ruff = "^0.8.0"
18+
pytest = "^8.3.0"
1119

1220
[build-system]
1321
requires = ["poetry-core"]

0 commit comments

Comments
 (0)