@sk8metal/michi-cli 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -847
- package/dist/scripts/phase-runner.js +1 -1
- package/dist/scripts/phase-runner.js.map +1 -1
- package/dist/scripts/utils/multi-repo-validator.d.ts +18 -0
- package/dist/scripts/utils/multi-repo-validator.d.ts.map +1 -1
- package/dist/scripts/utils/multi-repo-validator.js +42 -0
- package/dist/scripts/utils/multi-repo-validator.js.map +1 -1
- package/dist/scripts/utils/tasks-format-validator.js +3 -3
- package/dist/scripts/utils/tasks-format-validator.js.map +1 -1
- package/docs/README.md +20 -83
- package/docs/getting-started/configuration.md +379 -0
- package/docs/getting-started/installation.md +59 -0
- package/docs/getting-started/quick-start.md +76 -0
- package/docs/guides/ai-tools.md +311 -0
- package/docs/guides/atlassian-integration.md +116 -0
- package/docs/guides/claude-code.md +155 -0
- package/docs/guides/multi-repo.md +117 -0
- package/docs/guides/workflow.md +382 -0
- package/docs/reference/ai-commands.md +92 -0
- package/docs/reference/cli.md +756 -0
- package/docs/reference/environment-variables.md +192 -0
- package/docs/troubleshooting.md +543 -0
- package/package.json +1 -1
- package/scripts/phase-runner.ts +1 -1
- package/scripts/utils/__tests__/multi-repo-validator.test.ts +159 -1
- package/scripts/utils/multi-repo-validator.ts +50 -0
- package/scripts/utils/tasks-format-validator.ts +3 -3
- package/templates/claude/agents/e2e-first-planner/AGENT.md +1 -1
- package/templates/claude/agents/pr-resolver/AGENT.md +15 -3
- package/templates/claude/commands/michi/e2e-plan.md +1 -1
- package/templates/claude/commands/michi/spec-design.md +2 -2
- package/templates/claude/commands/michi/spec-tasks.md +156 -0
- package/templates/claude/commands/michi/test-planning.md +1 -1
- package/templates/claude/commands/michi/validate-design.md +3 -3
- package/templates/claude/commands/michi-multi-repo/impl-all.md +30 -1
- package/templates/claude/commands/michi-multi-repo/propagate-specs.md +14 -1
- package/templates/claude/commands/michi-multi-repo/spec-review.md +16 -2
- package/templates/claude-agent/agents/repo-spec-executor.md +1 -1
- package/templates/claude-agent/commands/michi/spec-tasks.md +117 -0
- package/templates/claude-agent/rules/code-size-monitor.md +26 -0
- package/templates/claude-agent/rules/code-size-rules.md +32 -0
- package/templates/codex/AGENTS.override.md +1 -1
- package/templates/codex/rules/README.md +2 -2
- package/templates/cursor/commands/michi/spec-tasks.md +117 -0
- package/templates/michi/cc-sdd-overrides/settings/rules/design-review-michi.md +1 -1
- package/docs/context.md +0 -59
- package/docs/michi-development/contributing/development.md +0 -341
- package/docs/michi-development/contributing/release.md +0 -365
- package/docs/michi-development/design/config-unification.md +0 -733
- package/docs/michi-development/design/design-config-current-state.md +0 -330
- package/docs/michi-development/design/design-config-implementation.md +0 -628
- package/docs/michi-development/design/design-config-migration.md +0 -952
- package/docs/michi-development/design/design-config-security.md +0 -771
- package/docs/michi-development/design/design-config-solution.md +0 -583
- package/docs/michi-development/design/design-config-testing.md +0 -892
- package/docs/michi-development/testing/manual-verification-flow.md +0 -871
- package/docs/michi-development/testing/manual-verification-other-tools.md +0 -1279
- package/docs/michi-development/testing/manual-verification-troubleshooting.md +0 -122
- package/docs/michi-development/testing/pre-publish-checklist.md +0 -560
- package/docs/michi-development/testing-strategy.md +0 -87
- package/docs/plan.md +0 -275
- package/docs/user-guide/getting-started/github-token-setup.md +0 -510
- package/docs/user-guide/getting-started/new-repository-setup.md +0 -704
- package/docs/user-guide/getting-started/quick-start.md +0 -212
- package/docs/user-guide/getting-started/setup.md +0 -819
- package/docs/user-guide/guides/agent-skills-integration.md +0 -222
- package/docs/user-guide/guides/customization.md +0 -537
- package/docs/user-guide/guides/internationalization.md +0 -540
- package/docs/user-guide/guides/migration-guide.md +0 -138
- package/docs/user-guide/guides/multi-project.md +0 -368
- package/docs/user-guide/guides/multi-repo-guide.md +0 -1590
- package/docs/user-guide/guides/phase-automation.md +0 -419
- package/docs/user-guide/guides/workflow.md +0 -574
- package/docs/user-guide/hands-on/README.md +0 -142
- package/docs/user-guide/hands-on/claude-agent-setup.md +0 -597
- package/docs/user-guide/hands-on/claude-setup.md +0 -452
- package/docs/user-guide/hands-on/cursor-setup.md +0 -353
- package/docs/user-guide/hands-on/troubleshooting.md +0 -964
- package/docs/user-guide/hands-on/verification-checklist.md +0 -439
- package/docs/user-guide/hands-on/workflow-walkthrough.md +0 -1078
- package/docs/user-guide/reference/config.md +0 -589
- package/docs/user-guide/reference/multi-repo-api.md +0 -771
- package/docs/user-guide/reference/quick-reference.md +0 -297
- package/docs/user-guide/reference/security-test-payloads.md +0 -50
- package/docs/user-guide/reference/tasks-template.md +0 -550
- package/docs/user-guide/release/ci-setup-java.md +0 -114
- package/docs/user-guide/release/ci-setup-nodejs.md +0 -94
- package/docs/user-guide/release/ci-setup-php.md +0 -102
- package/docs/user-guide/release/ci-setup-troubleshooting.md +0 -94
- package/docs/user-guide/release/ci-setup.md +0 -188
- package/docs/user-guide/release/release-flow.md +0 -476
- package/docs/user-guide/templates/test-specs/README.md +0 -173
- package/docs/user-guide/templates/test-specs/e2e-test-spec-template.md +0 -553
- package/docs/user-guide/templates/test-specs/integration-test-spec-template.md +0 -435
- package/docs/user-guide/templates/test-specs/performance-test-spec-template.md +0 -454
- package/docs/user-guide/templates/test-specs/security-test-spec-template.md +0 -625
- package/docs/user-guide/templates/test-specs/unit-test-spec-template.md +0 -328
- package/docs/user-guide/testing/integration-tests.md +0 -312
- package/docs/user-guide/testing/tdd-cycle.md +0 -349
- package/docs/user-guide/testing/test-execution-flow.md +0 -396
- package/docs/user-guide/testing/test-failure-handling.md +0 -521
- package/docs/user-guide/testing/test-planning-flow.md +0 -185
- package/docs/user-guide/testing-strategy.md +0 -185
- package/docs/verification-guide.md +0 -518
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
# Michi 設定統合設計書 - セキュリティとパフォーマンス
|
|
2
|
-
|
|
3
|
-
**バージョン**: 1.0
|
|
4
|
-
**作成日**: 2025-01-11
|
|
5
|
-
**ステータス**: Draft
|
|
6
|
-
**親ドキュメント**: [config-unification.md](./config-unification.md)
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 9. セキュリティとパフォーマンス
|
|
11
|
-
|
|
12
|
-
設定統一に伴うセキュリティとパフォーマンスの考慮事項です。
|
|
13
|
-
|
|
14
|
-
### 9.1 セキュリティ対策
|
|
15
|
-
|
|
16
|
-
#### 9.1.1 認証情報の保護
|
|
17
|
-
|
|
18
|
-
**脅威モデル**
|
|
19
|
-
|
|
20
|
-
| 脅威 | 影響 | 対策 |
|
|
21
|
-
|------|------|------|
|
|
22
|
-
| **認証情報の漏洩** | 高 | ファイルパーミッション、.gitignore |
|
|
23
|
-
| **環境変数の漏洩** | 中 | .env.example の提供、警告メッセージ |
|
|
24
|
-
| **不正アクセス** | 高 | 最小権限の原則、トークンの有効期限 |
|
|
25
|
-
| **中間者攻撃** | 中 | HTTPS必須、証明書検証 |
|
|
26
|
-
|
|
27
|
-
**実装されるセキュリティ対策**
|
|
28
|
-
|
|
29
|
-
1. **ファイルパーミッションの強制**
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
// scripts/utils/security.ts
|
|
33
|
-
import { chmodSync, statSync } from 'fs';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 機密ファイルのパーミッションを強制する
|
|
37
|
-
*/
|
|
38
|
-
export function enforceSecurePermissions(filePath: string): void {
|
|
39
|
-
const stats = statSync(filePath);
|
|
40
|
-
const mode = stats.mode & 0o777;
|
|
41
|
-
|
|
42
|
-
// 600 (rw-------) 以外の場合は警告
|
|
43
|
-
if (mode !== 0o600) {
|
|
44
|
-
console.warn(`⚠️ 警告: ${filePath} のパーミッションが不適切です`);
|
|
45
|
-
console.warn(` 現在: ${mode.toString(8)}, 推奨: 600`);
|
|
46
|
-
console.warn(` 自動的に 600 に変更します...`);
|
|
47
|
-
|
|
48
|
-
chmodSync(filePath, 0o600);
|
|
49
|
-
console.log(`✅ パーミッションを 600 に変更しました`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* グローバル .env ファイルの作成時にパーミッションを設定
|
|
55
|
-
*/
|
|
56
|
-
export function createSecureEnvFile(filePath: string, content: string): void {
|
|
57
|
-
writeFileSync(filePath, content, { mode: 0o600 });
|
|
58
|
-
console.log(`✅ セキュアなファイルを作成しました: ${filePath}`);
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
2. **.gitignore の自動更新**
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// scripts/utils/gitignore.ts
|
|
66
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* .gitignore に機密ファイルを追加する
|
|
70
|
-
*/
|
|
71
|
-
export function ensureGitignore(projectDir: string): void {
|
|
72
|
-
const gitignorePath = join(projectDir, '.gitignore');
|
|
73
|
-
const requiredEntries = [
|
|
74
|
-
'.env',
|
|
75
|
-
'.env.local',
|
|
76
|
-
'~/.michi/.env',
|
|
77
|
-
'.michi-backup-*/',
|
|
78
|
-
'.michi/migration.log'
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
let content = existsSync(gitignorePath)
|
|
82
|
-
? readFileSync(gitignorePath, 'utf-8')
|
|
83
|
-
: '';
|
|
84
|
-
|
|
85
|
-
let added = false;
|
|
86
|
-
for (const entry of requiredEntries) {
|
|
87
|
-
if (!content.includes(entry)) {
|
|
88
|
-
content += `\n${entry}`;
|
|
89
|
-
added = true;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (added) {
|
|
94
|
-
writeFileSync(gitignorePath, content.trim() + '\n');
|
|
95
|
-
console.log('✅ .gitignore を更新しました');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
3. **環境変数の検証**
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// src/config/validation.ts
|
|
104
|
-
import { z } from 'zod';
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 認証情報の形式を検証する
|
|
108
|
-
*/
|
|
109
|
-
export const CredentialsSchema = z.object({
|
|
110
|
-
// Atlassian API Token (24文字の英数字)
|
|
111
|
-
confluenceApiToken: z.string()
|
|
112
|
-
.min(24, 'API token is too short')
|
|
113
|
-
.regex(/^[A-Za-z0-9]+$/, 'Invalid API token format'),
|
|
114
|
-
|
|
115
|
-
jiraApiToken: z.string()
|
|
116
|
-
.min(24, 'API token is too short')
|
|
117
|
-
.regex(/^[A-Za-z0-9]+$/, 'Invalid API token format'),
|
|
118
|
-
|
|
119
|
-
// GitHub Personal Access Token (ghp_ または ghp_classic プレフィックス)
|
|
120
|
-
githubToken: z.string()
|
|
121
|
-
.regex(
|
|
122
|
-
/^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82})$/,
|
|
123
|
-
'Invalid GitHub token format'
|
|
124
|
-
),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 機密情報が含まれていないかチェック
|
|
129
|
-
*/
|
|
130
|
-
export function detectSecretsInLogs(message: string): string {
|
|
131
|
-
// トークンのパターンにマッチする部分をマスクする
|
|
132
|
-
return message
|
|
133
|
-
.replace(/ghp_[A-Za-z0-9]{36}/g, 'ghp_****')
|
|
134
|
-
.replace(/github_pat_[A-Za-z0-9_]{82}/g, 'github_pat_****')
|
|
135
|
-
.replace(/[A-Za-z0-9]{24,}/g, (match) => {
|
|
136
|
-
// 24文字以上の英数字はトークンの可能性があるのでマスク
|
|
137
|
-
return match.substring(0, 4) + '****';
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### 9.1.2 ファイルパーミッションの管理
|
|
143
|
-
|
|
144
|
-
**パーミッション設定基準**
|
|
145
|
-
|
|
146
|
-
| ファイル | パーミッション | 理由 |
|
|
147
|
-
|---------|--------------|------|
|
|
148
|
-
| `~/.michi/.env` | 600 (rw-------) | 組織の認証情報を含む |
|
|
149
|
-
| `.env` | 600 (rw-------) | プロジェクトの認証情報を含む |
|
|
150
|
-
| `.michi/config.json` | 644 (rw-r--r--) | 認証情報を含まない |
|
|
151
|
-
| `.michi/project.json` | 644 (rw-r--r--) | 認証情報を含まない |
|
|
152
|
-
| `.michi-backup-*/*` | 700 (rwx------) | バックアップディレクトリ |
|
|
153
|
-
|
|
154
|
-
**自動チェック機構**
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
// src/config/config-loader.ts (updated)
|
|
158
|
-
export class ConfigLoader {
|
|
159
|
-
private validateFilePermissions(): void {
|
|
160
|
-
const sensitiveFiles = [
|
|
161
|
-
this.globalEnvPath,
|
|
162
|
-
join(this.projectDir, '.env')
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
for (const filePath of sensitiveFiles) {
|
|
166
|
-
if (!existsSync(filePath)) continue;
|
|
167
|
-
|
|
168
|
-
const stats = statSync(filePath);
|
|
169
|
-
const mode = stats.mode & 0o777;
|
|
170
|
-
|
|
171
|
-
if (mode > 0o600) {
|
|
172
|
-
throw new SecurityError(
|
|
173
|
-
'INSECURE_PERMISSIONS',
|
|
174
|
-
`File ${filePath} has insecure permissions: ${mode.toString(8)}`,
|
|
175
|
-
{ filePath, mode, expected: 0o600 }
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
public load(): AppConfig {
|
|
182
|
-
// パーミッションチェック
|
|
183
|
-
this.validateFilePermissions();
|
|
184
|
-
|
|
185
|
-
// 設定読み込み
|
|
186
|
-
// ...
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
#### 9.1.3 入力検証
|
|
192
|
-
|
|
193
|
-
**URL検証**
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
// src/config/validators.ts
|
|
197
|
-
import { z } from 'zod';
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Atlassian URL(Confluence/JIRA)の検証
|
|
201
|
-
*/
|
|
202
|
-
export const AtlassianUrlSchema = z.string()
|
|
203
|
-
.url('Invalid URL format')
|
|
204
|
-
.regex(
|
|
205
|
-
/^https:\/\/[a-zA-Z0-9-]+\.atlassian\.net$/,
|
|
206
|
-
'Must be a valid Atlassian Cloud URL (https://xxx.atlassian.net)'
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* GitHub リポジトリURLの検証
|
|
211
|
-
*/
|
|
212
|
-
export const GitHubRepoUrlSchema = z.string()
|
|
213
|
-
.refine(
|
|
214
|
-
(url) => {
|
|
215
|
-
const httpsPattern = /^https:\/\/github\.com\/[^/]+\/[^/]+(\.git)?$/;
|
|
216
|
-
const sshPattern = /^git@github\.com:[^/]+\/[^/]+(\.git)?$/;
|
|
217
|
-
return httpsPattern.test(url) || sshPattern.test(url);
|
|
218
|
-
},
|
|
219
|
-
'Must be a valid GitHub repository URL'
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* メールアドレスの検証
|
|
224
|
-
*/
|
|
225
|
-
export const EmailSchema = z.string()
|
|
226
|
-
.email('Invalid email format')
|
|
227
|
-
.max(254, 'Email too long');
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
**コマンドインジェクション対策**
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
// scripts/utils/exec-safe.ts
|
|
234
|
-
import { execSync } from 'child_process';
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* 安全なコマンド実行(シェルインジェクション対策)
|
|
238
|
-
*/
|
|
239
|
-
export function execSafe(
|
|
240
|
-
command: string,
|
|
241
|
-
args: string[],
|
|
242
|
-
options?: any
|
|
243
|
-
): string {
|
|
244
|
-
// コマンドと引数を分離して実行
|
|
245
|
-
// execSync の shell: false オプションを使用
|
|
246
|
-
const fullCommand = `${command} ${args.map(escapeArg).join(' ')}`;
|
|
247
|
-
|
|
248
|
-
return execSync(fullCommand, {
|
|
249
|
-
...options,
|
|
250
|
-
shell: false, // シェルを経由しない
|
|
251
|
-
encoding: 'utf-8'
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* シェル引数のエスケープ
|
|
257
|
-
*/
|
|
258
|
-
function escapeArg(arg: string): string {
|
|
259
|
-
// シングルクォートで囲み、内部のシングルクォートをエスケープ
|
|
260
|
-
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
#### 9.1.4 セキュアなデフォルト設定
|
|
265
|
-
|
|
266
|
-
**デフォルト値の設計**
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// src/config/defaults.ts
|
|
270
|
-
|
|
271
|
-
export const SECURE_DEFAULTS = {
|
|
272
|
-
// HTTPS 強制
|
|
273
|
-
confluence: {
|
|
274
|
-
useHttps: true,
|
|
275
|
-
verifySsl: true,
|
|
276
|
-
},
|
|
277
|
-
|
|
278
|
-
jira: {
|
|
279
|
-
useHttps: true,
|
|
280
|
-
verifySsl: true,
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
github: {
|
|
284
|
-
useHttps: true,
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
// API呼び出しのタイムアウト(DoS対策)
|
|
288
|
-
api: {
|
|
289
|
-
timeout: 30000, // 30秒
|
|
290
|
-
maxRetries: 3,
|
|
291
|
-
retryDelay: 1000, // 1秒
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
// ロギング
|
|
295
|
-
logging: {
|
|
296
|
-
maskSecrets: true, // 機密情報を自動マスク
|
|
297
|
-
level: 'info',
|
|
298
|
-
},
|
|
299
|
-
} as const;
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
**設定値のサニタイズ**
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
// src/config/sanitize.ts
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* ログ出力前に機密情報をマスクする
|
|
309
|
-
*/
|
|
310
|
-
export function sanitizeForLog(config: AppConfig): AppConfig {
|
|
311
|
-
return {
|
|
312
|
-
...config,
|
|
313
|
-
confluence: config.confluence ? {
|
|
314
|
-
...config.confluence,
|
|
315
|
-
apiToken: '****',
|
|
316
|
-
password: '****',
|
|
317
|
-
} : undefined,
|
|
318
|
-
jira: config.jira ? {
|
|
319
|
-
...config.jira,
|
|
320
|
-
apiToken: '****',
|
|
321
|
-
password: '****',
|
|
322
|
-
} : undefined,
|
|
323
|
-
github: config.github ? {
|
|
324
|
-
...config.github,
|
|
325
|
-
token: config.github.token?.substring(0, 7) + '****',
|
|
326
|
-
} : undefined,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### 9.2 パフォーマンス最適化
|
|
332
|
-
|
|
333
|
-
#### 9.2.1 設定読み込みの最適化
|
|
334
|
-
|
|
335
|
-
**問題点**
|
|
336
|
-
|
|
337
|
-
- 毎回ファイルを読み込むとI/Oコストが高い
|
|
338
|
-
- 複数のコマンド実行時に同じファイルを何度も読む
|
|
339
|
-
- Zodバリデーションが重い
|
|
340
|
-
|
|
341
|
-
**解決策: キャッシュ機構**
|
|
342
|
-
|
|
343
|
-
```typescript
|
|
344
|
-
// src/config/config-loader.ts (updated)
|
|
345
|
-
|
|
346
|
-
export class ConfigLoader {
|
|
347
|
-
private static cache: Map<string, {
|
|
348
|
-
config: AppConfig;
|
|
349
|
-
timestamp: number;
|
|
350
|
-
hash: string;
|
|
351
|
-
}> = new Map();
|
|
352
|
-
|
|
353
|
-
private static CACHE_TTL = 60000; // 1分
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* ファイルのハッシュ値を計算
|
|
357
|
-
*/
|
|
358
|
-
private getFileHash(filePath: string): string {
|
|
359
|
-
if (!existsSync(filePath)) return '';
|
|
360
|
-
|
|
361
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
362
|
-
return createHash('sha256').update(content).digest('hex');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* キャッシュキーを生成
|
|
367
|
-
*/
|
|
368
|
-
private getCacheKey(): string {
|
|
369
|
-
const globalHash = this.getFileHash(this.globalEnvPath);
|
|
370
|
-
const projectConfigHash = this.getFileHash(this.projectConfigPath);
|
|
371
|
-
const projectEnvHash = this.getFileHash(this.projectEnvPath);
|
|
372
|
-
|
|
373
|
-
return `${globalHash}:${projectConfigHash}:${projectEnvHash}`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* キャッシュから設定を取得
|
|
378
|
-
*/
|
|
379
|
-
public load(options: { noCache?: boolean } = {}): AppConfig {
|
|
380
|
-
const cacheKey = this.getCacheKey();
|
|
381
|
-
const now = Date.now();
|
|
382
|
-
|
|
383
|
-
// キャッシュチェック
|
|
384
|
-
if (!options.noCache) {
|
|
385
|
-
const cached = ConfigLoader.cache.get(cacheKey);
|
|
386
|
-
if (cached && now - cached.timestamp < ConfigLoader.CACHE_TTL) {
|
|
387
|
-
return cached.config;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// 設定を読み込み
|
|
392
|
-
const config = this.loadInternal();
|
|
393
|
-
|
|
394
|
-
// キャッシュに保存
|
|
395
|
-
ConfigLoader.cache.set(cacheKey, {
|
|
396
|
-
config,
|
|
397
|
-
timestamp: now,
|
|
398
|
-
hash: cacheKey
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
return config;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* キャッシュをクリア
|
|
406
|
-
*/
|
|
407
|
-
public static clearCache(): void {
|
|
408
|
-
ConfigLoader.cache.clear();
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
**ベンチマーク目標**
|
|
414
|
-
|
|
415
|
-
| 操作 | 目標時間 | 現在値 |
|
|
416
|
-
|------|---------|--------|
|
|
417
|
-
| 初回読み込み | < 100ms | TBD |
|
|
418
|
-
| キャッシュヒット | < 10ms | TBD |
|
|
419
|
-
| バリデーション | < 50ms | TBD |
|
|
420
|
-
| マージ処理 | < 20ms | TBD |
|
|
421
|
-
|
|
422
|
-
#### 9.2.2 メモリ使用量
|
|
423
|
-
|
|
424
|
-
**メモリプロファイリング**
|
|
425
|
-
|
|
426
|
-
```typescript
|
|
427
|
-
// scripts/benchmark/memory-profile.ts
|
|
428
|
-
import { ConfigLoader } from '../src/config/config-loader.js';
|
|
429
|
-
|
|
430
|
-
function measureMemoryUsage() {
|
|
431
|
-
const before = process.memoryUsage();
|
|
432
|
-
|
|
433
|
-
// 100回設定を読み込む
|
|
434
|
-
for (let i = 0; i < 100; i++) {
|
|
435
|
-
const loader = new ConfigLoader(process.cwd());
|
|
436
|
-
loader.load();
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const after = process.memoryUsage();
|
|
440
|
-
|
|
441
|
-
console.log('Memory Usage:');
|
|
442
|
-
console.log(` Heap Used: ${(after.heapUsed - before.heapUsed) / 1024 / 1024}MB`);
|
|
443
|
-
console.log(` External: ${(after.external - before.external) / 1024 / 1024}MB`);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
measureMemoryUsage();
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**最適化目標**
|
|
450
|
-
|
|
451
|
-
- 1回の設定読み込み: < 5MB
|
|
452
|
-
- キャッシュ保持: < 10MB (100エントリ)
|
|
453
|
-
- ピークメモリ: < 50MB
|
|
454
|
-
|
|
455
|
-
#### 9.2.3 キャッシュ戦略
|
|
456
|
-
|
|
457
|
-
**多層キャッシュ**
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
// src/config/cache-strategy.ts
|
|
461
|
-
|
|
462
|
-
interface CacheStrategy {
|
|
463
|
-
get(key: string): AppConfig | null;
|
|
464
|
-
set(key: string, value: AppConfig): void;
|
|
465
|
-
invalidate(key: string): void;
|
|
466
|
-
clear(): void;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* LRU(Least Recently Used)キャッシュ
|
|
471
|
-
*/
|
|
472
|
-
class LRUCache implements CacheStrategy {
|
|
473
|
-
private cache: Map<string, { value: AppConfig; timestamp: number }>;
|
|
474
|
-
private maxSize: number;
|
|
475
|
-
|
|
476
|
-
constructor(maxSize: number = 100) {
|
|
477
|
-
this.cache = new Map();
|
|
478
|
-
this.maxSize = maxSize;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
get(key: string): AppConfig | null {
|
|
482
|
-
const entry = this.cache.get(key);
|
|
483
|
-
if (!entry) return null;
|
|
484
|
-
|
|
485
|
-
// アクセスされたエントリを最後に移動(LRU)
|
|
486
|
-
this.cache.delete(key);
|
|
487
|
-
this.cache.set(key, entry);
|
|
488
|
-
|
|
489
|
-
return entry.value;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
set(key: string, value: AppConfig): void {
|
|
493
|
-
// サイズ制限チェック
|
|
494
|
-
if (this.cache.size >= this.maxSize) {
|
|
495
|
-
// 最も古いエントリを削除
|
|
496
|
-
const oldestKey = this.cache.keys().next().value;
|
|
497
|
-
this.cache.delete(oldestKey);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
this.cache.set(key, {
|
|
501
|
-
value,
|
|
502
|
-
timestamp: Date.now()
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
invalidate(key: string): void {
|
|
507
|
-
this.cache.delete(key);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
clear(): void {
|
|
511
|
-
this.cache.clear();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* TTL(Time To Live)付きキャッシュ
|
|
517
|
-
*/
|
|
518
|
-
class TTLCache implements CacheStrategy {
|
|
519
|
-
private cache: Map<string, { value: AppConfig; expiry: number }>;
|
|
520
|
-
private ttl: number;
|
|
521
|
-
|
|
522
|
-
constructor(ttl: number = 60000) {
|
|
523
|
-
this.cache = new Map();
|
|
524
|
-
this.ttl = ttl;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
get(key: string): AppConfig | null {
|
|
528
|
-
const entry = this.cache.get(key);
|
|
529
|
-
if (!entry) return null;
|
|
530
|
-
|
|
531
|
-
// 有効期限チェック
|
|
532
|
-
if (Date.now() > entry.expiry) {
|
|
533
|
-
this.cache.delete(key);
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
return entry.value;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
set(key: string, value: AppConfig): void {
|
|
541
|
-
this.cache.set(key, {
|
|
542
|
-
value,
|
|
543
|
-
expiry: Date.now() + this.ttl
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
invalidate(key: string): void {
|
|
548
|
-
this.cache.delete(key);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
clear(): void {
|
|
552
|
-
this.cache.clear();
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
### 9.3 監査とロギング
|
|
558
|
-
|
|
559
|
-
#### 9.3.1 設定変更の監査ログ
|
|
560
|
-
|
|
561
|
-
```typescript
|
|
562
|
-
// src/config/audit-log.ts
|
|
563
|
-
import { writeFileSync, appendFileSync, existsSync } from 'fs';
|
|
564
|
-
import { join } from 'path';
|
|
565
|
-
|
|
566
|
-
interface AuditLogEntry {
|
|
567
|
-
timestamp: string;
|
|
568
|
-
user: string;
|
|
569
|
-
action: string;
|
|
570
|
-
target: string;
|
|
571
|
-
changes: Record<string, { old: any; new: any }>;
|
|
572
|
-
success: boolean;
|
|
573
|
-
error?: string;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
export class AuditLogger {
|
|
577
|
-
private logPath: string;
|
|
578
|
-
|
|
579
|
-
constructor(projectDir: string) {
|
|
580
|
-
this.logPath = join(projectDir, '.michi', 'audit.log');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* 設定変更を記録
|
|
585
|
-
*/
|
|
586
|
-
public logConfigChange(
|
|
587
|
-
action: string,
|
|
588
|
-
target: string,
|
|
589
|
-
changes: Record<string, { old: any; new: any }>,
|
|
590
|
-
success: boolean,
|
|
591
|
-
error?: string
|
|
592
|
-
): void {
|
|
593
|
-
const entry: AuditLogEntry = {
|
|
594
|
-
timestamp: new Date().toISOString(),
|
|
595
|
-
user: process.env.USER || 'unknown',
|
|
596
|
-
action,
|
|
597
|
-
target,
|
|
598
|
-
changes: this.sanitizeChanges(changes),
|
|
599
|
-
success,
|
|
600
|
-
error
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
const logLine = JSON.stringify(entry) + '\n';
|
|
604
|
-
|
|
605
|
-
if (!existsSync(this.logPath)) {
|
|
606
|
-
writeFileSync(this.logPath, logLine, { mode: 0o600 });
|
|
607
|
-
} else {
|
|
608
|
-
appendFileSync(this.logPath, logLine);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* 機密情報をマスク
|
|
614
|
-
*/
|
|
615
|
-
private sanitizeChanges(
|
|
616
|
-
changes: Record<string, { old: any; new: any }>
|
|
617
|
-
): Record<string, { old: any; new: any }> {
|
|
618
|
-
const sensitiveKeys = ['apiToken', 'token', 'password', 'secret'];
|
|
619
|
-
|
|
620
|
-
const sanitized: Record<string, { old: any; new: any }> = {};
|
|
621
|
-
|
|
622
|
-
for (const [key, value] of Object.entries(changes)) {
|
|
623
|
-
if (sensitiveKeys.some(k => key.toLowerCase().includes(k))) {
|
|
624
|
-
sanitized[key] = {
|
|
625
|
-
old: '****',
|
|
626
|
-
new: '****'
|
|
627
|
-
};
|
|
628
|
-
} else {
|
|
629
|
-
sanitized[key] = value;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return sanitized;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* 監査ログを取得
|
|
638
|
-
*/
|
|
639
|
-
public getAuditLog(limit?: number): AuditLogEntry[] {
|
|
640
|
-
if (!existsSync(this.logPath)) return [];
|
|
641
|
-
|
|
642
|
-
const content = readFileSync(this.logPath, 'utf-8');
|
|
643
|
-
const lines = content.trim().split('\n');
|
|
644
|
-
|
|
645
|
-
const entries = lines
|
|
646
|
-
.map(line => {
|
|
647
|
-
try {
|
|
648
|
-
return JSON.parse(line) as AuditLogEntry;
|
|
649
|
-
} catch {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
})
|
|
653
|
-
.filter((entry): entry is AuditLogEntry => entry !== null);
|
|
654
|
-
|
|
655
|
-
if (limit) {
|
|
656
|
-
return entries.slice(-limit);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return entries;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
**使用例**
|
|
665
|
-
|
|
666
|
-
```typescript
|
|
667
|
-
// michi migrate コマンド内
|
|
668
|
-
const auditLogger = new AuditLogger(projectDir);
|
|
669
|
-
|
|
670
|
-
try {
|
|
671
|
-
// 移行前の設定を記録
|
|
672
|
-
const oldConfig = readOldConfig();
|
|
673
|
-
const newConfig = performMigration();
|
|
674
|
-
|
|
675
|
-
// 変更内容を計算
|
|
676
|
-
const changes = calculateChanges(oldConfig, newConfig);
|
|
677
|
-
|
|
678
|
-
// 成功をログ
|
|
679
|
-
auditLogger.logConfigChange(
|
|
680
|
-
'migrate',
|
|
681
|
-
'~/.michi/.env',
|
|
682
|
-
changes,
|
|
683
|
-
true
|
|
684
|
-
);
|
|
685
|
-
} catch (error) {
|
|
686
|
-
// 失敗をログ
|
|
687
|
-
auditLogger.logConfigChange(
|
|
688
|
-
'migrate',
|
|
689
|
-
'~/.michi/.env',
|
|
690
|
-
{},
|
|
691
|
-
false,
|
|
692
|
-
error.message
|
|
693
|
-
);
|
|
694
|
-
throw error;
|
|
695
|
-
}
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
#### 9.3.2 セキュリティイベントの記録
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
// src/config/security-logger.ts
|
|
702
|
-
|
|
703
|
-
export class SecurityLogger {
|
|
704
|
-
/**
|
|
705
|
-
* 不正なアクセス試行を記録
|
|
706
|
-
*/
|
|
707
|
-
public logUnauthorizedAccess(
|
|
708
|
-
resource: string,
|
|
709
|
-
reason: string
|
|
710
|
-
): void {
|
|
711
|
-
const event = {
|
|
712
|
-
timestamp: new Date().toISOString(),
|
|
713
|
-
type: 'UNAUTHORIZED_ACCESS',
|
|
714
|
-
resource,
|
|
715
|
-
reason,
|
|
716
|
-
user: process.env.USER,
|
|
717
|
-
pid: process.pid
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
console.error('🚨 セキュリティイベント:', JSON.stringify(event));
|
|
721
|
-
|
|
722
|
-
// セキュリティログに記録
|
|
723
|
-
this.appendToSecurityLog(event);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* 機密ファイルへのアクセスを記録
|
|
728
|
-
*/
|
|
729
|
-
public logSensitiveFileAccess(
|
|
730
|
-
filePath: string,
|
|
731
|
-
operation: 'read' | 'write'
|
|
732
|
-
): void {
|
|
733
|
-
const event = {
|
|
734
|
-
timestamp: new Date().toISOString(),
|
|
735
|
-
type: 'SENSITIVE_FILE_ACCESS',
|
|
736
|
-
filePath,
|
|
737
|
-
operation,
|
|
738
|
-
user: process.env.USER,
|
|
739
|
-
pid: process.pid
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
this.appendToSecurityLog(event);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
private appendToSecurityLog(event: any): void {
|
|
746
|
-
const logPath = join(os.homedir(), '.michi', 'security.log');
|
|
747
|
-
const logLine = JSON.stringify(event) + '\n';
|
|
748
|
-
|
|
749
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
750
|
-
appendFileSync(logPath, logLine, { mode: 0o600 });
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
### 9.4 セキュリティチェックリスト
|
|
756
|
-
|
|
757
|
-
リリース前に確認すべき項目:
|
|
758
|
-
|
|
759
|
-
- [ ] すべての機密ファイルが .gitignore に追加されている
|
|
760
|
-
- [ ] ファイルパーミッションが適切(600 for .env files)
|
|
761
|
-
- [ ] Zodバリデーションがすべての入力に適用されている
|
|
762
|
-
- [ ] ログに機密情報が含まれていない
|
|
763
|
-
- [ ] HTTPS が強制されている
|
|
764
|
-
- [ ] エラーメッセージに内部情報が含まれていない
|
|
765
|
-
- [ ] セキュリティテストが全てパスしている
|
|
766
|
-
- [ ] 監査ログが正しく記録されている
|
|
767
|
-
- [ ] セキュリティドキュメントが最新である
|
|
768
|
-
- [ ] 脆弱性スキャンを実行している(npm audit)
|
|
769
|
-
|
|
770
|
-
---
|
|
771
|
-
|