archrisk-engine 1.0.0 → 1.0.1
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/dist/repoAnalyzer.d.ts +6 -1
- package/dist/repoAnalyzer.js +172 -99
- package/package.json +1 -1
- package/src/repoAnalyzer.ts +185 -97
package/dist/repoAnalyzer.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface RepoAnalysisResult {
|
|
|
2
2
|
score: number;
|
|
3
3
|
status: 'Ready for Production' | 'Needs Attention' | 'Not Ready for Deployment';
|
|
4
4
|
findings: {
|
|
5
|
+
id: string;
|
|
5
6
|
title: string;
|
|
6
7
|
file: string;
|
|
7
8
|
line: number;
|
|
@@ -30,7 +31,11 @@ export interface RepoAnalysisResult {
|
|
|
30
31
|
/**
|
|
31
32
|
* 3-Tier Business Audit Translation
|
|
32
33
|
*/
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* 3-Tier Business Audit Translation with Standard IDs
|
|
36
|
+
*/
|
|
37
|
+
export declare function getAuditDetails(id: string, type: string, issue: string): {
|
|
38
|
+
id: string;
|
|
34
39
|
title: string;
|
|
35
40
|
category: RepoAnalysisResult['findings'][0]['category'];
|
|
36
41
|
evidence: string;
|
package/dist/repoAnalyzer.js
CHANGED
|
@@ -46,88 +46,144 @@ const archScanner_js_1 = require("./archScanner.js");
|
|
|
46
46
|
/**
|
|
47
47
|
* 3-Tier Business Audit Translation
|
|
48
48
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 3-Tier Business Audit Translation with Standard IDs
|
|
51
|
+
*/
|
|
52
|
+
function getAuditDetails(id, type, issue) {
|
|
53
|
+
const commonFields = { id };
|
|
54
|
+
if (id === 'RR-SEC-001' || type === 'SecurityRisk') {
|
|
51
55
|
return {
|
|
56
|
+
...commonFields,
|
|
52
57
|
title: "보안 취약점 위험 (Security Vulnerability)",
|
|
53
58
|
category: 'Security',
|
|
54
59
|
evidence: issue,
|
|
55
60
|
standard: "OWASP Top 10 A03:2021 – Injection",
|
|
56
|
-
impact: "외부 공격자가 시스템 권한을 탈취하거나 민감 정보를 유출할 수 있는
|
|
57
|
-
action:
|
|
61
|
+
impact: "외부 공격자가 시스템 권한을 탈취하거나 민감 정보를 유출할 수 있는 조건이 형성됩니다.",
|
|
62
|
+
action: `
|
|
63
|
+
# Action: 격리 및 환경변수 사용
|
|
64
|
+
subprocess.run(..., shell=False) # 권장
|
|
65
|
+
# 또는 .env 파일 사용
|
|
66
|
+
import os
|
|
67
|
+
SECRET = os.getenv('MY_SECRET')
|
|
68
|
+
`,
|
|
58
69
|
reference: "https://docs.python.org/3/library/subprocess.html#security-considerations",
|
|
59
|
-
whenItMatters: "배포 즉시 자동화된 스캐너나 공격자에 의해 탐지될 수
|
|
70
|
+
whenItMatters: "배포 즉시 자동화된 스캐너나 공격자에 의해 탐지될 수 있습니다."
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
|
-
if (
|
|
63
|
-
if (issue.includes('테스트')) {
|
|
64
|
-
return {
|
|
65
|
-
title: "자동화 테스트 부재 (Missing Automated Tests)",
|
|
66
|
-
category: 'Service Interruption',
|
|
67
|
-
evidence: "tests/ 디렉토리 또는 pytest/unittest 관련 설정을 찾을 수 없습니다.",
|
|
68
|
-
standard: "Stability Standard: Code Coverage > 70%",
|
|
69
|
-
impact: "수동 테스트에 의존하게 되어 코드 변경 시마다 예상치 못한 장애가 운영 환경에 배포될 위험이 높으며, 장애 복구 시간(MTTR)이 매우 길어집니다.",
|
|
70
|
-
action: "pytest를 설치하고 핵심 로직에 대한 Smoke Test 3개를 먼저 작성하세요.",
|
|
71
|
-
reference: "https://docs.pytest.org/en/7.1.x/getting-started.html",
|
|
72
|
-
whenItMatters: "코드 변경 주기가 빨라지거나 팀원이 2명 이상으로 늘어날 때 병목이 발생합니다."
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
if (issue.includes('CI')) {
|
|
76
|
-
return {
|
|
77
|
-
title: "배포 자동화 파이프라인 부재 (Missing CI Pipeline)",
|
|
78
|
-
category: 'Service Interruption',
|
|
79
|
-
evidence: "GitHub Actions 또는 CI 도구 워크플로우 설정을 발견할 수 없습니다.",
|
|
80
|
-
standard: "Modern DevOps Standard: Continuous Integration",
|
|
81
|
-
impact: "코드 머지 전 검증 자동화가 불가능하여, 사람이 개입하는 과정에서 실수가 반복됩니다. 이는 배포 안정성을 근본적으로 파악할 수 없게 만듭니다.",
|
|
82
|
-
action: ".github/workflows/main.yml 파일을 생성하고 자동 테스트 프로세스를 구축하세요.",
|
|
83
|
-
reference: "https://github.com/features/actions",
|
|
84
|
-
whenItMatters: "하루에 2회 이상 배포를 시도할 때 검증 누락으로 인한 사고 확률이 80%를 상회하게 됩니다."
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (issue.includes('Docker')) {
|
|
88
|
-
return {
|
|
89
|
-
title: "컨테이너화 구성 누락 (Environment Inconsistency)",
|
|
90
|
-
category: 'Service Interruption',
|
|
91
|
-
evidence: "Dockerfile 또는 docker-compose.yml 파일이 존재하지 않습니다.",
|
|
92
|
-
standard: "Cloud Native Standard: Immutable Infrastructure",
|
|
93
|
-
impact: "로컬 개발 환경과 실제 운영 환경의 버전 차이로 인해 '내 컴퓨터에선 되는데 서버에선 안 되는' 예측 불가능한 장애가 발생합니다.",
|
|
94
|
-
action: "Dockerfile을 작성하여 환경 설정을 컨테이너화하세요.",
|
|
95
|
-
reference: "https://docs.docker.com/engine/reference/builder/",
|
|
96
|
-
whenItMatters: "신규 개발자가 팀에 합류하거나 클라우드 인프라 확장이 필요할 때 시간 낭비가 심화됩니다."
|
|
97
|
-
};
|
|
98
|
-
}
|
|
73
|
+
if (id === 'RR-TEST-001') {
|
|
99
74
|
return {
|
|
100
|
-
|
|
75
|
+
...commonFields,
|
|
76
|
+
title: "자동화 테스트 부재 (Missing Automated Tests)",
|
|
101
77
|
category: 'Service Interruption',
|
|
102
|
-
evidence:
|
|
103
|
-
standard: "
|
|
104
|
-
impact: "
|
|
105
|
-
action:
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
evidence: "tests/ 디렉토리 또는 pytest/unittest 관련 설정을 찾을 수 없습니다.",
|
|
79
|
+
standard: "pytest Framework Documentation",
|
|
80
|
+
impact: "코드 변경 시 기존 기능이 파괴되었는지 확인할 방법이 없어, 배포 후 장애 발생 확률이 높아집니다.",
|
|
81
|
+
action: `
|
|
82
|
+
# Action: Create tests/test_smoke.py
|
|
83
|
+
def test_health_check():
|
|
84
|
+
assert True # Basic sanity check
|
|
85
|
+
`,
|
|
86
|
+
reference: "https://docs.pytest.org/",
|
|
87
|
+
whenItMatters: "팀원이 2명 이상으로 늘어나거나 배포 주기가 빨라질 때."
|
|
108
88
|
};
|
|
109
89
|
}
|
|
110
|
-
if (
|
|
90
|
+
if (id === 'RR-CI-001') {
|
|
111
91
|
return {
|
|
92
|
+
...commonFields,
|
|
93
|
+
title: "배포 자동화 파이프라인 부재 (Missing CI Pipeline)",
|
|
94
|
+
category: 'Service Interruption',
|
|
95
|
+
evidence: "GitHub Actions (.github/workflows/*.yml) 또는 CI 설정 파일이 없습니다.",
|
|
96
|
+
standard: "GitHub Actions Documentation",
|
|
97
|
+
impact: "사람의 수동 배포 과정에서 실수가 발생할 수 있으며, 일관된 배포 상태를 보장할 수 없습니다.",
|
|
98
|
+
action: `
|
|
99
|
+
# Action: Create .github/workflows/ci.yml
|
|
100
|
+
name: CI
|
|
101
|
+
on: [push]
|
|
102
|
+
jobs:
|
|
103
|
+
test:
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
steps:
|
|
106
|
+
- uses: actions/checkout@v3
|
|
107
|
+
- run: npm test
|
|
108
|
+
`,
|
|
109
|
+
reference: "https://docs.github.com/en/actions",
|
|
110
|
+
whenItMatters: "배포 빈도가 주 2회 이상으로 증가할 때."
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (id === 'RR-OPS-001') {
|
|
114
|
+
return {
|
|
115
|
+
...commonFields,
|
|
116
|
+
title: "운영 기본 위생 체크 실패 (Project Hygiene)",
|
|
117
|
+
category: 'Service Interruption',
|
|
118
|
+
evidence: issue, // Consolidated list will be passed here
|
|
119
|
+
standard: "12-Factor App / Docker Documentation",
|
|
120
|
+
impact: "개발 환경과 운영 환경의 불일치로 인해 '내 컴퓨터에서는 되는데 서버에서는 안 되는' 문제가 발생합니다.",
|
|
121
|
+
action: `
|
|
122
|
+
# Checklist to Fix:
|
|
123
|
+
1. Create 'Dockerfile'
|
|
124
|
+
2. Create '.gitignore' (use gitignore.io)
|
|
125
|
+
3. Create 'requirements.txt' or 'package.json'
|
|
126
|
+
4. Create '.env.example'
|
|
127
|
+
`,
|
|
128
|
+
reference: "https://12factor.net/",
|
|
129
|
+
whenItMatters: "신규 입사자 온보딩 또는 서버 이관 시."
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (id === 'RR-LOG-001') {
|
|
133
|
+
return {
|
|
134
|
+
...commonFields,
|
|
135
|
+
title: "로깅 설정 미흡 (Insufficient Logging)",
|
|
136
|
+
category: 'Maintenance',
|
|
137
|
+
evidence: "코드 내에서 로깅 설정(logging, loguru 등)이 발견되지 않았습니다.",
|
|
138
|
+
standard: "Python Logging Cookbook",
|
|
139
|
+
impact: "장애 발생 시 원인을 추적할 수 있는 데이터가 없어 해결 시간이 길어집니다.",
|
|
140
|
+
action: `
|
|
141
|
+
# Action: Python Logging Setup
|
|
142
|
+
import logging
|
|
143
|
+
logging.basicConfig(level=logging.INFO)
|
|
144
|
+
logger = logging.getLogger(__name__)
|
|
145
|
+
logger.info("Server started")
|
|
146
|
+
`,
|
|
147
|
+
reference: "https://docs.python.org/3/howto/logging-cookbook.html",
|
|
148
|
+
whenItMatters: "운영 중 알 수 없는 500 에러가 발생했을 때."
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (id === 'RR-DEP-001' || type === 'CircularDependency') {
|
|
152
|
+
return {
|
|
153
|
+
...commonFields,
|
|
112
154
|
title: "구조적 의존성 결함 (Structural Dependency Issue)",
|
|
113
155
|
category: 'Scalability',
|
|
114
|
-
evidence:
|
|
156
|
+
evidence: issue,
|
|
115
157
|
standard: "Clean Architecture: Dependency Rule",
|
|
116
|
-
impact: "모듈 간 결합도가 높아져
|
|
117
|
-
action: "
|
|
118
|
-
reference: "https://
|
|
119
|
-
whenItMatters: "프로젝트
|
|
158
|
+
impact: "모듈 간 결합도가 높아져 유지보수가 어려워지고, 사이드 이펙트가 발생하기 쉽습니다.",
|
|
159
|
+
action: "상호 참조하는 모듈을 분리하거나 공통 모듈로 추출하세요.",
|
|
160
|
+
reference: "https://refactoring.guru/design-patterns",
|
|
161
|
+
whenItMatters: "프로젝트 규모가 커질수록 리팩토링 비용이 기하급수적으로 증가합니다."
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (id === 'RR-LINT-001' || type === 'GodModule') {
|
|
165
|
+
return {
|
|
166
|
+
...commonFields,
|
|
167
|
+
title: "거대 모듈 감지 (God Module)",
|
|
168
|
+
category: 'Maintenance',
|
|
169
|
+
evidence: issue,
|
|
170
|
+
standard: "Clean Code: Functions",
|
|
171
|
+
impact: "단일 파일의 책임이 과도하여 변경 시 영향 범위를 예측하기 어렵습니다.",
|
|
172
|
+
action: "책임에 따라 파일을 분리하세요 (Separation of Concerns).",
|
|
173
|
+
reference: "https://pypi.org/project/flake8/",
|
|
174
|
+
whenItMatters: "기능 추가 시마다 버그가 발생할 때."
|
|
120
175
|
};
|
|
121
176
|
}
|
|
122
177
|
return {
|
|
178
|
+
...commonFields,
|
|
123
179
|
title: "기타 잠재적 리스크 (Other Potential Risks)",
|
|
124
180
|
category: 'Maintenance',
|
|
125
181
|
evidence: issue,
|
|
126
182
|
standard: "General Coding Best Practices",
|
|
127
|
-
impact: "
|
|
128
|
-
action: "
|
|
183
|
+
impact: "잠재적인 버그나 유지보수 어려움이 있을 수 있습니다.",
|
|
184
|
+
action: "해당 코드를 리뷰하고 리팩토링을 고려하세요.",
|
|
129
185
|
reference: "#",
|
|
130
|
-
whenItMatters: "지속적인
|
|
186
|
+
whenItMatters: "지속적인 코드 품질 저하가 우려될 때."
|
|
131
187
|
};
|
|
132
188
|
}
|
|
133
189
|
async function analyzeRepository(repoPath, resultsDir) {
|
|
@@ -158,44 +214,61 @@ async function analyzeRepository(repoPath, resultsDir) {
|
|
|
158
214
|
let criticalCount = 0;
|
|
159
215
|
let operationalGapCount = 0;
|
|
160
216
|
// 1. Operational Audit (Critical -30 each)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
else {
|
|
176
|
-
const allFiles = fs.readdirSync(repoPath);
|
|
177
|
-
found = allFiles.some(f => check.pattern.test(f.toLowerCase()));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch (e) {
|
|
181
|
-
found = false;
|
|
182
|
-
}
|
|
183
|
-
if (!found) {
|
|
184
|
-
const details = getAuditDetails('ProductionRisk', check.message);
|
|
185
|
-
findings.push({
|
|
186
|
-
file: 'Repository Root',
|
|
187
|
-
line: 0,
|
|
188
|
-
type: 'ProductionRisk',
|
|
189
|
-
...details
|
|
190
|
-
});
|
|
191
|
-
operationalGapCount++;
|
|
192
|
-
}
|
|
217
|
+
// 1. Operational Audit & Consolidation
|
|
218
|
+
const hygieneMissing = [];
|
|
219
|
+
// Check Tests
|
|
220
|
+
const hasTests = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
221
|
+
.some(d => d.isDirectory() && /tests?|spec/i.test(d.name));
|
|
222
|
+
if (!hasTests) {
|
|
223
|
+
const details = getAuditDetails('RR-TEST-001', 'ProductionRisk', '');
|
|
224
|
+
findings.push({
|
|
225
|
+
file: 'Repository Root',
|
|
226
|
+
line: 0,
|
|
227
|
+
type: 'ProductionRisk',
|
|
228
|
+
...details
|
|
229
|
+
});
|
|
230
|
+
operationalGapCount++;
|
|
193
231
|
}
|
|
194
|
-
//
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
// Check CI
|
|
233
|
+
const hasCI = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
234
|
+
.some(d => d.isDirectory() && /(\.github|\.circleci|jenkins|gitlab)/i.test(d.name));
|
|
235
|
+
if (!hasCI) {
|
|
236
|
+
const details = getAuditDetails('RR-CI-001', 'ProductionRisk', '');
|
|
237
|
+
findings.push({
|
|
238
|
+
file: 'Repository Root',
|
|
239
|
+
line: 0,
|
|
240
|
+
type: 'ProductionRisk',
|
|
241
|
+
...details
|
|
242
|
+
});
|
|
243
|
+
operationalGapCount++;
|
|
244
|
+
}
|
|
245
|
+
// Check Hygiene (Docker, Pinning, Env, GitIgnore)
|
|
246
|
+
const allFiles = fs.readdirSync(repoPath);
|
|
247
|
+
// Docker
|
|
248
|
+
if (!allFiles.some(f => /Dockerfile|docker-compose/i.test(f))) {
|
|
249
|
+
hygieneMissing.push("✘ Dockerfile or docker-compose.yml missing");
|
|
250
|
+
}
|
|
251
|
+
// Pinning
|
|
252
|
+
if (!allFiles.some(f => /requirements\.txt|poetry\.lock|pyproject\.toml|package-lock\.json|pnpm-lock\.yaml/i.test(f))) {
|
|
253
|
+
hygieneMissing.push("✘ Dependency Lockfile (requirements.txt, package-lock.json, etc) missing");
|
|
254
|
+
}
|
|
255
|
+
// Env
|
|
256
|
+
if (!allFiles.some(f => /\.env\.example|\.env\.sample/i.test(f))) {
|
|
257
|
+
hygieneMissing.push("✘ .env.example (Safe environment template) missing");
|
|
258
|
+
}
|
|
259
|
+
// GitIgnore
|
|
260
|
+
if (!fs.existsSync(path.join(repoPath, '.gitignore'))) {
|
|
261
|
+
hygieneMissing.push("✘ .gitignore missing");
|
|
262
|
+
}
|
|
263
|
+
if (hygieneMissing.length > 0) {
|
|
264
|
+
const evidenceBlock = "\n" + hygieneMissing.join("\n");
|
|
265
|
+
const details = getAuditDetails('RR-OPS-001', 'ProductionRisk', evidenceBlock);
|
|
266
|
+
findings.push({
|
|
267
|
+
file: 'Repository Root',
|
|
268
|
+
line: 0,
|
|
269
|
+
type: 'ProductionRisk',
|
|
270
|
+
...details
|
|
271
|
+
});
|
|
199
272
|
operationalGapCount++;
|
|
200
273
|
}
|
|
201
274
|
let hasLogging = false;
|
|
@@ -211,13 +284,13 @@ async function analyzeRepository(repoPath, resultsDir) {
|
|
|
211
284
|
}
|
|
212
285
|
// God Module Check (>500 lines)
|
|
213
286
|
if (lines.length > 500) {
|
|
214
|
-
const details = getAuditDetails('
|
|
287
|
+
const details = getAuditDetails('RR-LINT-001', 'GodModule', `File length: ${lines.length} lines`);
|
|
215
288
|
findings.push({ file: relativePath, line: 0, type: 'ProductionRisk', ...details });
|
|
216
289
|
operationalGapCount++;
|
|
217
290
|
}
|
|
218
291
|
const result = await (0, analyzer_js_1.analyzePythonCode)(code, relativePath);
|
|
219
292
|
if (result.hasError) {
|
|
220
|
-
const details = getAuditDetails(result.type || 'Error', result.error || 'Unknown Issue');
|
|
293
|
+
const details = getAuditDetails('RR-SEC-001', result.type || 'Error', result.error || 'Unknown Issue');
|
|
221
294
|
findings.push({
|
|
222
295
|
file: relativePath,
|
|
223
296
|
line: result.line || 0,
|
|
@@ -233,14 +306,14 @@ async function analyzeRepository(repoPath, resultsDir) {
|
|
|
233
306
|
}
|
|
234
307
|
}
|
|
235
308
|
if (!hasLogging && files.filter(f => f.endsWith('.py')).length > 0) {
|
|
236
|
-
const details = getAuditDetails('
|
|
309
|
+
const details = getAuditDetails('RR-LOG-001', 'ProductionRisk', '');
|
|
237
310
|
findings.push({ file: 'Repository Root', line: 0, type: 'ProductionRisk', ...details });
|
|
238
311
|
operationalGapCount++;
|
|
239
312
|
}
|
|
240
313
|
// 3. Dependency Scan (Scalability -15)
|
|
241
314
|
const { adj, cycles } = (0, archScanner_js_1.depsScan)(repoPath);
|
|
242
315
|
for (const cycle of cycles) {
|
|
243
|
-
const details = getAuditDetails('CircularDependency', '');
|
|
316
|
+
const details = getAuditDetails('RR-DEP-001', 'CircularDependency', `Cycle: ${cycle.join(' -> ')}`);
|
|
244
317
|
findings.push({
|
|
245
318
|
file: cycle[0],
|
|
246
319
|
line: 0,
|
package/package.json
CHANGED
package/src/repoAnalyzer.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface RepoAnalysisResult {
|
|
|
8
8
|
score: number; // Release Readiness Score (RRS)
|
|
9
9
|
status: 'Ready for Production' | 'Needs Attention' | 'Not Ready for Deployment';
|
|
10
10
|
findings: {
|
|
11
|
+
id: string;
|
|
11
12
|
title: string;
|
|
12
13
|
file: string;
|
|
13
14
|
line: number;
|
|
@@ -37,88 +38,152 @@ export interface RepoAnalysisResult {
|
|
|
37
38
|
/**
|
|
38
39
|
* 3-Tier Business Audit Translation
|
|
39
40
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 3-Tier Business Audit Translation with Standard IDs
|
|
43
|
+
*/
|
|
44
|
+
export function getAuditDetails(id: string, type: string, issue: string): { id: string; title: string; category: RepoAnalysisResult['findings'][0]['category']; evidence: string; standard: string; impact: string; action: string; reference: string; whenItMatters: string } {
|
|
45
|
+
const commonFields = { id };
|
|
46
|
+
|
|
47
|
+
if (id === 'RR-SEC-001' || type === 'SecurityRisk') {
|
|
42
48
|
return {
|
|
49
|
+
...commonFields,
|
|
43
50
|
title: "보안 취약점 위험 (Security Vulnerability)",
|
|
44
51
|
category: 'Security',
|
|
45
52
|
evidence: issue,
|
|
46
53
|
standard: "OWASP Top 10 A03:2021 – Injection",
|
|
47
|
-
impact: "외부 공격자가 시스템 권한을 탈취하거나 민감 정보를 유출할 수 있는
|
|
48
|
-
action:
|
|
54
|
+
impact: "외부 공격자가 시스템 권한을 탈취하거나 민감 정보를 유출할 수 있는 조건이 형성됩니다.",
|
|
55
|
+
action: `
|
|
56
|
+
# Action: 격리 및 환경변수 사용
|
|
57
|
+
subprocess.run(..., shell=False) # 권장
|
|
58
|
+
# 또는 .env 파일 사용
|
|
59
|
+
import os
|
|
60
|
+
SECRET = os.getenv('MY_SECRET')
|
|
61
|
+
`,
|
|
49
62
|
reference: "https://docs.python.org/3/library/subprocess.html#security-considerations",
|
|
50
|
-
whenItMatters: "배포 즉시 자동화된 스캐너나 공격자에 의해 탐지될 수
|
|
63
|
+
whenItMatters: "배포 즉시 자동화된 스캐너나 공격자에 의해 탐지될 수 있습니다."
|
|
51
64
|
};
|
|
52
65
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
title: "자동화 테스트 부재 (Missing Automated Tests)",
|
|
57
|
-
category: 'Service Interruption',
|
|
58
|
-
evidence: "tests/ 디렉토리 또는 pytest/unittest 관련 설정을 찾을 수 없습니다.",
|
|
59
|
-
standard: "Stability Standard: Code Coverage > 70%",
|
|
60
|
-
impact: "수동 테스트에 의존하게 되어 코드 변경 시마다 예상치 못한 장애가 운영 환경에 배포될 위험이 높으며, 장애 복구 시간(MTTR)이 매우 길어집니다.",
|
|
61
|
-
action: "pytest를 설치하고 핵심 로직에 대한 Smoke Test 3개를 먼저 작성하세요.",
|
|
62
|
-
reference: "https://docs.pytest.org/en/7.1.x/getting-started.html",
|
|
63
|
-
whenItMatters: "코드 변경 주기가 빨라지거나 팀원이 2명 이상으로 늘어날 때 병목이 발생합니다."
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
if (issue.includes('CI')) {
|
|
67
|
-
return {
|
|
68
|
-
title: "배포 자동화 파이프라인 부재 (Missing CI Pipeline)",
|
|
69
|
-
category: 'Service Interruption',
|
|
70
|
-
evidence: "GitHub Actions 또는 CI 도구 워크플로우 설정을 발견할 수 없습니다.",
|
|
71
|
-
standard: "Modern DevOps Standard: Continuous Integration",
|
|
72
|
-
impact: "코드 머지 전 검증 자동화가 불가능하여, 사람이 개입하는 과정에서 실수가 반복됩니다. 이는 배포 안정성을 근본적으로 파악할 수 없게 만듭니다.",
|
|
73
|
-
action: ".github/workflows/main.yml 파일을 생성하고 자동 테스트 프로세스를 구축하세요.",
|
|
74
|
-
reference: "https://github.com/features/actions",
|
|
75
|
-
whenItMatters: "하루에 2회 이상 배포를 시도할 때 검증 누락으로 인한 사고 확률이 80%를 상회하게 됩니다."
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (issue.includes('Docker')) {
|
|
79
|
-
return {
|
|
80
|
-
title: "컨테이너화 구성 누락 (Environment Inconsistency)",
|
|
81
|
-
category: 'Service Interruption',
|
|
82
|
-
evidence: "Dockerfile 또는 docker-compose.yml 파일이 존재하지 않습니다.",
|
|
83
|
-
standard: "Cloud Native Standard: Immutable Infrastructure",
|
|
84
|
-
impact: "로컬 개발 환경과 실제 운영 환경의 버전 차이로 인해 '내 컴퓨터에선 되는데 서버에선 안 되는' 예측 불가능한 장애가 발생합니다.",
|
|
85
|
-
action: "Dockerfile을 작성하여 환경 설정을 컨테이너화하세요.",
|
|
86
|
-
reference: "https://docs.docker.com/engine/reference/builder/",
|
|
87
|
-
whenItMatters: "신규 개발자가 팀에 합류하거나 클라우드 인프라 확장이 필요할 때 시간 낭비가 심화됩니다."
|
|
88
|
-
};
|
|
89
|
-
}
|
|
66
|
+
|
|
67
|
+
if (id === 'RR-TEST-001') {
|
|
90
68
|
return {
|
|
91
|
-
|
|
69
|
+
...commonFields,
|
|
70
|
+
title: "자동화 테스트 부재 (Missing Automated Tests)",
|
|
92
71
|
category: 'Service Interruption',
|
|
93
|
-
evidence:
|
|
94
|
-
standard: "
|
|
95
|
-
impact: "
|
|
96
|
-
action:
|
|
97
|
-
|
|
98
|
-
|
|
72
|
+
evidence: "tests/ 디렉토리 또는 pytest/unittest 관련 설정을 찾을 수 없습니다.",
|
|
73
|
+
standard: "pytest Framework Documentation",
|
|
74
|
+
impact: "코드 변경 시 기존 기능이 파괴되었는지 확인할 방법이 없어, 배포 후 장애 발생 확률이 높아집니다.",
|
|
75
|
+
action: `
|
|
76
|
+
# Action: Create tests/test_smoke.py
|
|
77
|
+
def test_health_check():
|
|
78
|
+
assert True # Basic sanity check
|
|
79
|
+
`,
|
|
80
|
+
reference: "https://docs.pytest.org/",
|
|
81
|
+
whenItMatters: "팀원이 2명 이상으로 늘어나거나 배포 주기가 빨라질 때."
|
|
99
82
|
};
|
|
100
83
|
}
|
|
101
|
-
|
|
84
|
+
|
|
85
|
+
if (id === 'RR-CI-001') {
|
|
102
86
|
return {
|
|
87
|
+
...commonFields,
|
|
88
|
+
title: "배포 자동화 파이프라인 부재 (Missing CI Pipeline)",
|
|
89
|
+
category: 'Service Interruption',
|
|
90
|
+
evidence: "GitHub Actions (.github/workflows/*.yml) 또는 CI 설정 파일이 없습니다.",
|
|
91
|
+
standard: "GitHub Actions Documentation",
|
|
92
|
+
impact: "사람의 수동 배포 과정에서 실수가 발생할 수 있으며, 일관된 배포 상태를 보장할 수 없습니다.",
|
|
93
|
+
action: `
|
|
94
|
+
# Action: Create .github/workflows/ci.yml
|
|
95
|
+
name: CI
|
|
96
|
+
on: [push]
|
|
97
|
+
jobs:
|
|
98
|
+
test:
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v3
|
|
102
|
+
- run: npm test
|
|
103
|
+
`,
|
|
104
|
+
reference: "https://docs.github.com/en/actions",
|
|
105
|
+
whenItMatters: "배포 빈도가 주 2회 이상으로 증가할 때."
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (id === 'RR-OPS-001') {
|
|
110
|
+
return {
|
|
111
|
+
...commonFields,
|
|
112
|
+
title: "운영 기본 위생 체크 실패 (Project Hygiene)",
|
|
113
|
+
category: 'Service Interruption',
|
|
114
|
+
evidence: issue, // Consolidated list will be passed here
|
|
115
|
+
standard: "12-Factor App / Docker Documentation",
|
|
116
|
+
impact: "개발 환경과 운영 환경의 불일치로 인해 '내 컴퓨터에서는 되는데 서버에서는 안 되는' 문제가 발생합니다.",
|
|
117
|
+
action: `
|
|
118
|
+
# Checklist to Fix:
|
|
119
|
+
1. Create 'Dockerfile'
|
|
120
|
+
2. Create '.gitignore' (use gitignore.io)
|
|
121
|
+
3. Create 'requirements.txt' or 'package.json'
|
|
122
|
+
4. Create '.env.example'
|
|
123
|
+
`,
|
|
124
|
+
reference: "https://12factor.net/",
|
|
125
|
+
whenItMatters: "신규 입사자 온보딩 또는 서버 이관 시."
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (id === 'RR-LOG-001') {
|
|
130
|
+
return {
|
|
131
|
+
...commonFields,
|
|
132
|
+
title: "로깅 설정 미흡 (Insufficient Logging)",
|
|
133
|
+
category: 'Maintenance',
|
|
134
|
+
evidence: "코드 내에서 로깅 설정(logging, loguru 등)이 발견되지 않았습니다.",
|
|
135
|
+
standard: "Python Logging Cookbook",
|
|
136
|
+
impact: "장애 발생 시 원인을 추적할 수 있는 데이터가 없어 해결 시간이 길어집니다.",
|
|
137
|
+
action: `
|
|
138
|
+
# Action: Python Logging Setup
|
|
139
|
+
import logging
|
|
140
|
+
logging.basicConfig(level=logging.INFO)
|
|
141
|
+
logger = logging.getLogger(__name__)
|
|
142
|
+
logger.info("Server started")
|
|
143
|
+
`,
|
|
144
|
+
reference: "https://docs.python.org/3/howto/logging-cookbook.html",
|
|
145
|
+
whenItMatters: "운영 중 알 수 없는 500 에러가 발생했을 때."
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (id === 'RR-DEP-001' || type === 'CircularDependency') {
|
|
150
|
+
return {
|
|
151
|
+
...commonFields,
|
|
103
152
|
title: "구조적 의존성 결함 (Structural Dependency Issue)",
|
|
104
153
|
category: 'Scalability',
|
|
105
|
-
evidence:
|
|
154
|
+
evidence: issue,
|
|
106
155
|
standard: "Clean Architecture: Dependency Rule",
|
|
107
|
-
impact: "모듈 간 결합도가 높아져
|
|
108
|
-
action: "
|
|
109
|
-
reference: "https://
|
|
110
|
-
whenItMatters: "프로젝트
|
|
156
|
+
impact: "모듈 간 결합도가 높아져 유지보수가 어려워지고, 사이드 이펙트가 발생하기 쉽습니다.",
|
|
157
|
+
action: "상호 참조하는 모듈을 분리하거나 공통 모듈로 추출하세요.",
|
|
158
|
+
reference: "https://refactoring.guru/design-patterns",
|
|
159
|
+
whenItMatters: "프로젝트 규모가 커질수록 리팩토링 비용이 기하급수적으로 증가합니다."
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (id === 'RR-LINT-001' || type === 'GodModule') {
|
|
164
|
+
return {
|
|
165
|
+
...commonFields,
|
|
166
|
+
title: "거대 모듈 감지 (God Module)",
|
|
167
|
+
category: 'Maintenance',
|
|
168
|
+
evidence: issue,
|
|
169
|
+
standard: "Clean Code: Functions",
|
|
170
|
+
impact: "단일 파일의 책임이 과도하여 변경 시 영향 범위를 예측하기 어렵습니다.",
|
|
171
|
+
action: "책임에 따라 파일을 분리하세요 (Separation of Concerns).",
|
|
172
|
+
reference: "https://pypi.org/project/flake8/",
|
|
173
|
+
whenItMatters: "기능 추가 시마다 버그가 발생할 때."
|
|
111
174
|
};
|
|
112
175
|
}
|
|
176
|
+
|
|
113
177
|
return {
|
|
178
|
+
...commonFields,
|
|
114
179
|
title: "기타 잠재적 리스크 (Other Potential Risks)",
|
|
115
180
|
category: 'Maintenance',
|
|
116
181
|
evidence: issue,
|
|
117
182
|
standard: "General Coding Best Practices",
|
|
118
|
-
impact: "
|
|
119
|
-
action: "
|
|
183
|
+
impact: "잠재적인 버그나 유지보수 어려움이 있을 수 있습니다.",
|
|
184
|
+
action: "해당 코드를 리뷰하고 리팩토링을 고려하세요.",
|
|
120
185
|
reference: "#",
|
|
121
|
-
whenItMatters: "지속적인
|
|
186
|
+
whenItMatters: "지속적인 코드 품질 저하가 우려될 때."
|
|
122
187
|
};
|
|
123
188
|
}
|
|
124
189
|
|
|
@@ -147,43 +212,66 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
147
212
|
let operationalGapCount = 0;
|
|
148
213
|
|
|
149
214
|
// 1. Operational Audit (Critical -30 each)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const allFiles = fs.readdirSync(repoPath);
|
|
166
|
-
found = allFiles.some(f => check.pattern.test(f.toLowerCase()));
|
|
167
|
-
}
|
|
168
|
-
} catch (e) { found = false; }
|
|
169
|
-
|
|
170
|
-
if (!found) {
|
|
171
|
-
const details = getAuditDetails('ProductionRisk', check.message);
|
|
172
|
-
findings.push({
|
|
173
|
-
file: 'Repository Root',
|
|
174
|
-
line: 0,
|
|
175
|
-
type: 'ProductionRisk',
|
|
176
|
-
...details
|
|
177
|
-
});
|
|
178
|
-
operationalGapCount++;
|
|
179
|
-
}
|
|
215
|
+
// 1. Operational Audit & Consolidation
|
|
216
|
+
const hygieneMissing: string[] = [];
|
|
217
|
+
|
|
218
|
+
// Check Tests
|
|
219
|
+
const hasTests = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
220
|
+
.some(d => d.isDirectory() && /tests?|spec/i.test(d.name));
|
|
221
|
+
if (!hasTests) {
|
|
222
|
+
const details = getAuditDetails('RR-TEST-001', 'ProductionRisk', '');
|
|
223
|
+
findings.push({
|
|
224
|
+
file: 'Repository Root',
|
|
225
|
+
line: 0,
|
|
226
|
+
type: 'ProductionRisk',
|
|
227
|
+
...details
|
|
228
|
+
});
|
|
229
|
+
operationalGapCount++;
|
|
180
230
|
}
|
|
181
231
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
232
|
+
// Check CI
|
|
233
|
+
const hasCI = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
234
|
+
.some(d => d.isDirectory() && /(\.github|\.circleci|jenkins|gitlab)/i.test(d.name));
|
|
235
|
+
if (!hasCI) {
|
|
236
|
+
const details = getAuditDetails('RR-CI-001', 'ProductionRisk', '');
|
|
237
|
+
findings.push({
|
|
238
|
+
file: 'Repository Root',
|
|
239
|
+
line: 0,
|
|
240
|
+
type: 'ProductionRisk',
|
|
241
|
+
...details
|
|
242
|
+
});
|
|
243
|
+
operationalGapCount++;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check Hygiene (Docker, Pinning, Env, GitIgnore)
|
|
247
|
+
const allFiles = fs.readdirSync(repoPath);
|
|
248
|
+
|
|
249
|
+
// Docker
|
|
250
|
+
if (!allFiles.some(f => /Dockerfile|docker-compose/i.test(f))) {
|
|
251
|
+
hygieneMissing.push("✘ Dockerfile or docker-compose.yml missing");
|
|
252
|
+
}
|
|
253
|
+
// Pinning
|
|
254
|
+
if (!allFiles.some(f => /requirements\.txt|poetry\.lock|pyproject\.toml|package-lock\.json|pnpm-lock\.yaml/i.test(f))) {
|
|
255
|
+
hygieneMissing.push("✘ Dependency Lockfile (requirements.txt, package-lock.json, etc) missing");
|
|
256
|
+
}
|
|
257
|
+
// Env
|
|
258
|
+
if (!allFiles.some(f => /\.env\.example|\.env\.sample/i.test(f))) {
|
|
259
|
+
hygieneMissing.push("✘ .env.example (Safe environment template) missing");
|
|
260
|
+
}
|
|
261
|
+
// GitIgnore
|
|
262
|
+
if (!fs.existsSync(path.join(repoPath, '.gitignore'))) {
|
|
263
|
+
hygieneMissing.push("✘ .gitignore missing");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (hygieneMissing.length > 0) {
|
|
267
|
+
const evidenceBlock = "\n" + hygieneMissing.join("\n");
|
|
268
|
+
const details = getAuditDetails('RR-OPS-001', 'ProductionRisk', evidenceBlock);
|
|
269
|
+
findings.push({
|
|
270
|
+
file: 'Repository Root',
|
|
271
|
+
line: 0,
|
|
272
|
+
type: 'ProductionRisk',
|
|
273
|
+
...details
|
|
274
|
+
});
|
|
187
275
|
operationalGapCount++;
|
|
188
276
|
}
|
|
189
277
|
|
|
@@ -203,7 +291,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
203
291
|
|
|
204
292
|
// God Module Check (>500 lines)
|
|
205
293
|
if (lines.length > 500) {
|
|
206
|
-
const details = getAuditDetails('
|
|
294
|
+
const details = getAuditDetails('RR-LINT-001', 'GodModule', `File length: ${lines.length} lines`);
|
|
207
295
|
findings.push({ file: relativePath, line: 0, type: 'ProductionRisk', ...details });
|
|
208
296
|
operationalGapCount++;
|
|
209
297
|
}
|
|
@@ -211,7 +299,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
211
299
|
const result = await analyzePythonCode(code, relativePath);
|
|
212
300
|
|
|
213
301
|
if (result.hasError) {
|
|
214
|
-
const details = getAuditDetails(result.type || 'Error', result.error || 'Unknown Issue');
|
|
302
|
+
const details = getAuditDetails('RR-SEC-001', result.type || 'Error', result.error || 'Unknown Issue');
|
|
215
303
|
findings.push({
|
|
216
304
|
file: relativePath,
|
|
217
305
|
line: result.line || 0,
|
|
@@ -227,7 +315,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
227
315
|
}
|
|
228
316
|
|
|
229
317
|
if (!hasLogging && files.filter(f => f.endsWith('.py')).length > 0) {
|
|
230
|
-
const details = getAuditDetails('
|
|
318
|
+
const details = getAuditDetails('RR-LOG-001', 'ProductionRisk', '');
|
|
231
319
|
findings.push({ file: 'Repository Root', line: 0, type: 'ProductionRisk', ...details });
|
|
232
320
|
operationalGapCount++;
|
|
233
321
|
}
|
|
@@ -235,7 +323,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
235
323
|
// 3. Dependency Scan (Scalability -15)
|
|
236
324
|
const { adj, cycles } = depsScan(repoPath);
|
|
237
325
|
for (const cycle of cycles) {
|
|
238
|
-
const details = getAuditDetails('CircularDependency', '');
|
|
326
|
+
const details = getAuditDetails('RR-DEP-001', 'CircularDependency', `Cycle: ${cycle.join(' -> ')}`);
|
|
239
327
|
findings.push({
|
|
240
328
|
file: cycle[0],
|
|
241
329
|
line: 0,
|