archrisk-engine 1.0.0 β 1.0.2
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jsAnalyzer.d.ts +8 -0
- package/dist/jsAnalyzer.js +129 -0
- package/dist/repoAnalyzer.d.ts +6 -1
- package/dist/repoAnalyzer.js +197 -99
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/jsAnalyzer.ts +143 -0
- package/src/repoAnalyzer.ts +213 -97
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -19,3 +19,4 @@ __exportStar(require("./aiDiagnosis.js"), exports);
|
|
|
19
19
|
__exportStar(require("./archScanner.js"), exports);
|
|
20
20
|
__exportStar(require("./repoAnalyzer.js"), exports);
|
|
21
21
|
__exportStar(require("./deepAnalysis.js"), exports);
|
|
22
|
+
__exportStar(require("./jsAnalyzer.js"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AnalysisResult } from './analyzer.js';
|
|
2
|
+
/**
|
|
3
|
+
* [The Eye] JS/TS Analysis Engine
|
|
4
|
+
*
|
|
5
|
+
* RegEx-based static analysis for JavaScript/TypeScript files.
|
|
6
|
+
* MVP: Focused on 8 specific credibility rules.
|
|
7
|
+
*/
|
|
8
|
+
export declare function analyzeJsTsCode(code: string, fileName: string): Promise<AnalysisResult>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeJsTsCode = analyzeJsTsCode;
|
|
4
|
+
/**
|
|
5
|
+
* [The Eye] JS/TS Analysis Engine
|
|
6
|
+
*
|
|
7
|
+
* RegEx-based static analysis for JavaScript/TypeScript files.
|
|
8
|
+
* MVP: Focused on 8 specific credibility rules.
|
|
9
|
+
*/
|
|
10
|
+
async function analyzeJsTsCode(code, fileName) {
|
|
11
|
+
// 1. Architecture Health Scan (God Module / Large File)
|
|
12
|
+
const lines = code.split('\n');
|
|
13
|
+
if (lines.length > 800) {
|
|
14
|
+
return {
|
|
15
|
+
hasError: true,
|
|
16
|
+
error: `Large File Risk: ${lines.length} lines. Maintanability risk.`,
|
|
17
|
+
line: 1,
|
|
18
|
+
type: 'ProductionRisk',
|
|
19
|
+
file: fileName
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// 2. Risk Scan (Security & Production Readiness)
|
|
23
|
+
const riskResult = scanForJsRisks(code, fileName, lines);
|
|
24
|
+
if (riskResult.hasError) {
|
|
25
|
+
return riskResult;
|
|
26
|
+
}
|
|
27
|
+
return { hasError: false };
|
|
28
|
+
}
|
|
29
|
+
function scanForJsRisks(code, fileName, lines) {
|
|
30
|
+
const risks = [
|
|
31
|
+
// Critical (π΄) - Security
|
|
32
|
+
{
|
|
33
|
+
pattern: /eval\s*\(|new\s+Function\s*\(/,
|
|
34
|
+
type: 'SecurityRisk',
|
|
35
|
+
message: '[Security] Dynamic code execution detected (eval/new Function). This is a severe security risk.'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /child_process\.(exec|execSync)\s*\(/,
|
|
39
|
+
type: 'SecurityRisk',
|
|
40
|
+
message: '[Security] Shell command execution detected. Ensure inputs are sanitized or use spawn without shell.'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /spawn\s*\(.*,\s*\{.*shell:\s*true/s, // Multi-line match attempt with DOTALL flag simulated or just simple check
|
|
44
|
+
type: 'SecurityRisk',
|
|
45
|
+
message: '[Security] spawn with { shell: true } detected. This enables shell command injection.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /(?:api[._-]?key|password|secret|token)\s*[:=]\s*['"][a-zA-Z0-9_-]{10,}['"]/i,
|
|
49
|
+
type: 'SecurityRisk',
|
|
50
|
+
message: '[Security] Hardcoded secret detected. Use environment variables.'
|
|
51
|
+
},
|
|
52
|
+
// Warning (π‘) - Production Readiness
|
|
53
|
+
{
|
|
54
|
+
condition: (l) => /(axios(\.[a-z]+)?|fetch|http\.(get|request))\s*\(/.test(l) && !/timeout/.test(l),
|
|
55
|
+
type: 'ProductionRisk',
|
|
56
|
+
message: '[Reliability] HTTP call missing explicit timeout. This can cause cascading failures.'
|
|
57
|
+
},
|
|
58
|
+
// Heuristic: App listen but no global error handler (simplified: check for generic app.use((err... pattern)
|
|
59
|
+
{
|
|
60
|
+
condition: (c) => c.includes('app.listen') && !/app\.use\s*\(\s*\(\s*err/.test(c),
|
|
61
|
+
type: 'ProductionRisk',
|
|
62
|
+
message: '[Reliability] Express app detected but Global Error Handler missing. Unhandled errors may crash the server.'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pattern: /console\.log\s*\(/,
|
|
66
|
+
type: 'ProductionRisk',
|
|
67
|
+
message: '[Observability] console.log used in production code. Use a structured logger (winston/pino).'
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
// Line-based checks
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
for (const risk of risks) {
|
|
74
|
+
// Pattern check
|
|
75
|
+
if (risk.pattern && risk.pattern.test(line)) {
|
|
76
|
+
// Skip comments for simple cases
|
|
77
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*'))
|
|
78
|
+
continue;
|
|
79
|
+
return {
|
|
80
|
+
hasError: true,
|
|
81
|
+
error: risk.message,
|
|
82
|
+
line: i + 1,
|
|
83
|
+
type: risk.type,
|
|
84
|
+
file: fileName
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Line-based Condition check (special case for timeout)
|
|
88
|
+
// We distinguish global vs line condition by context?
|
|
89
|
+
// The 'app.listen' check is clearly global (checks whole code).
|
|
90
|
+
// The 'timeout' check is clearly line based (checks 'l').
|
|
91
|
+
// Let's rely on the message content to know if it is line-based for MVP simplicity
|
|
92
|
+
if (risk.condition && risk.message.includes('HTTP missing timeout') && risk.condition(line)) {
|
|
93
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*'))
|
|
94
|
+
continue;
|
|
95
|
+
return {
|
|
96
|
+
hasError: true,
|
|
97
|
+
error: risk.message,
|
|
98
|
+
line: i + 1,
|
|
99
|
+
type: risk.type,
|
|
100
|
+
file: fileName
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// File-based checks (Global patterns)
|
|
106
|
+
for (const risk of risks) {
|
|
107
|
+
// Global Condition check
|
|
108
|
+
if (risk.condition && !risk.message.includes('HTTP missing timeout') && risk.condition(code)) {
|
|
109
|
+
return {
|
|
110
|
+
hasError: true,
|
|
111
|
+
error: risk.message,
|
|
112
|
+
line: 0,
|
|
113
|
+
type: risk.type,
|
|
114
|
+
file: fileName
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Multi-line regex checks
|
|
118
|
+
if (risk.pattern && risk.pattern.flags.includes('s') && risk.pattern.test(code)) {
|
|
119
|
+
return {
|
|
120
|
+
hasError: true,
|
|
121
|
+
error: risk.message,
|
|
122
|
+
line: 0,
|
|
123
|
+
type: risk.type,
|
|
124
|
+
file: fileName
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { hasError: false };
|
|
129
|
+
}
|
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
|
@@ -39,6 +39,7 @@ const fs = __importStar(require("fs"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const child_process_1 = require("child_process");
|
|
41
41
|
const analyzer_js_1 = require("./analyzer.js");
|
|
42
|
+
const jsAnalyzer_js_1 = require("./jsAnalyzer.js");
|
|
42
43
|
const archScanner_js_1 = require("./archScanner.js");
|
|
43
44
|
/**
|
|
44
45
|
* CEO-ready "Business Translation" for technical risks
|
|
@@ -46,88 +47,144 @@ const archScanner_js_1 = require("./archScanner.js");
|
|
|
46
47
|
/**
|
|
47
48
|
* 3-Tier Business Audit Translation
|
|
48
49
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* 3-Tier Business Audit Translation with Standard IDs
|
|
52
|
+
*/
|
|
53
|
+
function getAuditDetails(id, type, issue) {
|
|
54
|
+
const commonFields = { id };
|
|
55
|
+
if (id === 'RR-SEC-001' || type === 'SecurityRisk') {
|
|
51
56
|
return {
|
|
57
|
+
...commonFields,
|
|
52
58
|
title: "보μ μ·¨μ½μ μν (Security Vulnerability)",
|
|
53
59
|
category: 'Security',
|
|
54
60
|
evidence: issue,
|
|
55
61
|
standard: "OWASP Top 10 A03:2021 β Injection",
|
|
56
|
-
impact: "μΈλΆ 곡격μκ° μμ€ν
κΆνμ νμ·¨νκ±°λ λ―Όκ° μ 보λ₯Ό μ μΆν μ μλ
|
|
57
|
-
action:
|
|
62
|
+
impact: "μΈλΆ 곡격μκ° μμ€ν
κΆνμ νμ·¨νκ±°λ λ―Όκ° μ 보λ₯Ό μ μΆν μ μλ μ‘°κ±΄μ΄ νμ±λ©λλ€.",
|
|
63
|
+
action: `
|
|
64
|
+
# Action: 격리 λ° νκ²½λ³μ μ¬μ©
|
|
65
|
+
subprocess.run(..., shell=False) # κΆμ₯
|
|
66
|
+
# λλ .env νμΌ μ¬μ©
|
|
67
|
+
import os
|
|
68
|
+
SECRET = os.getenv('MY_SECRET')
|
|
69
|
+
`,
|
|
58
70
|
reference: "https://docs.python.org/3/library/subprocess.html#security-considerations",
|
|
59
|
-
whenItMatters: "λ°°ν¬ μ¦μ μλνλ μ€μΊλλ 곡격μμ μν΄ νμ§λ μ
|
|
71
|
+
whenItMatters: "λ°°ν¬ μ¦μ μλνλ μ€μΊλλ 곡격μμ μν΄ νμ§λ μ μμ΅λλ€."
|
|
60
72
|
};
|
|
61
73
|
}
|
|
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
|
-
}
|
|
74
|
+
if (id === 'RR-TEST-001') {
|
|
99
75
|
return {
|
|
100
|
-
|
|
76
|
+
...commonFields,
|
|
77
|
+
title: "μλν ν
μ€νΈ λΆμ¬ (Missing Automated Tests)",
|
|
101
78
|
category: 'Service Interruption',
|
|
102
|
-
evidence:
|
|
103
|
-
standard: "
|
|
104
|
-
impact: "
|
|
105
|
-
action:
|
|
106
|
-
|
|
107
|
-
|
|
79
|
+
evidence: "tests/ λλ ν 리 λλ pytest/unittest κ΄λ ¨ μ€μ μ μ°Ύμ μ μμ΅λλ€.",
|
|
80
|
+
standard: "pytest Framework Documentation",
|
|
81
|
+
impact: "μ½λ λ³κ²½ μ κΈ°μ‘΄ κΈ°λ₯μ΄ νκ΄΄λμλμ§ νμΈν λ°©λ²μ΄ μμ΄, λ°°ν¬ ν μ₯μ λ°μ νλ₯ μ΄ λμμ§λλ€.",
|
|
82
|
+
action: `
|
|
83
|
+
# Action: Create tests/test_smoke.py
|
|
84
|
+
def test_health_check():
|
|
85
|
+
assert True # Basic sanity check
|
|
86
|
+
`,
|
|
87
|
+
reference: "https://docs.pytest.org/",
|
|
88
|
+
whenItMatters: "νμμ΄ 2λͺ
μ΄μμΌλ‘ λμ΄λκ±°λ λ°°ν¬ μ£ΌκΈ°κ° λΉ¨λΌμ§ λ."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (id === 'RR-CI-001') {
|
|
92
|
+
return {
|
|
93
|
+
...commonFields,
|
|
94
|
+
title: "λ°°ν¬ μλν νμ΄νλΌμΈ λΆμ¬ (Missing CI Pipeline)",
|
|
95
|
+
category: 'Service Interruption',
|
|
96
|
+
evidence: "GitHub Actions (.github/workflows/*.yml) λλ CI μ€μ νμΌμ΄ μμ΅λλ€.",
|
|
97
|
+
standard: "GitHub Actions Documentation",
|
|
98
|
+
impact: "μ¬λμ μλ λ°°ν¬ κ³Όμ μμ μ€μκ° λ°μν μ μμΌλ©°, μΌκ΄λ λ°°ν¬ μνλ₯Ό 보μ₯ν μ μμ΅λλ€.",
|
|
99
|
+
action: `
|
|
100
|
+
# Action: Create .github/workflows/ci.yml
|
|
101
|
+
name: CI
|
|
102
|
+
on: [push]
|
|
103
|
+
jobs:
|
|
104
|
+
test:
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/checkout@v3
|
|
108
|
+
- run: npm test
|
|
109
|
+
`,
|
|
110
|
+
reference: "https://docs.github.com/en/actions",
|
|
111
|
+
whenItMatters: "λ°°ν¬ λΉλκ° μ£Ό 2ν μ΄μμΌλ‘ μ¦κ°ν λ."
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (id === 'RR-OPS-001') {
|
|
115
|
+
return {
|
|
116
|
+
...commonFields,
|
|
117
|
+
title: "μ΄μ κΈ°λ³Έ μμ μ²΄ν¬ μ€ν¨ (Project Hygiene)",
|
|
118
|
+
category: 'Service Interruption',
|
|
119
|
+
evidence: issue, // Consolidated list will be passed here
|
|
120
|
+
standard: "12-Factor App / Docker Documentation",
|
|
121
|
+
impact: "κ°λ° νκ²½κ³Ό μ΄μ νκ²½μ λΆμΌμΉλ‘ μΈν΄ 'λ΄ μ»΄ν¨ν°μμλ λλλ° μλ²μμλ μ λλ' λ¬Έμ κ° λ°μν©λλ€.",
|
|
122
|
+
action: `
|
|
123
|
+
# Checklist to Fix:
|
|
124
|
+
1. Create 'Dockerfile'
|
|
125
|
+
2. Create '.gitignore' (use gitignore.io)
|
|
126
|
+
3. Create 'requirements.txt' or 'package.json'
|
|
127
|
+
4. Create '.env.example'
|
|
128
|
+
`,
|
|
129
|
+
reference: "https://12factor.net/",
|
|
130
|
+
whenItMatters: "μ κ· μ
μ¬μ μ¨λ³΄λ© λλ μλ² μ΄κ΄ μ."
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (id === 'RR-LOG-001') {
|
|
134
|
+
return {
|
|
135
|
+
...commonFields,
|
|
136
|
+
title: "λ‘κΉ
μ€μ λ―Έν‘ (Insufficient Logging)",
|
|
137
|
+
category: 'Maintenance',
|
|
138
|
+
evidence: "μ½λ λ΄μμ λ‘κΉ
μ€μ (logging, loguru λ±)μ΄ λ°κ²¬λμ§ μμμ΅λλ€.",
|
|
139
|
+
standard: "Python Logging Cookbook",
|
|
140
|
+
impact: "μ₯μ λ°μ μ μμΈμ μΆμ ν μ μλ λ°μ΄ν°κ° μμ΄ ν΄κ²° μκ°μ΄ κΈΈμ΄μ§λλ€.",
|
|
141
|
+
action: `
|
|
142
|
+
# Action: Python Logging Setup
|
|
143
|
+
import logging
|
|
144
|
+
logging.basicConfig(level=logging.INFO)
|
|
145
|
+
logger = logging.getLogger(__name__)
|
|
146
|
+
logger.info("Server started")
|
|
147
|
+
`,
|
|
148
|
+
reference: "https://docs.python.org/3/howto/logging-cookbook.html",
|
|
149
|
+
whenItMatters: "μ΄μ μ€ μ μ μλ 500 μλ¬κ° λ°μνμ λ."
|
|
108
150
|
};
|
|
109
151
|
}
|
|
110
|
-
if (
|
|
152
|
+
if (id === 'RR-DEP-001' || type === 'CircularDependency') {
|
|
111
153
|
return {
|
|
154
|
+
...commonFields,
|
|
112
155
|
title: "ꡬ쑰μ μμ‘΄μ± κ²°ν¨ (Structural Dependency Issue)",
|
|
113
156
|
category: 'Scalability',
|
|
114
|
-
evidence:
|
|
157
|
+
evidence: issue,
|
|
115
158
|
standard: "Clean Architecture: Dependency Rule",
|
|
116
|
-
impact: "λͺ¨λ κ° κ²°ν©λκ° λμμ Έ
|
|
117
|
-
action: "
|
|
118
|
-
reference: "https://
|
|
119
|
-
whenItMatters: "νλ‘μ νΈ
|
|
159
|
+
impact: "λͺ¨λ κ° κ²°ν©λκ° λμμ Έ μ μ§λ³΄μκ° μ΄λ €μμ§κ³ , μ¬μ΄λ μ΄ννΈκ° λ°μνκΈ° μ½μ΅λλ€.",
|
|
160
|
+
action: "μνΈ μ°Έμ‘°νλ λͺ¨λμ λΆλ¦¬νκ±°λ κ³΅ν΅ λͺ¨λλ‘ μΆμΆνμΈμ.",
|
|
161
|
+
reference: "https://refactoring.guru/design-patterns",
|
|
162
|
+
whenItMatters: "νλ‘μ νΈ κ·λͺ¨κ° 컀μ§μλ‘ λ¦¬ν©ν λ§ λΉμ©μ΄ κΈ°νκΈμμ μΌλ‘ μ¦κ°ν©λλ€."
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (id === 'RR-LINT-001' || type === 'GodModule') {
|
|
166
|
+
return {
|
|
167
|
+
...commonFields,
|
|
168
|
+
title: "κ±°λ λͺ¨λ κ°μ§ (God Module)",
|
|
169
|
+
category: 'Maintenance',
|
|
170
|
+
evidence: issue,
|
|
171
|
+
standard: "Clean Code: Functions",
|
|
172
|
+
impact: "λ¨μΌ νμΌμ μ±
μμ΄ κ³Όλνμ¬ λ³κ²½ μ μν₯ λ²μλ₯Ό μμΈ‘νκΈ° μ΄λ ΅μ΅λλ€.",
|
|
173
|
+
action: "μ±
μμ λ°λΌ νμΌμ λΆλ¦¬νμΈμ (Separation of Concerns).",
|
|
174
|
+
reference: "https://pypi.org/project/flake8/",
|
|
175
|
+
whenItMatters: "κΈ°λ₯ μΆκ° μλ§λ€ λ²κ·Έκ° λ°μν λ."
|
|
120
176
|
};
|
|
121
177
|
}
|
|
122
178
|
return {
|
|
179
|
+
...commonFields,
|
|
123
180
|
title: "κΈ°ν μ μ¬μ 리μ€ν¬ (Other Potential Risks)",
|
|
124
181
|
category: 'Maintenance',
|
|
125
182
|
evidence: issue,
|
|
126
183
|
standard: "General Coding Best Practices",
|
|
127
|
-
impact: "
|
|
128
|
-
action: "
|
|
184
|
+
impact: "μ μ¬μ μΈ λ²κ·Έλ μ μ§λ³΄μ μ΄λ €μμ΄ μμ μ μμ΅λλ€.",
|
|
185
|
+
action: "ν΄λΉ μ½λλ₯Ό 리뷰νκ³ λ¦¬ν©ν λ§μ κ³ λ €νμΈμ.",
|
|
129
186
|
reference: "#",
|
|
130
|
-
whenItMatters: "μ§μμ μΈ
|
|
187
|
+
whenItMatters: "μ§μμ μΈ μ½λ νμ§ μ νκ° μ°λ €λ λ."
|
|
131
188
|
};
|
|
132
189
|
}
|
|
133
190
|
async function analyzeRepository(repoPath, resultsDir) {
|
|
@@ -158,44 +215,61 @@ async function analyzeRepository(repoPath, resultsDir) {
|
|
|
158
215
|
let criticalCount = 0;
|
|
159
216
|
let operationalGapCount = 0;
|
|
160
217
|
// 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
|
-
}
|
|
218
|
+
// 1. Operational Audit & Consolidation
|
|
219
|
+
const hygieneMissing = [];
|
|
220
|
+
// Check Tests
|
|
221
|
+
const hasTests = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
222
|
+
.some(d => d.isDirectory() && /tests?|spec/i.test(d.name));
|
|
223
|
+
if (!hasTests) {
|
|
224
|
+
const details = getAuditDetails('RR-TEST-001', 'ProductionRisk', '');
|
|
225
|
+
findings.push({
|
|
226
|
+
file: 'Repository Root',
|
|
227
|
+
line: 0,
|
|
228
|
+
type: 'ProductionRisk',
|
|
229
|
+
...details
|
|
230
|
+
});
|
|
231
|
+
operationalGapCount++;
|
|
193
232
|
}
|
|
194
|
-
//
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
233
|
+
// Check CI
|
|
234
|
+
const hasCI = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
235
|
+
.some(d => d.isDirectory() && /(\.github|\.circleci|jenkins|gitlab)/i.test(d.name));
|
|
236
|
+
if (!hasCI) {
|
|
237
|
+
const details = getAuditDetails('RR-CI-001', 'ProductionRisk', '');
|
|
238
|
+
findings.push({
|
|
239
|
+
file: 'Repository Root',
|
|
240
|
+
line: 0,
|
|
241
|
+
type: 'ProductionRisk',
|
|
242
|
+
...details
|
|
243
|
+
});
|
|
244
|
+
operationalGapCount++;
|
|
245
|
+
}
|
|
246
|
+
// Check Hygiene (Docker, Pinning, Env, GitIgnore)
|
|
247
|
+
const allFiles = fs.readdirSync(repoPath);
|
|
248
|
+
// Docker
|
|
249
|
+
if (!allFiles.some(f => /Dockerfile|docker-compose/i.test(f))) {
|
|
250
|
+
hygieneMissing.push("β Dockerfile or docker-compose.yml missing");
|
|
251
|
+
}
|
|
252
|
+
// Pinning
|
|
253
|
+
if (!allFiles.some(f => /requirements\.txt|poetry\.lock|pyproject\.toml|package-lock\.json|pnpm-lock\.yaml/i.test(f))) {
|
|
254
|
+
hygieneMissing.push("β Dependency Lockfile (requirements.txt, package-lock.json, etc) missing");
|
|
255
|
+
}
|
|
256
|
+
// Env
|
|
257
|
+
if (!allFiles.some(f => /\.env\.example|\.env\.sample/i.test(f))) {
|
|
258
|
+
hygieneMissing.push("β .env.example (Safe environment template) missing");
|
|
259
|
+
}
|
|
260
|
+
// GitIgnore
|
|
261
|
+
if (!fs.existsSync(path.join(repoPath, '.gitignore'))) {
|
|
262
|
+
hygieneMissing.push("β .gitignore missing");
|
|
263
|
+
}
|
|
264
|
+
if (hygieneMissing.length > 0) {
|
|
265
|
+
const evidenceBlock = "\n" + hygieneMissing.join("\n");
|
|
266
|
+
const details = getAuditDetails('RR-OPS-001', 'ProductionRisk', evidenceBlock);
|
|
267
|
+
findings.push({
|
|
268
|
+
file: 'Repository Root',
|
|
269
|
+
line: 0,
|
|
270
|
+
type: 'ProductionRisk',
|
|
271
|
+
...details
|
|
272
|
+
});
|
|
199
273
|
operationalGapCount++;
|
|
200
274
|
}
|
|
201
275
|
let hasLogging = false;
|
|
@@ -211,13 +285,37 @@ async function analyzeRepository(repoPath, resultsDir) {
|
|
|
211
285
|
}
|
|
212
286
|
// God Module Check (>500 lines)
|
|
213
287
|
if (lines.length > 500) {
|
|
214
|
-
const details = getAuditDetails('
|
|
288
|
+
const details = getAuditDetails('RR-LINT-001', 'GodModule', `File length: ${lines.length} lines`);
|
|
215
289
|
findings.push({ file: relativePath, line: 0, type: 'ProductionRisk', ...details });
|
|
216
290
|
operationalGapCount++;
|
|
217
291
|
}
|
|
218
292
|
const result = await (0, analyzer_js_1.analyzePythonCode)(code, relativePath);
|
|
219
293
|
if (result.hasError) {
|
|
220
|
-
const details = getAuditDetails(result.type || 'Error', result.error || 'Unknown Issue');
|
|
294
|
+
const details = getAuditDetails('RR-SEC-001', result.type || 'Error', result.error || 'Unknown Issue');
|
|
295
|
+
findings.push({
|
|
296
|
+
file: relativePath,
|
|
297
|
+
line: result.line || 0,
|
|
298
|
+
type: result.type || 'Error',
|
|
299
|
+
...details
|
|
300
|
+
});
|
|
301
|
+
if (result.type === 'SecurityRisk')
|
|
302
|
+
criticalCount++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
console.error(`Error analyzing ${f}:`, e);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// 2.1 Code Level Analysis (JS/TS logic)
|
|
310
|
+
for (const f of files.filter(f => /\.(js|ts|jsx|tsx)$/.test(f))) {
|
|
311
|
+
try {
|
|
312
|
+
const code = fs.readFileSync(f, 'utf8');
|
|
313
|
+
const relativePath = path.relative(repoPath, f);
|
|
314
|
+
// Logging Check (Simple console.log check is in rules, but here specific frameworks?)
|
|
315
|
+
// We rely on rules for now.
|
|
316
|
+
const result = await (0, jsAnalyzer_js_1.analyzeJsTsCode)(code, relativePath);
|
|
317
|
+
if (result.hasError) {
|
|
318
|
+
const details = getAuditDetails('RR-SEC-002', result.type || 'Error', result.error || 'Unknown Issue'); // Use SEC-002 for JS? Or reuse.
|
|
221
319
|
findings.push({
|
|
222
320
|
file: relativePath,
|
|
223
321
|
line: result.line || 0,
|
|
@@ -233,14 +331,14 @@ async function analyzeRepository(repoPath, resultsDir) {
|
|
|
233
331
|
}
|
|
234
332
|
}
|
|
235
333
|
if (!hasLogging && files.filter(f => f.endsWith('.py')).length > 0) {
|
|
236
|
-
const details = getAuditDetails('
|
|
334
|
+
const details = getAuditDetails('RR-LOG-001', 'ProductionRisk', '');
|
|
237
335
|
findings.push({ file: 'Repository Root', line: 0, type: 'ProductionRisk', ...details });
|
|
238
336
|
operationalGapCount++;
|
|
239
337
|
}
|
|
240
338
|
// 3. Dependency Scan (Scalability -15)
|
|
241
339
|
const { adj, cycles } = (0, archScanner_js_1.depsScan)(repoPath);
|
|
242
340
|
for (const cycle of cycles) {
|
|
243
|
-
const details = getAuditDetails('CircularDependency', '');
|
|
341
|
+
const details = getAuditDetails('RR-DEP-001', 'CircularDependency', `Cycle: ${cycle.join(' -> ')}`);
|
|
244
342
|
findings.push({
|
|
245
343
|
file: cycle[0],
|
|
246
344
|
line: 0,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AnalysisResult } from './analyzer.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* [The Eye] JS/TS Analysis Engine
|
|
5
|
+
*
|
|
6
|
+
* RegEx-based static analysis for JavaScript/TypeScript files.
|
|
7
|
+
* MVP: Focused on 8 specific credibility rules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export async function analyzeJsTsCode(code: string, fileName: string): Promise<AnalysisResult> {
|
|
11
|
+
// 1. Architecture Health Scan (God Module / Large File)
|
|
12
|
+
const lines = code.split('\n');
|
|
13
|
+
if (lines.length > 800) {
|
|
14
|
+
return {
|
|
15
|
+
hasError: true,
|
|
16
|
+
error: `Large File Risk: ${lines.length} lines. Maintanability risk.`,
|
|
17
|
+
line: 1,
|
|
18
|
+
type: 'ProductionRisk',
|
|
19
|
+
file: fileName
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 2. Risk Scan (Security & Production Readiness)
|
|
24
|
+
const riskResult = scanForJsRisks(code, fileName, lines);
|
|
25
|
+
if (riskResult.hasError) {
|
|
26
|
+
return riskResult;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { hasError: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RiskRule {
|
|
33
|
+
pattern?: RegExp;
|
|
34
|
+
condition?: (codeOrLine: string) => boolean;
|
|
35
|
+
type: string;
|
|
36
|
+
message: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scanForJsRisks(code: string, fileName: string, lines: string[]): AnalysisResult {
|
|
40
|
+
const risks: RiskRule[] = [
|
|
41
|
+
// Critical (π΄) - Security
|
|
42
|
+
{
|
|
43
|
+
pattern: /eval\s*\(|new\s+Function\s*\(/,
|
|
44
|
+
type: 'SecurityRisk',
|
|
45
|
+
message: '[Security] Dynamic code execution detected (eval/new Function). This is a severe security risk.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /child_process\.(exec|execSync)\s*\(/,
|
|
49
|
+
type: 'SecurityRisk',
|
|
50
|
+
message: '[Security] Shell command execution detected. Ensure inputs are sanitized or use spawn without shell.'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pattern: /spawn\s*\(.*,\s*\{.*shell:\s*true/s, // Multi-line match attempt with DOTALL flag simulated or just simple check
|
|
54
|
+
type: 'SecurityRisk',
|
|
55
|
+
message: '[Security] spawn with { shell: true } detected. This enables shell command injection.'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
pattern: /(?:api[._-]?key|password|secret|token)\s*[:=]\s*['"][a-zA-Z0-9_-]{10,}['"]/i,
|
|
59
|
+
type: 'SecurityRisk',
|
|
60
|
+
message: '[Security] Hardcoded secret detected. Use environment variables.'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Warning (π‘) - Production Readiness
|
|
64
|
+
{
|
|
65
|
+
condition: (l) => /(axios(\.[a-z]+)?|fetch|http\.(get|request))\s*\(/.test(l) && !/timeout/.test(l),
|
|
66
|
+
type: 'ProductionRisk',
|
|
67
|
+
message: '[Reliability] HTTP call missing explicit timeout. This can cause cascading failures.'
|
|
68
|
+
},
|
|
69
|
+
// Heuristic: App listen but no global error handler (simplified: check for generic app.use((err... pattern)
|
|
70
|
+
{
|
|
71
|
+
condition: (c) => c.includes('app.listen') && !/app\.use\s*\(\s*\(\s*err/.test(c),
|
|
72
|
+
type: 'ProductionRisk',
|
|
73
|
+
message: '[Reliability] Express app detected but Global Error Handler missing. Unhandled errors may crash the server.'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /console\.log\s*\(/,
|
|
77
|
+
type: 'ProductionRisk',
|
|
78
|
+
message: '[Observability] console.log used in production code. Use a structured logger (winston/pino).'
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// Line-based checks
|
|
83
|
+
for (let i = 0; i < lines.length; i++) {
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
for (const risk of risks) {
|
|
86
|
+
// Pattern check
|
|
87
|
+
if (risk.pattern && risk.pattern.test(line)) {
|
|
88
|
+
// Skip comments for simple cases
|
|
89
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
hasError: true,
|
|
93
|
+
error: risk.message,
|
|
94
|
+
line: i + 1,
|
|
95
|
+
type: risk.type,
|
|
96
|
+
file: fileName
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Line-based Condition check (special case for timeout)
|
|
101
|
+
// We distinguish global vs line condition by context?
|
|
102
|
+
// The 'app.listen' check is clearly global (checks whole code).
|
|
103
|
+
// The 'timeout' check is clearly line based (checks 'l').
|
|
104
|
+
// Let's rely on the message content to know if it is line-based for MVP simplicity
|
|
105
|
+
if (risk.condition && risk.message.includes('HTTP missing timeout') && risk.condition(line)) {
|
|
106
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
|
107
|
+
return {
|
|
108
|
+
hasError: true,
|
|
109
|
+
error: risk.message,
|
|
110
|
+
line: i + 1,
|
|
111
|
+
type: risk.type,
|
|
112
|
+
file: fileName
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// File-based checks (Global patterns)
|
|
119
|
+
for (const risk of risks) {
|
|
120
|
+
// Global Condition check
|
|
121
|
+
if (risk.condition && !risk.message.includes('HTTP missing timeout') && risk.condition(code)) {
|
|
122
|
+
return {
|
|
123
|
+
hasError: true,
|
|
124
|
+
error: risk.message,
|
|
125
|
+
line: 0,
|
|
126
|
+
type: risk.type,
|
|
127
|
+
file: fileName
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Multi-line regex checks
|
|
131
|
+
if (risk.pattern && risk.pattern.flags.includes('s') && risk.pattern.test(code)) {
|
|
132
|
+
return {
|
|
133
|
+
hasError: true,
|
|
134
|
+
error: risk.message,
|
|
135
|
+
line: 0,
|
|
136
|
+
type: risk.type,
|
|
137
|
+
file: fileName
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { hasError: false };
|
|
143
|
+
}
|
package/src/repoAnalyzer.ts
CHANGED
|
@@ -2,12 +2,14 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import { analyzePythonCode, AnalysisResult } from './analyzer.js';
|
|
5
|
+
import { analyzeJsTsCode } from './jsAnalyzer.js';
|
|
5
6
|
import { depsScan } from './archScanner.js';
|
|
6
7
|
|
|
7
8
|
export interface RepoAnalysisResult {
|
|
8
9
|
score: number; // Release Readiness Score (RRS)
|
|
9
10
|
status: 'Ready for Production' | 'Needs Attention' | 'Not Ready for Deployment';
|
|
10
11
|
findings: {
|
|
12
|
+
id: string;
|
|
11
13
|
title: string;
|
|
12
14
|
file: string;
|
|
13
15
|
line: number;
|
|
@@ -37,88 +39,152 @@ export interface RepoAnalysisResult {
|
|
|
37
39
|
/**
|
|
38
40
|
* 3-Tier Business Audit Translation
|
|
39
41
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* 3-Tier Business Audit Translation with Standard IDs
|
|
44
|
+
*/
|
|
45
|
+
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 } {
|
|
46
|
+
const commonFields = { id };
|
|
47
|
+
|
|
48
|
+
if (id === 'RR-SEC-001' || type === 'SecurityRisk') {
|
|
42
49
|
return {
|
|
50
|
+
...commonFields,
|
|
43
51
|
title: "보μ μ·¨μ½μ μν (Security Vulnerability)",
|
|
44
52
|
category: 'Security',
|
|
45
53
|
evidence: issue,
|
|
46
54
|
standard: "OWASP Top 10 A03:2021 β Injection",
|
|
47
|
-
impact: "μΈλΆ 곡격μκ° μμ€ν
κΆνμ νμ·¨νκ±°λ λ―Όκ° μ 보λ₯Ό μ μΆν μ μλ
|
|
48
|
-
action:
|
|
55
|
+
impact: "μΈλΆ 곡격μκ° μμ€ν
κΆνμ νμ·¨νκ±°λ λ―Όκ° μ 보λ₯Ό μ μΆν μ μλ μ‘°κ±΄μ΄ νμ±λ©λλ€.",
|
|
56
|
+
action: `
|
|
57
|
+
# Action: 격리 λ° νκ²½λ³μ μ¬μ©
|
|
58
|
+
subprocess.run(..., shell=False) # κΆμ₯
|
|
59
|
+
# λλ .env νμΌ μ¬μ©
|
|
60
|
+
import os
|
|
61
|
+
SECRET = os.getenv('MY_SECRET')
|
|
62
|
+
`,
|
|
49
63
|
reference: "https://docs.python.org/3/library/subprocess.html#security-considerations",
|
|
50
|
-
whenItMatters: "λ°°ν¬ μ¦μ μλνλ μ€μΊλλ 곡격μμ μν΄ νμ§λ μ
|
|
64
|
+
whenItMatters: "λ°°ν¬ μ¦μ μλνλ μ€μΊλλ 곡격μμ μν΄ νμ§λ μ μμ΅λλ€."
|
|
51
65
|
};
|
|
52
66
|
}
|
|
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
|
-
}
|
|
67
|
+
|
|
68
|
+
if (id === 'RR-TEST-001') {
|
|
90
69
|
return {
|
|
91
|
-
|
|
70
|
+
...commonFields,
|
|
71
|
+
title: "μλν ν
μ€νΈ λΆμ¬ (Missing Automated Tests)",
|
|
92
72
|
category: 'Service Interruption',
|
|
93
|
-
evidence:
|
|
94
|
-
standard: "
|
|
95
|
-
impact: "
|
|
96
|
-
action:
|
|
97
|
-
|
|
98
|
-
|
|
73
|
+
evidence: "tests/ λλ ν 리 λλ pytest/unittest κ΄λ ¨ μ€μ μ μ°Ύμ μ μμ΅λλ€.",
|
|
74
|
+
standard: "pytest Framework Documentation",
|
|
75
|
+
impact: "μ½λ λ³κ²½ μ κΈ°μ‘΄ κΈ°λ₯μ΄ νκ΄΄λμλμ§ νμΈν λ°©λ²μ΄ μμ΄, λ°°ν¬ ν μ₯μ λ°μ νλ₯ μ΄ λμμ§λλ€.",
|
|
76
|
+
action: `
|
|
77
|
+
# Action: Create tests/test_smoke.py
|
|
78
|
+
def test_health_check():
|
|
79
|
+
assert True # Basic sanity check
|
|
80
|
+
`,
|
|
81
|
+
reference: "https://docs.pytest.org/",
|
|
82
|
+
whenItMatters: "νμμ΄ 2λͺ
μ΄μμΌλ‘ λμ΄λκ±°λ λ°°ν¬ μ£ΌκΈ°κ° λΉ¨λΌμ§ λ."
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (id === 'RR-CI-001') {
|
|
87
|
+
return {
|
|
88
|
+
...commonFields,
|
|
89
|
+
title: "λ°°ν¬ μλν νμ΄νλΌμΈ λΆμ¬ (Missing CI Pipeline)",
|
|
90
|
+
category: 'Service Interruption',
|
|
91
|
+
evidence: "GitHub Actions (.github/workflows/*.yml) λλ CI μ€μ νμΌμ΄ μμ΅λλ€.",
|
|
92
|
+
standard: "GitHub Actions Documentation",
|
|
93
|
+
impact: "μ¬λμ μλ λ°°ν¬ κ³Όμ μμ μ€μκ° λ°μν μ μμΌλ©°, μΌκ΄λ λ°°ν¬ μνλ₯Ό 보μ₯ν μ μμ΅λλ€.",
|
|
94
|
+
action: `
|
|
95
|
+
# Action: Create .github/workflows/ci.yml
|
|
96
|
+
name: CI
|
|
97
|
+
on: [push]
|
|
98
|
+
jobs:
|
|
99
|
+
test:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v3
|
|
103
|
+
- run: npm test
|
|
104
|
+
`,
|
|
105
|
+
reference: "https://docs.github.com/en/actions",
|
|
106
|
+
whenItMatters: "λ°°ν¬ λΉλκ° μ£Ό 2ν μ΄μμΌλ‘ μ¦κ°ν λ."
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (id === 'RR-OPS-001') {
|
|
111
|
+
return {
|
|
112
|
+
...commonFields,
|
|
113
|
+
title: "μ΄μ κΈ°λ³Έ μμ μ²΄ν¬ μ€ν¨ (Project Hygiene)",
|
|
114
|
+
category: 'Service Interruption',
|
|
115
|
+
evidence: issue, // Consolidated list will be passed here
|
|
116
|
+
standard: "12-Factor App / Docker Documentation",
|
|
117
|
+
impact: "κ°λ° νκ²½κ³Ό μ΄μ νκ²½μ λΆμΌμΉλ‘ μΈν΄ 'λ΄ μ»΄ν¨ν°μμλ λλλ° μλ²μμλ μ λλ' λ¬Έμ κ° λ°μν©λλ€.",
|
|
118
|
+
action: `
|
|
119
|
+
# Checklist to Fix:
|
|
120
|
+
1. Create 'Dockerfile'
|
|
121
|
+
2. Create '.gitignore' (use gitignore.io)
|
|
122
|
+
3. Create 'requirements.txt' or 'package.json'
|
|
123
|
+
4. Create '.env.example'
|
|
124
|
+
`,
|
|
125
|
+
reference: "https://12factor.net/",
|
|
126
|
+
whenItMatters: "μ κ· μ
μ¬μ μ¨λ³΄λ© λλ μλ² μ΄κ΄ μ."
|
|
99
127
|
};
|
|
100
128
|
}
|
|
101
|
-
|
|
129
|
+
|
|
130
|
+
if (id === 'RR-LOG-001') {
|
|
102
131
|
return {
|
|
132
|
+
...commonFields,
|
|
133
|
+
title: "λ‘κΉ
μ€μ λ―Έν‘ (Insufficient Logging)",
|
|
134
|
+
category: 'Maintenance',
|
|
135
|
+
evidence: "μ½λ λ΄μμ λ‘κΉ
μ€μ (logging, loguru λ±)μ΄ λ°κ²¬λμ§ μμμ΅λλ€.",
|
|
136
|
+
standard: "Python Logging Cookbook",
|
|
137
|
+
impact: "μ₯μ λ°μ μ μμΈμ μΆμ ν μ μλ λ°μ΄ν°κ° μμ΄ ν΄κ²° μκ°μ΄ κΈΈμ΄μ§λλ€.",
|
|
138
|
+
action: `
|
|
139
|
+
# Action: Python Logging Setup
|
|
140
|
+
import logging
|
|
141
|
+
logging.basicConfig(level=logging.INFO)
|
|
142
|
+
logger = logging.getLogger(__name__)
|
|
143
|
+
logger.info("Server started")
|
|
144
|
+
`,
|
|
145
|
+
reference: "https://docs.python.org/3/howto/logging-cookbook.html",
|
|
146
|
+
whenItMatters: "μ΄μ μ€ μ μ μλ 500 μλ¬κ° λ°μνμ λ."
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (id === 'RR-DEP-001' || type === 'CircularDependency') {
|
|
151
|
+
return {
|
|
152
|
+
...commonFields,
|
|
103
153
|
title: "ꡬ쑰μ μμ‘΄μ± κ²°ν¨ (Structural Dependency Issue)",
|
|
104
154
|
category: 'Scalability',
|
|
105
|
-
evidence:
|
|
155
|
+
evidence: issue,
|
|
106
156
|
standard: "Clean Architecture: Dependency Rule",
|
|
107
|
-
impact: "λͺ¨λ κ° κ²°ν©λκ° λμμ Έ
|
|
108
|
-
action: "
|
|
109
|
-
reference: "https://
|
|
110
|
-
whenItMatters: "νλ‘μ νΈ
|
|
157
|
+
impact: "λͺ¨λ κ° κ²°ν©λκ° λμμ Έ μ μ§λ³΄μκ° μ΄λ €μμ§κ³ , μ¬μ΄λ μ΄ννΈκ° λ°μνκΈ° μ½μ΅λλ€.",
|
|
158
|
+
action: "μνΈ μ°Έμ‘°νλ λͺ¨λμ λΆλ¦¬νκ±°λ κ³΅ν΅ λͺ¨λλ‘ μΆμΆνμΈμ.",
|
|
159
|
+
reference: "https://refactoring.guru/design-patterns",
|
|
160
|
+
whenItMatters: "νλ‘μ νΈ κ·λͺ¨κ° 컀μ§μλ‘ λ¦¬ν©ν λ§ λΉμ©μ΄ κΈ°νκΈμμ μΌλ‘ μ¦κ°ν©λλ€."
|
|
161
|
+
};
|
|
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: "κΈ°λ₯ μΆκ° μλ§λ€ λ²κ·Έκ° λ°μν λ."
|
|
111
175
|
};
|
|
112
176
|
}
|
|
177
|
+
|
|
113
178
|
return {
|
|
179
|
+
...commonFields,
|
|
114
180
|
title: "κΈ°ν μ μ¬μ 리μ€ν¬ (Other Potential Risks)",
|
|
115
181
|
category: 'Maintenance',
|
|
116
182
|
evidence: issue,
|
|
117
183
|
standard: "General Coding Best Practices",
|
|
118
|
-
impact: "
|
|
119
|
-
action: "
|
|
184
|
+
impact: "μ μ¬μ μΈ λ²κ·Έλ μ μ§λ³΄μ μ΄λ €μμ΄ μμ μ μμ΅λλ€.",
|
|
185
|
+
action: "ν΄λΉ μ½λλ₯Ό 리뷰νκ³ λ¦¬ν©ν λ§μ κ³ λ €νμΈμ.",
|
|
120
186
|
reference: "#",
|
|
121
|
-
whenItMatters: "μ§μμ μΈ
|
|
187
|
+
whenItMatters: "μ§μμ μΈ μ½λ νμ§ μ νκ° μ°λ €λ λ."
|
|
122
188
|
};
|
|
123
189
|
}
|
|
124
190
|
|
|
@@ -147,43 +213,66 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
147
213
|
let operationalGapCount = 0;
|
|
148
214
|
|
|
149
215
|
// 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
|
-
}
|
|
216
|
+
// 1. Operational Audit & Consolidation
|
|
217
|
+
const hygieneMissing: string[] = [];
|
|
218
|
+
|
|
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++;
|
|
180
231
|
}
|
|
181
232
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
233
|
+
// Check CI
|
|
234
|
+
const hasCI = fs.readdirSync(repoPath, { withFileTypes: true })
|
|
235
|
+
.some(d => d.isDirectory() && /(\.github|\.circleci|jenkins|gitlab)/i.test(d.name));
|
|
236
|
+
if (!hasCI) {
|
|
237
|
+
const details = getAuditDetails('RR-CI-001', 'ProductionRisk', '');
|
|
238
|
+
findings.push({
|
|
239
|
+
file: 'Repository Root',
|
|
240
|
+
line: 0,
|
|
241
|
+
type: 'ProductionRisk',
|
|
242
|
+
...details
|
|
243
|
+
});
|
|
244
|
+
operationalGapCount++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check Hygiene (Docker, Pinning, Env, GitIgnore)
|
|
248
|
+
const allFiles = fs.readdirSync(repoPath);
|
|
249
|
+
|
|
250
|
+
// Docker
|
|
251
|
+
if (!allFiles.some(f => /Dockerfile|docker-compose/i.test(f))) {
|
|
252
|
+
hygieneMissing.push("β Dockerfile or docker-compose.yml missing");
|
|
253
|
+
}
|
|
254
|
+
// Pinning
|
|
255
|
+
if (!allFiles.some(f => /requirements\.txt|poetry\.lock|pyproject\.toml|package-lock\.json|pnpm-lock\.yaml/i.test(f))) {
|
|
256
|
+
hygieneMissing.push("β Dependency Lockfile (requirements.txt, package-lock.json, etc) missing");
|
|
257
|
+
}
|
|
258
|
+
// Env
|
|
259
|
+
if (!allFiles.some(f => /\.env\.example|\.env\.sample/i.test(f))) {
|
|
260
|
+
hygieneMissing.push("β .env.example (Safe environment template) missing");
|
|
261
|
+
}
|
|
262
|
+
// GitIgnore
|
|
263
|
+
if (!fs.existsSync(path.join(repoPath, '.gitignore'))) {
|
|
264
|
+
hygieneMissing.push("β .gitignore missing");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (hygieneMissing.length > 0) {
|
|
268
|
+
const evidenceBlock = "\n" + hygieneMissing.join("\n");
|
|
269
|
+
const details = getAuditDetails('RR-OPS-001', 'ProductionRisk', evidenceBlock);
|
|
270
|
+
findings.push({
|
|
271
|
+
file: 'Repository Root',
|
|
272
|
+
line: 0,
|
|
273
|
+
type: 'ProductionRisk',
|
|
274
|
+
...details
|
|
275
|
+
});
|
|
187
276
|
operationalGapCount++;
|
|
188
277
|
}
|
|
189
278
|
|
|
@@ -203,7 +292,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
203
292
|
|
|
204
293
|
// God Module Check (>500 lines)
|
|
205
294
|
if (lines.length > 500) {
|
|
206
|
-
const details = getAuditDetails('
|
|
295
|
+
const details = getAuditDetails('RR-LINT-001', 'GodModule', `File length: ${lines.length} lines`);
|
|
207
296
|
findings.push({ file: relativePath, line: 0, type: 'ProductionRisk', ...details });
|
|
208
297
|
operationalGapCount++;
|
|
209
298
|
}
|
|
@@ -211,7 +300,34 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
211
300
|
const result = await analyzePythonCode(code, relativePath);
|
|
212
301
|
|
|
213
302
|
if (result.hasError) {
|
|
214
|
-
const details = getAuditDetails(result.type || 'Error', result.error || 'Unknown Issue');
|
|
303
|
+
const details = getAuditDetails('RR-SEC-001', result.type || 'Error', result.error || 'Unknown Issue');
|
|
304
|
+
findings.push({
|
|
305
|
+
file: relativePath,
|
|
306
|
+
line: result.line || 0,
|
|
307
|
+
type: result.type || 'Error',
|
|
308
|
+
...details
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (result.type === 'SecurityRisk') criticalCount++;
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error(`Error analyzing ${f}:`, e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 2.1 Code Level Analysis (JS/TS logic)
|
|
319
|
+
for (const f of files.filter(f => /\.(js|ts|jsx|tsx)$/.test(f))) {
|
|
320
|
+
try {
|
|
321
|
+
const code = fs.readFileSync(f, 'utf8');
|
|
322
|
+
const relativePath = path.relative(repoPath, f);
|
|
323
|
+
|
|
324
|
+
// Logging Check (Simple console.log check is in rules, but here specific frameworks?)
|
|
325
|
+
// We rely on rules for now.
|
|
326
|
+
|
|
327
|
+
const result = await analyzeJsTsCode(code, relativePath);
|
|
328
|
+
|
|
329
|
+
if (result.hasError) {
|
|
330
|
+
const details = getAuditDetails('RR-SEC-002', result.type || 'Error', result.error || 'Unknown Issue'); // Use SEC-002 for JS? Or reuse.
|
|
215
331
|
findings.push({
|
|
216
332
|
file: relativePath,
|
|
217
333
|
line: result.line || 0,
|
|
@@ -227,7 +343,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
227
343
|
}
|
|
228
344
|
|
|
229
345
|
if (!hasLogging && files.filter(f => f.endsWith('.py')).length > 0) {
|
|
230
|
-
const details = getAuditDetails('
|
|
346
|
+
const details = getAuditDetails('RR-LOG-001', 'ProductionRisk', '');
|
|
231
347
|
findings.push({ file: 'Repository Root', line: 0, type: 'ProductionRisk', ...details });
|
|
232
348
|
operationalGapCount++;
|
|
233
349
|
}
|
|
@@ -235,7 +351,7 @@ export async function analyzeRepository(repoPath: string, resultsDir?: string):
|
|
|
235
351
|
// 3. Dependency Scan (Scalability -15)
|
|
236
352
|
const { adj, cycles } = depsScan(repoPath);
|
|
237
353
|
for (const cycle of cycles) {
|
|
238
|
-
const details = getAuditDetails('CircularDependency', '');
|
|
354
|
+
const details = getAuditDetails('RR-DEP-001', 'CircularDependency', `Cycle: ${cycle.join(' -> ')}`);
|
|
239
355
|
findings.push({
|
|
240
356
|
file: cycle[0],
|
|
241
357
|
line: 0,
|