diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 864111f..3da7d3d 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -45,7 +45,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} review_type: full language: ko - file_patterns: "src/**/*.js,test-files/**/*.js,*.yml,*.json,*.md" + file_patterns: "**/*.js,**/*.ts,**/*.jsx,**/*.tsx,**/*.py,**/*.java,**/*.go,**/*.rs" exclude_patterns: "dist/**,node_modules/**,test/**" max_files: 8 severity_filter: medium diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..033cc0c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 chimaek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index fef9de1..281c551 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Claude API를 활용한 지능형 AI 코드 리뷰 GitHub Action입니다. Pull ... (상세한 이슈 목록) ``` -![PR Review Comment](https://github.com/chimaek/claude-code-review-action/blob/master/example/images/pr_example.png) +![PR Review Comment](https://github.com/chimaek/claude-code-review-action/blob/master/examples/images/pr_example.png) ## 🚀 빠른 시작 diff --git a/examples/DatabaseManager.java b/examples/DatabaseManager.java new file mode 100644 index 0000000..1ea6657 --- /dev/null +++ b/examples/DatabaseManager.java @@ -0,0 +1,194 @@ +// Java 예시: 데이터베이스 매니저 (보안 및 성능 이슈) +package com.example.database; + +import java.sql.*; +import java.util.*; +import java.io.FileWriter; +import java.io.IOException; +import java.util.logging.Logger; + +public class DatabaseManager { + // 보안 이슈: 하드코딩된 데이터베이스 정보 + private static final String DB_URL = "jdbc:mysql://localhost:3306/mydb"; + private static final String USERNAME = "admin"; + private static final String PASSWORD = "password123"; + + private Connection connection; + private static Logger logger = Logger.getLogger(DatabaseManager.class.getName()); + + // 성능 이슈: 싱글톤 패턴 미사용, 매번 새 연결 + public DatabaseManager() { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + connection = DriverManager.getConnection(DB_URL, USERNAME, PASSWORD); + } catch (Exception e) { + // 보안 이슈: 상세한 에러 정보 노출 + logger.severe("Database connection failed: " + e.getMessage()); + e.printStackTrace(); + } + } + + // 보안 이슈: SQL Injection 취약점 + public List getUsersByRole(String role) { + List users = new ArrayList<>(); + + try { + // 파라미터 바인딩 없이 문자열 연결 + String query = "SELECT * FROM users WHERE role = '" + role + "'"; + logger.info("Executing query: " + query); // 쿼리 로깅 (정보 노출) + + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query); + + while (rs.next()) { + User user = new User(); + user.setId(rs.getInt("id")); + user.setUsername(rs.getString("username")); + user.setPassword(rs.getString("password")); // 보안 이슈: 비밀번호 포함 + user.setRole(rs.getString("role")); + users.add(user); + } + + // 리소스 해제 안함 (메모리 누수) + + } catch (SQLException e) { + logger.severe("Query execution failed: " + e.getMessage()); + } + + return users; + } + + // 성능 이슈: N+1 쿼리 문제 + public void updateUserProfiles(List userIds) { + for (Integer userId : userIds) { + try { + // 각 사용자마다 개별 쿼리 실행 + String query = "UPDATE users SET last_updated = NOW() WHERE id = ?"; + PreparedStatement stmt = connection.prepareStatement(query); + stmt.setInt(1, userId); + stmt.executeUpdate(); + + // 연결 닫지 않음 (리소스 누수) + + } catch (SQLException e) { + logger.severe("Update failed for user " + userId + ": " + e.getMessage()); + } + } + } + + // 보안 이슈: 권한 검증 없는 삭제 + public boolean deleteUser(String username) { + try { + // 권한 확인 없이 삭제 실행 + String query = "DELETE FROM users WHERE username = '" + username + "'"; + Statement stmt = connection.createStatement(); + int affectedRows = stmt.executeUpdate(query); + + // 보안 이슈: 삭제된 사용자 정보 로깅 + logger.info("Deleted user: " + username + ", affected rows: " + affectedRows); + + return affectedRows > 0; + } catch (SQLException e) { + logger.severe("User deletion failed: " + e.getMessage()); + return false; + } + } + + // 성능 이슈: 대용량 데이터를 한번에 메모리로 로드 + public List getAllUserEmails() { + List emails = new ArrayList<>(); + + try { + String query = "SELECT email FROM users"; // LIMIT 없음 + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query); + + // 모든 결과를 메모리에 저장 + while (rs.next()) { + emails.add(rs.getString("email")); + } + + } catch (SQLException e) { + logger.severe("Failed to fetch emails: " + e.getMessage()); + } + + return emails; + } + + // 보안 이슈: 민감한 정보를 평문 파일로 저장 + public void exportUserData(List users) { + try { + FileWriter writer = new FileWriter("user_export.txt"); + + for (User user : users) { + // 비밀번호 포함하여 내보내기 + String line = user.getId() + "," + user.getUsername() + "," + + user.getPassword() + "," + user.getRole() + "\n"; + writer.write(line); + } + + writer.close(); + logger.info("User data exported to user_export.txt"); + + } catch (IOException e) { + logger.severe("Export failed: " + e.getMessage()); + } + } + + // 코드 스타일 이슈: 네이밍 컨벤션 위반 + public void ExecuteBatchOperation(List SQLQueries) { + try { + connection.setAutoCommit(false); + Statement stmt = connection.createStatement(); + + // 코드 스타일 이슈: 일관성 없는 들여쓰기 + for(String query : SQLQueries) { + stmt.addBatch(query); // SQL Injection 가능 + logger.info("Added to batch: " + query); + } + + int[] results = stmt.executeBatch(); + connection.commit(); + + // 성능 이슈: 불필요한 로깅 + for (int i = 0; i < results.length; i++) { + logger.info("Batch operation " + i + " affected " + results[i] + " rows"); + } + + } catch (SQLException e) { + try { + connection.rollback(); + } catch (SQLException rollbackError) { + logger.severe("Rollback failed: " + rollbackError.getMessage()); + } + logger.severe("Batch operation failed: " + e.getMessage()); + } + } + + // 메모리 누수: finalize에서 리소스 정리 (잘못된 패턴) + @Override + protected void finalize() throws Throwable { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + super.finalize(); + } +} + +// 코드 스타일 이슈: 같은 파일에 여러 클래스 +class User { + private int id; + private String username; + private String password; + private String role; + + // getter/setter 생략 + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } +} \ No newline at end of file diff --git a/examples/data_analyzer.py b/examples/data_analyzer.py new file mode 100644 index 0000000..f712968 --- /dev/null +++ b/examples/data_analyzer.py @@ -0,0 +1,123 @@ +# Python 예시: 데이터 분석기 (성능 및 보안 이슈) +import os +import pickle +import hashlib +import subprocess +import pandas as pd +from typing import List, Dict, Any + +class DataAnalyzer: + def __init__(self, data_path: str): + self.data_path = data_path + self.cache = {} + self.api_key = "sk-1234567890abcdef" # 보안 이슈: 하드코딩된 API 키 + + # 보안 이슈: 경로 순회 공격 가능 + def load_data(self, filename: str) -> pd.DataFrame: + # 입력 검증 없음 + full_path = os.path.join(self.data_path, filename) + print(f"Loading data from: {full_path}") # 경로 정보 노출 + + try: + # 보안 이슈: pickle.load 사용 (코드 실행 가능) + if filename.endswith('.pkl'): + with open(full_path, 'rb') as f: + return pickle.load(f) # 매우 위험! + else: + return pd.read_csv(full_path) + except Exception as e: + print(f"Error loading data: {str(e)}") # 에러 정보 노출 + return pd.DataFrame() + + # 성능 이슈: 비효율적인 데이터 처리 + def analyze_data(self, data: pd.DataFrame) -> Dict[str, Any]: + results = {} + + # 성능 이슈: 반복문으로 통계 계산 + for column in data.columns: + column_stats = [] + for value in data[column]: + # 매우 비효율적인 처리 + if str(value).isdigit(): + column_stats.append(float(value)) + + if column_stats: + results[column] = { + 'mean': sum(column_stats) / len(column_stats), + 'max': max(column_stats), + 'min': min(column_stats) + } + + return results + + # 보안 이슈: 명령어 인젝션 가능 + def execute_query(self, query: str) -> str: + # 입력 검증 없이 시스템 명령어 실행 + command = f"mysql -e '{query}'" + result = subprocess.run(command, shell=True, capture_output=True, text=True) + return result.stdout + + # 메모리 누수: 캐시 정리 없음 + def cache_result(self, key: str, data: Any) -> None: + # 캐시 크기 제한 없음 + self.cache[key] = data + print(f"Cached {len(str(data))} bytes for key: {key}") + + # 보안 이슈: 약한 해시 함수 사용 + def generate_hash(self, data: str) -> str: + # MD5는 보안상 취약함 + return hashlib.md5(data.encode()).hexdigest() + + # 성능 이슈: 중첩 반복문 + def find_duplicates(self, data_list: List[Dict]) -> List[Dict]: + duplicates = [] + + # O(n²) 복잡도 + for i in range(len(data_list)): + for j in range(i + 1, len(data_list)): + if data_list[i] == data_list[j]: + duplicates.append(data_list[i]) + + return duplicates + + # 코드 스타일 이슈: 네이밍 컨벤션 위반 + def ProcessLargeDataset(self, DataSet: List) -> None: + # 변수명이 일관성 없음 + processedData = [] + Total_Count = 0 + + for item in DataSet: + # 들여쓰기 일관성 없음 + processed_item = self.process_item(item) + processedData.append(processed_item) + Total_Count += 1 + + print(f"Processed {Total_Count} items") + + def process_item(self, item): + # 타입 힌트 없음 + return item + + # 보안 이슈: 민감한 정보 로깅 + def authenticate_user(self, username: str, password: str) -> bool: + print(f"Authenticating user: {username} with password: {password}") # 비밀번호 로깅! + + # 보안 이슈: 약한 비밀번호 정책 + if len(password) >= 4: + return True + return False + + # 메모리 누수: 대용량 데이터 처리 시 문제 + def load_all_data(self) -> List[pd.DataFrame]: + all_data = [] + + # 메모리 사용량 고려 없음 + for filename in os.listdir(self.data_path): + if filename.endswith(('.csv', '.pkl')): + data = self.load_data(filename) + all_data.append(data) # 모든 데이터를 메모리에 보관 + + return all_data + +# 모듈 레벨에서 인스턴스 생성 (좋지 않은 패턴) +analyzer = DataAnalyzer("/tmp/data") \ No newline at end of file diff --git a/examples/file_processor.go b/examples/file_processor.go new file mode 100644 index 0000000..b06cc4f --- /dev/null +++ b/examples/file_processor.go @@ -0,0 +1,212 @@ +// Go 예시: 파일 처리기 (보안 및 성능 이슈) +package main + +import ( + "bufio" + "crypto/md5" // 보안 이슈: 약한 해시 함수 + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// 보안 이슈: 전역 변수에 민감한 정보 +var ( + API_KEY = "secret-api-key-12345" + SECRET_KEY = "my-secret-key" +) + +type FileProcessor struct { + basePath string + cache map[string][]byte // 메모리 누수 가능성 + processedFiles int +} + +// 생성자에서 검증 부족 +func NewFileProcessor(basePath string) *FileProcessor { + return &FileProcessor{ + basePath: basePath, // 경로 검증 없음 + cache: make(map[string][]byte), + } +} + +// 보안 이슈: 경로 순회 공격 가능 +func (fp *FileProcessor) ReadFile(filename string) ([]byte, error) { + // 입력 검증 없음 + fullPath := filepath.Join(fp.basePath, filename) + + // 보안 이슈: 경로 정보 로깅 + log.Printf("Reading file: %s", fullPath) + + // 성능 이슈: 파일을 통째로 메모리에 로드 + data, err := ioutil.ReadFile(fullPath) + if err != nil { + // 보안 이슈: 에러 정보 노출 + log.Printf("File read error: %v", err) + return nil, err + } + + // 메모리 누수: 캐시 크기 제한 없음 + fp.cache[filename] = data + + return data, nil +} + +// 보안 이슈: 명령어 인젝션 가능 +func (fp *FileProcessor) ExecuteCommand(command string) (string, error) { + // 입력 검증 없이 시스템 명령어 실행 + log.Printf("Executing command: %s", command) // 명령어 로깅 + + cmd := exec.Command("sh", "-c", command) // 매우 위험! + output, err := cmd.Output() + + if err != nil { + log.Printf("Command execution failed: %v", err) + return "", err + } + + return string(output), nil +} + +// 성능 이슈: 비효율적인 파일 처리 +func (fp *FileProcessor) ProcessFiles(pattern string) error { + files, err := filepath.Glob(filepath.Join(fp.basePath, pattern)) + if err != nil { + return err + } + + // 성능 이슈: 모든 파일을 순차 처리 (병렬 처리 없음) + for _, file := range files { + // 성능 이슈: 매번 파일 크기 확인 + info, err := os.Stat(file) + if err != nil { + continue + } + + // 대용량 파일도 동일하게 처리 + if info.Size() > 0 { + data, err := fp.ReadFile(filepath.Base(file)) + if err != nil { + continue + } + + // 성능 이슈: 불필요한 문자열 변환 + content := string(data) + + // 보안 이슈: 약한 해시 함수 사용 + hash := md5.Sum(data) + log.Printf("Processed file: %s, hash: %x", file, hash) + + // 성능 이슈: 비효율적인 문자열 검색 + for i := 0; i < len(content); i++ { + if strings.HasPrefix(content[i:], "password") { + log.Printf("Found password reference in %s at position %d", file, i) + } + } + } + + fp.processedFiles++ + } + + return nil +} + +// 메모리 누수: 무한 성장하는 캐시 +func (fp *FileProcessor) GetFromCache(filename string) []byte { + // 캐시 만료 로직 없음 + return fp.cache[filename] +} + +// 보안 이슈: 민감한 정보 평문 저장 +func (fp *FileProcessor) SaveConfig(config map[string]string) error { + file, err := os.Create("config.txt") + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + + for key, value := range config { + // 비밀번호도 평문으로 저장 + line := fmt.Sprintf("%s=%s\n", key, value) + writer.WriteString(line) + } + + writer.Flush() + + // 보안 이슈: 파일 권한 설정 없음 (기본적으로 모든 사용자가 읽기 가능) + log.Println("Configuration saved to config.txt") + + return nil +} + +// 성능 이슈: 고루틴 누수 가능성 +func (fp *FileProcessor) StartBackgroundProcessor() { + // 고루틴 생명주기 관리 없음 + go func() { + for { + // 무한 루프에서 지속적으로 작업 + time.Sleep(1 * time.Second) + + // 성능 이슈: 매초마다 모든 파일 스캔 + files, _ := filepath.Glob(fp.basePath + "/*") + for _, file := range files { + // 불필요한 파일 접근 + os.Stat(file) + } + + log.Printf("Background scan completed, processed files: %d", fp.processedFiles) + } + }() +} + +// 코드 스타일 이슈: 네이밍 컨벤션 위반 +func (fp *FileProcessor) Process_Large_File(FileName string) error { + // 변수명 일관성 없음 + file_path := filepath.Join(fp.basePath, FileName) + + // 들여쓰기 일관성 없음 + data, err := ioutil.ReadFile(file_path) + if err != nil { + return err + } + + // 성능 이슈: 대용량 파일을 한번에 처리 + lines := strings.Split(string(data), "\n") + + // 성능 이슈: 비효율적인 문자열 연산 + var result string + for _, line := range lines { + result += line + "\n" // 매번 새 문자열 생성 + } + + log.Printf("Processed %d lines", len(lines)) + return nil +} + +// 보안 이슈: 사용자 입력을 직접 시스템 명령어로 실행 +func (fp *FileProcessor) CustomCommand(userInput string) { + // 입력 검증 전혀 없음 + command := fmt.Sprintf("echo %s > output.txt", userInput) + + // 명령어 인젝션 취약점 + exec.Command("sh", "-c", command).Run() +} + +func main() { + processor := NewFileProcessor("/tmp") + + // 보안 이슈: 하드코딩된 패턴 + processor.ProcessFiles("*.txt") + + // 백그라운드 프로세서 시작 (고루틴 누수 위험) + processor.StartBackgroundProcessor() + + // 메인 고루틴 종료되지 않음 + select {} +} \ No newline at end of file diff --git a/example/images/pr_example.png b/examples/images/pr_example.png similarity index 100% rename from example/images/pr_example.png rename to examples/images/pr_example.png diff --git a/examples/payment-processor.ts b/examples/payment-processor.ts new file mode 100644 index 0000000..e57f69a --- /dev/null +++ b/examples/payment-processor.ts @@ -0,0 +1,100 @@ +// TypeScript 예시: 결제 처리기 (보안 및 성능 이슈) +import * as crypto from 'crypto'; + +interface PaymentRequest { + amount: number; + currency: string; + cardNumber: string; + cvv: string; + expiryDate: string; +} + +interface PaymentResponse { + success: boolean; + transactionId?: string; + error?: string; +} + +class PaymentProcessor { + private apiKey: string = process.env.PAYMENT_API_KEY || "default-key"; // 보안 이슈: 기본값 설정 + private transactions: PaymentRequest[] = []; // 메모리에 민감한 정보 저장 + + // 보안 이슈: 신용카드 정보 로깅 + async processPayment(request: PaymentRequest): Promise { + console.log("Processing payment:", JSON.stringify(request)); // 민감한 정보 로깅 + + // 입력 검증 부족 + if (!request.amount || request.amount <= 0) { + throw new Error("Invalid amount"); + } + + // 보안 이슈: 신용카드 번호 검증 없음 + if (request.cardNumber.length < 13) { + return { success: false, error: "Invalid card number" }; + } + + // 성능 이슈: 동기 처리 + const isValid = this.validateCard(request); + if (!isValid) { + return { success: false, error: "Card validation failed" }; + } + + // 보안 이슈: 약한 트랜잭션 ID 생성 + const transactionId = Math.random().toString(36).substr(2, 9); + + // 메모리에 민감한 정보 저장 + this.transactions.push(request); + + return { + success: true, + transactionId: transactionId + }; + } + + // 성능 이슈: 비효율적인 검색 + private validateCard(request: PaymentRequest): boolean { + // 실제 카드 검증 로직 없음 + for (let i = 0; i < 10000; i++) { + // 불필요한 반복 + if (request.cardNumber.includes("0000")) { + return false; + } + } + + // 보안 이슈: CVV 검증 없음 + return true; + } + + // 보안 이슈: 권한 검증 없이 모든 거래 반환 + public getAllTransactions(): PaymentRequest[] { + return this.transactions; + } + + // 메모리 누수: 거래 내역 정리 없음 + public getTransactionHistory(limit?: number): PaymentRequest[] { + const history = [...this.transactions]; + + // 성능 이슈: 매번 새 배열 생성 + if (limit) { + return history.slice(-limit); + } + + return history; + } + + // 보안 이슈: 평문으로 카드 정보 저장 + private storeCardInfo(cardNumber: string, cvv: string): void { + const cardInfo = { + number: cardNumber, // 평문 저장 + cvv: cvv, // 평문 저장 + timestamp: Date.now() + }; + + // 파일에 평문으로 저장 (매우 위험) + require('fs').appendFileSync('cards.txt', JSON.stringify(cardInfo) + '\n'); + } +} + +// 코드 스타일 이슈: export 방식 일관성 없음 +export default PaymentProcessor; +export { PaymentRequest, PaymentResponse }; \ No newline at end of file diff --git a/examples/security_manager.rs b/examples/security_manager.rs new file mode 100644 index 0000000..bf84863 --- /dev/null +++ b/examples/security_manager.rs @@ -0,0 +1,216 @@ +// Rust 예시: 보안 매니저 (안전하지 않은 코드 사용 및 보안 이슈) +use std::collections::HashMap; +use std::fs::File; +use std::io::{Read, Write}; +use std::process::Command; +use std::ffi::CString; +use std::ptr; + +// 보안 이슈: 하드코딩된 시크릿 +const SECRET_KEY: &str = "hardcoded-secret-key-123"; +const ADMIN_PASSWORD: &str = "admin123"; + +pub struct SecurityManager { + users: HashMap, + sessions: HashMap, + // 메모리 누수 가능성: Vec이 무제한 성장 + audit_log: Vec, +} + +impl SecurityManager { + pub fn new() -> Self { + Self { + users: HashMap::new(), + sessions: HashMap::new(), + audit_log: Vec::new(), + } + } + + // 보안 이슈: 평문 비밀번호 저장 + pub fn create_user(&mut self, username: String, password: String) -> bool { + // 입력 검증 없음 + if username.is_empty() || password.is_empty() { + return false; + } + + // 보안 이슈: 비밀번호 평문 저장 + self.users.insert(username.clone(), password); + + // 보안 이슈: 민감한 정보 로깅 + let log_entry = format!("User created: {} with password: {}", username, password); + self.audit_log.push(log_entry); + + true + } + + // 보안 이슈: 약한 세션 ID 생성 + pub fn authenticate(&mut self, username: &str, password: &str) -> Option { + if let Some(stored_password) = self.users.get(username) { + // 보안 이슈: 평문 비밀번호 비교 + if stored_password == password { + // 보안 이슈: 예측 가능한 세션 ID + let session_id = format!("session_{}", self.sessions.len()); + self.sessions.insert(session_id.clone(), 1); + + // 보안 이슈: 인증 정보 로깅 + println!("Authentication successful for: {}", username); + + return Some(session_id); + } + } + + // 보안 이슈: 사용자 존재 여부 유추 가능한 에러 메시지 + println!("Authentication failed: Invalid username or password"); + None + } + + // 안전하지 않은 코드: 메모리 안전성 위반 가능 + pub unsafe fn process_raw_data(&self, data: *const u8, len: usize) -> Vec { + let mut result = Vec::new(); + + // 경계 검사 없음 - 버퍼 오버플로우 가능 + for i in 0..len { + let byte_val = *data.offset(i as isize); + result.push(byte_val); + } + + result + } + + // 보안 이슈: 명령어 인젝션 취약점 + pub fn execute_system_command(&self, user_input: &str) -> Result { + // 입력 검증 없음 + let command = format!("echo {}", user_input); + + // 명령어 인젝션 가능 + let output = Command::new("sh") + .arg("-c") + .arg(&command) + .output()?; + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } + + // 메모리 누수: 무한 성장하는 로그 + pub fn add_audit_log(&mut self, message: String) { + // 로그 크기 제한 없음 + self.audit_log.push(message); + + // 성능 이슈: 매번 전체 로그 출력 + for (i, log) in self.audit_log.iter().enumerate() { + println!("Log {}: {}", i, log); + } + } + + // 보안 이슈: 민감한 정보를 평문 파일로 저장 + pub fn export_user_data(&self, filename: &str) -> Result<(), std::io::Error> { + let mut file = File::create(filename)?; + + // 사용자 데이터를 평문으로 저장 + for (username, password) in &self.users { + let line = format!("{}:{}\n", username, password); + file.write_all(line.as_bytes())?; + } + + // 보안 이슈: 파일 경로 로깅 + println!("User data exported to: {}", filename); + + Ok(()) + } + + // 안전하지 않은 코드: C 스타일 포인터 사용 + pub unsafe fn manipulate_memory(&self, ptr: *mut u8, size: usize) { + // 경계 검사 없음 + for i in 0..size * 2 { // 의도적으로 잘못된 크기 + *ptr.offset(i as isize) = 0xFF; // 버퍼 오버플로우 가능 + } + } + + // 성능 이슈: 비효율적인 검색 + pub fn find_user_by_partial_name(&self, partial: &str) -> Vec { + let mut results = Vec::new(); + + // O(n) 검색을 여러 번 수행 + for username in self.users.keys() { + // 성능 이슈: 비효율적인 문자열 비교 + for i in 0..username.len() { + if username[i..].starts_with(partial) { + results.push(username.clone()); + break; + } + } + } + + results + } + + // 보안 이슈: 권한 검증 없는 관리자 기능 + pub fn admin_reset_all_passwords(&mut self) { + // 권한 확인 없음 + for (username, password) in &mut self.users { + *password = "temp123".to_string(); // 모든 비밀번호를 약한 것으로 변경 + + // 보안 이슈: 비밀번호 변경 로깅 + println!("Reset password for user: {}", username); + } + } + + // 안전하지 않은 코드: 원시 포인터로 문자열 생성 + pub unsafe fn create_c_string(&self, data: &[u8]) -> *const i8 { + let c_string = CString::new(data).unwrap(); + let ptr = c_string.as_ptr(); + + // 메모리 해제 없이 포인터 반환 (use-after-free 가능성) + std::mem::forget(c_string); + + ptr + } +} + +// 전역 변수 (thread-safety 문제) +static mut GLOBAL_MANAGER: Option = None; + +// 보안 이슈: 스레드 안전성 없는 전역 접근 +pub unsafe fn get_global_manager() -> &'static mut SecurityManager { + if GLOBAL_MANAGER.is_none() { + GLOBAL_MANAGER = Some(SecurityManager::new()); + } + + GLOBAL_MANAGER.as_mut().unwrap() +} + +// 코드 스타일 이슈: 네이밍 컨벤션 위반 +pub struct User_Session { + pub Session_ID: String, + pub User_Name: String, + pub Is_Admin: bool, +} + +impl User_Session { + // 보안 이슈: 관리자 권한 체크 로직 결함 + pub fn Check_Admin_Access(&self) -> bool { + // 단순한 문자열 비교로 관리자 확인 + self.User_Name == "admin" || self.Session_ID.contains("admin") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unsafe_operations() { + let mut manager = SecurityManager::new(); + + unsafe { + // 안전하지 않은 테스트 코드 + let data = vec![1, 2, 3, 4, 5]; + let processed = manager.process_raw_data(data.as_ptr(), data.len()); + assert_eq!(processed, data); + + // 메모리 오류 가능성이 있는 테스트 + let mut buffer = vec![0u8; 10]; + manager.manipulate_memory(buffer.as_mut_ptr(), 5); // 의도적으로 작은 크기 + } + } +} \ No newline at end of file diff --git a/examples/simple-test.js b/examples/simple-test.js new file mode 100644 index 0000000..48a554d --- /dev/null +++ b/examples/simple-test.js @@ -0,0 +1,6 @@ +// 간단한 테스트 파일 - Action이 감지하는지 확인 +function simpleFunction() { + var password = "12345"; // 보안 이슈: 하드코딩된 비밀번호 + console.log("Password is: " + password); // 보안 이슈: 비밀번호 로깅 + return true; +} \ No newline at end of file diff --git a/examples/user-service.js b/examples/user-service.js new file mode 100644 index 0000000..27a6309 --- /dev/null +++ b/examples/user-service.js @@ -0,0 +1,91 @@ +// JavaScript 예시: 사용자 서비스 (다양한 이슈 포함) +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); + +class UserService { + constructor() { + this.users = new Map(); + this.SECRET_KEY = "hardcoded-secret-123"; // 보안 이슈: 하드코딩된 시크릿 + } + + // 성능 이슈: 비효율적인 검색 + findUserByEmail(email) { + for (let [id, user] of this.users) { + if (user.email === email) { + return user; + } + } + return null; + } + + // 보안 이슈: SQL Injection 가능성 + authenticateUser(email, password) { + const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`; + console.log("Executing query:", query); // 보안 이슈: 비밀번호 로깅 + + const user = this.findUserByEmail(email); + if (!user) return null; + + // 보안 이슈: 평문 비밀번호 비교 + if (user.password === password) { + return this.generateToken(user); + } + return null; + } + + // 코드 스타일 이슈: 네이밍 컨벤션 위반 + CreateNewUser(Email, Password, UserName) { + // 입력 검증 없음 (보안 이슈) + const userId = Math.random().toString(36); // 약한 ID 생성 + + const newUser = { + id: userId, + email: Email, + password: Password, // 평문 저장 (보안 이슈) + username: UserName, + createdAt: new Date() + }; + + this.users.set(userId, newUser); + return newUser; + } + + generateToken(user) { + // 보안 이슈: 토큰 만료시간 없음 + return jwt.sign( + { + userId: user.id, + email: user.email, + password: user.password // 보안 이슈: 토큰에 비밀번호 포함 + }, + this.SECRET_KEY + ); + } + + // 성능 이슈: 동기 처리 + validateToken(token) { + try { + const decoded = jwt.verify(token, this.SECRET_KEY); + return decoded; + } catch (error) { + console.log("Token validation error:", error.message); + return null; + } + } + + // 메모리 누수 가능성 + getUserSessions(userId) { + const sessions = []; + // 세션 정리 로직 없음 + for (let i = 0; i < 1000; i++) { + sessions.push({ + id: i, + userId: userId, + data: new Array(1000).fill("session-data") + }); + } + return sessions; + } +} + +module.exports = UserService; \ No newline at end of file diff --git a/src/code-reviewer.js b/src/code-reviewer.js index 0e66afb..c62289c 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -21,7 +21,7 @@ class CodeReviewer { // Claude API 클라이언트 초기화 this.client = new Anthropic({ apiKey }); this.language = language; - this.maxTokens = 2000; // Claude 응답 최대 토큰 수 (속도 개선) + this.maxTokens = 4000; // Claude 응답 최대 토큰 수 (JSON 완성도 향상) } /** @@ -38,19 +38,24 @@ class CodeReviewer { const prompt = this.buildPrompt(filename, content, diff, reviewType); try { - // Claude API 호출 + // Claude API 호출 (토큰 수 증가 및 스트림 비활성화) const response = await this.client.messages.create({ model: 'claude-sonnet-4-20250514', // 코드 분석에 적합한 모델 - max_tokens: this.maxTokens, + max_tokens: 8000, // 토큰 수 증가로 완전한 응답 보장 temperature: 0.1, // 일관성 있는 응답을 위해 낮은 temperature 사용 + system: "You are a senior code reviewer. CRITICAL: Always respond with complete, valid JSON format. Never truncate your response. Ensure the JSON is properly closed with all brackets and braces. Do not include any explanation, markdown, or other text outside the JSON structure.", messages: [{ role: 'user', content: prompt }] }); + const responseText = response.content[0].text; + console.log(`Response length: ${responseText.length} characters`); + console.log(`Response ends with: "${responseText.slice(-50)}"`); + // API 응답을 구조화된 형식으로 파싱 - return this.parseResponse(response.content[0].text); + return this.parseResponse(responseText); } catch (error) { throw new Error(`Claude API error: ${error.message}`); } @@ -79,7 +84,7 @@ class CodeReviewer { diff.substring(0, 1000) + '\n// ... (truncated)' : diff; - // 간결한 프롬프트 구성 (속도 개선) + // 명확한 JSON 형식 요청 return `${basePrompt} ${languageInstruction} 파일: ${filename} @@ -91,12 +96,10 @@ ${truncatedDiff ? `변경사항:\n\`\`\`diff\n${truncatedDiff}\n\`\`\`` : ''} ${truncatedContent} \`\`\` -JSON 응답 (간결하게): -{ - "summary": "요약", - "issues": [{"line": 0, "severity": "medium", "type": "bug", "title": "제목", "description": "설명", "suggestion": "제안"}], - "overall_score": 7 -}`; +**중요**: 완전한 JSON만 반환하세요. 최대 3개 이슈만 포함하고 간결하게 작성하세요. + +형식: +{"summary":"요약(30자)","issues":[{"line":숫자,"severity":"low/medium/high/critical","type":"bug/security/performance/style","title":"제목(20자)","description":"설명(50자)","suggestion":"제안(50자)"}],"overall_score":숫자}`; } /** @@ -174,46 +177,132 @@ JSON 응답 (간결하게): */ parseResponse(responseText) { try { - // JSON 응답 추출 (Claude가 마크다운 블록으로 감쌀 수 있음) - const jsonMatch = responseText.match(/\{[\s\S]*\}/); - if (!jsonMatch) { - throw new Error('No valid JSON found in response'); + // 디버깅을 위한 원본 응답 로깅 + console.log('Raw Claude response:', responseText.substring(0, 500)); + + // 1. 응답 텍스트 전처리 + let cleanedText = responseText.trim(); + + // 2. 여러 방법으로 JSON 추출 시도 + let jsonText = null; + + // 방법 1: 마크다운 코드 블록에서 추출 + const codeBlockMatch = cleanedText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); + if (codeBlockMatch) { + jsonText = codeBlockMatch[1].trim(); } - - const parsed = JSON.parse(jsonMatch[0]); - // 응답 형식 검증 - if (!parsed.summary || !Array.isArray(parsed.issues)) { - throw new Error('Invalid response format'); + // 방법 2: 일반 JSON 패턴 매칭 (가장 완전한 JSON 찾기) + if (!jsonText) { + const jsonMatches = cleanedText.match(/\{[\s\S]*?\}/g); + if (jsonMatches && jsonMatches.length > 0) { + // 가장 긴 JSON 객체 선택 (더 완전할 가능성) + jsonText = jsonMatches.reduce((longest, current) => + current.length > longest.length ? current : longest + ); + } } - - // 응답 정규화 및 기본값 설정 - return { - summary: parsed.summary, - issues: parsed.issues.map(issue => ({ - line: issue.line || null, - severity: issue.severity || 'medium', - type: issue.type || 'general', - title: issue.title || 'Issue found', - description: issue.description || '', - suggestion: issue.suggestion || '', - codeExample: issue.code_example || null - })), - positiveFeedback: parsed.positive_feedback || [], - overallScore: parsed.overall_score || 5 + + // 방법 3: 첫 번째 { 부터 마지막 } 까지 + if (!jsonText) { + const firstBrace = cleanedText.indexOf('{'); + const lastBrace = cleanedText.lastIndexOf('}'); + if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { + jsonText = cleanedText.substring(firstBrace, lastBrace + 1); + } + } + + if (!jsonText) { + throw new Error('No JSON structure found in response'); + } + + console.log('Extracted JSON text:', jsonText.substring(0, 200)); + + // 3. JSON 정리 및 수정 + jsonText = jsonText + // 잘못된 콤마 제거 + .replace(/,(\s*[}\]])/g, '$1') + // 문자열 내 특수문자 처리 + .replace(/\\/g, '\\\\') // 백슬래시 이스케이프 + .replace(/\n/g, ' ') // 개행을 공백으로 변경 + .replace(/\r/g, '') // 캐리지 리턴 제거 + .replace(/\t/g, ' ') // 탭을 공백으로 변경 + // 잘못된 따옴표 수정 + .replace(/"/g, '"') // 유니코드 따옴표 정규화 + .replace(/"/g, '"') + .replace(/'/g, "'") // 작은따옴표 정규화 + // 여분의 공백 제거 + .replace(/\s+/g, ' ') + .trim(); + + // 4. JSON 파싱 시도 (여러 번 시도) + let parsed; + try { + parsed = JSON.parse(jsonText); + } catch (firstError) { + console.log('First parse attempt failed:', firstError.message); + + // JSON 수정 재시도 1: 기본 수정 + let fixedJson = jsonText + .replace(/([{,]\s*)(\w+):/g, '$1"$2":') // 키에 따옴표 추가 + .replace(/:\s*([^",\[\]{}]+)(\s*[,}])/g, ': "$1"$2') // 값에 따옴표 추가 + .replace(/: ""(\d+)""([,}])/g, ': $1$2') // 숫자 값 수정 + .replace(/: ""(true|false|null)""([,}])/g, ': $1$2'); // 불린/null 값 수정 + + try { + parsed = JSON.parse(fixedJson); + } catch (secondError) { + console.log('Second parse attempt failed:', secondError.message); + + // JSON 수정 재시도 2: 불완전한 JSON 복구 + fixedJson = this.repairIncompleteJson(jsonText); + + try { + parsed = JSON.parse(fixedJson); + console.log('Successfully repaired incomplete JSON'); + } catch (thirdError) { + console.log('Third parse attempt failed:', thirdError.message); + throw new Error(`JSON parsing failed: ${firstError.message}`); + } + } + } + + // 5. 응답 형식 검증 및 기본값 설정 + const result = { + summary: parsed.summary || 'Code review completed', + issues: Array.isArray(parsed.issues) ? parsed.issues : [], + positiveFeedback: Array.isArray(parsed.positive_feedback) ? parsed.positive_feedback : [], + overallScore: typeof parsed.overall_score === 'number' ? parsed.overall_score : 5 }; + + // 6. 이슈 정규화 + result.issues = result.issues.map(issue => ({ + line: typeof issue.line === 'number' ? issue.line : null, + severity: ['low', 'medium', 'high', 'critical'].includes(issue.severity) ? issue.severity : 'medium', + type: ['bug', 'security', 'performance', 'style', 'maintainability'].includes(issue.type) ? issue.type : 'general', + title: issue.title || 'Issue found', + description: issue.description || '', + suggestion: issue.suggestion || '', + codeExample: issue.code_example || issue.codeExample || null + })); + + console.log(`Successfully parsed review with ${result.issues.length} issues`); + return result; + } catch (error) { - // JSON 파싱 실패 시 fallback 응답 생성 - // 이렇게 하면 액션이 완전히 실패하지 않고 계속 진행됨 + console.error('Parse error details:', error); + console.error('Original response:', responseText); + + // JSON 파싱 실패 시 안전한 fallback 응답 생성 return { - summary: 'Code review completed, but response parsing failed.', + summary: 'Code review was processed, but the response format was invalid.', issues: [{ line: null, - severity: 'medium', - type: 'general', - title: 'Review Processing Error', - description: `Failed to parse review response: ${error.message}`, - suggestion: 'Please check the code manually.', + severity: 'low', + type: 'system', + title: 'Response Parsing Issue', + description: `AI 응답 파싱 중 오류가 발생했습니다: ${error.message}. 원본 응답을 확인해주세요.`, + suggestion: '코드를 수동으로 검토하거나 다시 시도해주세요.', codeExample: null }], positiveFeedback: [], @@ -221,6 +310,112 @@ JSON 응답 (간결하게): }; } } + + /** + * 불완전한 JSON을 복구하는 메서드 + * @param {string} incompleteJson - 불완전한 JSON 문자열 + * @returns {string} 복구된 JSON 문자열 + */ + repairIncompleteJson(incompleteJson) { + console.log('Attempting to repair incomplete JSON...'); + + let repaired = incompleteJson.trim(); + + // 1. 가장 마지막 완전한 이슈 찾기 + const issuesMatch = repaired.match(/"issues":\s*\[([\s\S]*)/); + if (issuesMatch) { + const issuesContent = issuesMatch[1]; + + // 완전한 이슈 객체들만 추출 + const completeIssues = []; + let depth = 0; + let currentIssue = ''; + let inString = false; + let escapeNext = false; + + for (let i = 0; i < issuesContent.length; i++) { + const char = issuesContent[i]; + + if (escapeNext) { + escapeNext = false; + currentIssue += char; + continue; + } + + if (char === '\\') { + escapeNext = true; + currentIssue += char; + continue; + } + + if (char === '"' && !escapeNext) { + inString = !inString; + } + + if (!inString) { + if (char === '{') { + depth++; + } else if (char === '}') { + depth--; + + if (depth === 0) { + currentIssue += char; + + // 완전한 이슈 객체 발견 + try { + const issueObj = JSON.parse(currentIssue); + if (issueObj.title && issueObj.description) { + completeIssues.push(currentIssue); + } + } catch (e) { + // 파싱 실패한 객체는 무시 + } + + currentIssue = ''; + continue; + } + } + } + + currentIssue += char; + } + + // 복구된 JSON 생성 + const summaryMatch = repaired.match(/"summary":\s*"([^"]*)"/) || ["", "Code review completed"]; + const scoreMatch = repaired.match(/"overall_score":\s*(\d+)/) || ["", "5"]; + + const repairedJson = { + summary: summaryMatch[1] || "Code review completed", + issues: completeIssues.map(issue => JSON.parse(issue)), + overall_score: parseInt(scoreMatch[1]) || 5 + }; + + console.log(`Repaired JSON with ${repairedJson.issues.length} complete issues`); + return JSON.stringify(repairedJson); + } + + // 기본 복구 로직 (위 방법이 실패한 경우) + const openBraces = (repaired.match(/\{/g) || []).length; + const closeBraces = (repaired.match(/\}/g) || []).length; + const openBrackets = (repaired.match(/\[/g) || []).length; + const closeBrackets = (repaired.match(/\]/g) || []).length; + + // 불완전한 마지막 부분 제거 + repaired = repaired.replace(/,\s*\{[^}]*$/, ''); + repaired = repaired.replace(/,\s*"[^"]*":[^,}]*$/, ''); + repaired = repaired.replace(/:\s*"[^"]*$/, ': "incomplete"'); + + // 괄호 닫기 + for (let i = closeBrackets; i < openBrackets; i++) { + repaired += ']'; + } + for (let i = closeBraces; i < openBraces; i++) { + repaired += '}'; + } + + console.log('Fallback repair applied'); + return repaired; + } } module.exports = CodeReviewer; \ No newline at end of file diff --git a/src/comment-manager.js b/src/comment-manager.js index 2675c79..dd619fe 100644 --- a/src/comment-manager.js +++ b/src/comment-manager.js @@ -246,36 +246,13 @@ class CommentManager { */ async postPullRequestComment(commentBody) { try { - // 기존 봇 댓글 찾기 (중복 방지) - const { data: comments } = await this.octokit.rest.issues.listComments({ + // 항상 새 댓글 생성 + await this.octokit.rest.issues.createComment({ owner: this.context.repo.owner, repo: this.context.repo.repo, - issue_number: this.context.payload.pull_request.number + issue_number: this.context.payload.pull_request.number, + body: commentBody }); - - // 이전에 작성한 봇 댓글 찾기 - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('🤖 Claude AI 코드 리뷰') - ); - - if (botComment) { - // 기존 댓글 업데이트 - await this.octokit.rest.issues.updateComment({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - comment_id: botComment.id, - body: commentBody - }); - } else { - // 새 댓글 생성 - await this.octokit.rest.issues.createComment({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - issue_number: this.context.payload.pull_request.number, - body: commentBody - }); - } } catch (error) { throw new Error(`Failed to post PR comment: ${error.message}`); } diff --git a/src/file-analyzer.js b/src/file-analyzer.js index 3fbb4d4..3e74a9f 100644 --- a/src/file-analyzer.js +++ b/src/file-analyzer.js @@ -156,14 +156,16 @@ class FileAnalyzer { async filterFiles(files) { // 1. 패턴 기반 필터링 const patternFiltered = files.filter(file => { + const filename = file.filename || file; + // 포함 패턴 체크: 하나라도 매치되면 포함 const isIncluded = this.filePatterns.some(pattern => - minimatch(file.filename, pattern) + minimatch(filename, pattern) ); // 제외 패턴 체크: 하나라도 매치되면 제외 const isExcluded = this.excludePatterns.some(pattern => - minimatch(file.filename, pattern) + minimatch(filename, pattern) ); return isIncluded && !isExcluded; diff --git a/test-files/api-endpoints.js b/test-files/api-endpoints.js deleted file mode 100644 index 180fee7..0000000 --- a/test-files/api-endpoints.js +++ /dev/null @@ -1,119 +0,0 @@ -// Express API 엔드포인트 - 다양한 문제점들 - -const express = require('express'); -const app = express(); - -// CORS 설정 문제 -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); // 모든 도메인 허용 - 보안 위험 - res.header('Access-Control-Allow-Headers', '*'); - next(); -}); - -// Rate limiting 없음 - DoS 공격 위험 -app.post('/api/login', async (req, res) => { - const { username, password } = req.body; - - // 입력 검증 없음 - if (!username || !password) { - return res.status(400).json({ error: 'Missing credentials' }); - } - - // 비동기 처리 오류 - try { - const user = authenticateUser(username, password); // await 누락 - res.json({ success: true, user }); - } catch (error) { - console.log(error); // 에러 정보 노출 - res.status(500).json({ error: error.message }); // 내부 에러 노출 - } -}); - -// Path traversal 취약점 -app.get('/api/files/:filename', (req, res) => { - const filename = req.params.filename; - const filePath = `./uploads/${filename}`; // 경로 검증 없음 - - // 파일 존재 여부만 확인 - if (fs.existsSync(filePath)) { - res.sendFile(filePath); // 절대 경로가 아님 - } else { - res.status(404).send('File not found'); - } -}); - -// SQL 인젝션 위험 -app.get('/api/users/:id', (req, res) => { - const userId = req.params.id; - - // 매개변수화된 쿼리 대신 문자열 연결 - const query = `SELECT * FROM users WHERE id = ${userId}`; - - db.query(query, (err, results) => { - if (err) { - console.error('Database error:', err); - return res.status(500).json({ error: 'Database error' }); - } - res.json(results); - }); -}); - -// 메모리 누수 위험 -const cache = new Map(); -app.get('/api/cache/:key', (req, res) => { - const key = req.params.key; - const value = generateExpensiveData(key); - - cache.set(key, value); // 캐시 크기 제한 없음 - res.json({ data: value }); -}); - -// 인증 없이 민감한 정보 노출 -app.get('/api/admin/logs', (req, res) => { - // 권한 검사 없음 - const logs = fs.readFileSync('./app.log', 'utf8'); - res.json({ logs }); -}); - -// 비효율적인 데이터베이스 쿼리 -app.get('/api/dashboard', async (req, res) => { - const users = []; - - // N+1 쿼리 문제 - const userIds = await db.query('SELECT id FROM users'); - for (const userId of userIds) { - const user = await db.query(`SELECT * FROM users WHERE id = ${userId.id}`); - const posts = await db.query(`SELECT * FROM posts WHERE user_id = ${userId.id}`); - users.push({ ...user, posts }); - } - - res.json(users); -}); - -// 파일 업로드 검증 부족 -app.post('/api/upload', upload.single('file'), (req, res) => { - const file = req.file; - - // 파일 타입, 크기 검증 없음 - if (!file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - // 원본 파일명 그대로 사용 (경로 조작 위험) - const filename = file.originalname; - fs.writeFileSync(`./uploads/${filename}`, file.buffer); - - res.json({ message: 'File uploaded', filename }); -}); - -// 하드코딩된 설정값들 -const PORT = 3000; -const DB_PASSWORD = 'admin123'; // 하드코딩된 비밀번호 -const JWT_SECRET = 'my-secret-key'; // 하드코딩된 JWT 시크릿 - -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); - console.log(`Database password: ${DB_PASSWORD}`); // 로그에 비밀번호 출력 -}); - -module.exports = app; \ No newline at end of file diff --git a/test-files/buggy-calculator.js b/test-files/buggy-calculator.js deleted file mode 100644 index 65a0eed..0000000 --- a/test-files/buggy-calculator.js +++ /dev/null @@ -1,60 +0,0 @@ -// 의도적으로 문제가 있는 계산기 코드 (테스트용) - -class Calculator { - constructor() { - this.history = []; - } - - // 보안 문제: 입력 검증 없음 - add(a, b) { - let result = eval(a + '+' + b); // XSS 취약점 위험 - this.history.push(result); - return result; - } - - // 성능 문제: 비효율적인 알고리즘 - fibonacci(n) { - if (n <= 1) return n; - return this.fibonacci(n - 1) + this.fibonacci(n - 2); // O(2^n) 복잡도 - } - - // 스타일 문제: 일관성 없는 코딩 스타일 - divide(x,y){ - if(y==0)return "Cannot divide by zero" // 세미콜론 누락, 공백 불일치 - var result=x/y // var 사용 (let/const 권장) - return result - } - - // 버그: 잘못된 로직 - getAverage() { - if (this.history.length = 0) { // = 사용 (== 또는 === 의도) - return 0; - } - - let sum = 0; - for (let i = 0; i <= this.history.length; i++) { // off-by-one 에러 - sum += this.history[i]; - } - - return sum / this.history.length; - } - - // 메모리 누수 위험 - addToHistory(value) { - this.history.push(value); - // history 배열이 무한정 커질 수 있음 - } - - // 타입 안전성 문제 - multiply(a, b) { - return a * b; // 타입 체크 없음 - } -} - -// 전역 변수 사용 (안티패턴) -var globalCalculator = new Calculator(); - -// 콘솔에 민감한 정보 로깅 -console.log("API Key: sk-1234567890abcdef"); // 보안 위험 - -module.exports = Calculator; \ No newline at end of file diff --git a/test-files/user-service.js b/test-files/user-service.js deleted file mode 100644 index 77dd603..0000000 --- a/test-files/user-service.js +++ /dev/null @@ -1,109 +0,0 @@ -// 사용자 서비스 - 보안 및 성능 문제가 있는 코드 - -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); - -class UserService { - constructor() { - this.users = []; - this.secret = 'hardcoded-secret-key'; // 보안 문제: 하드코딩된 시크릿 - } - - // SQL 인젝션 취약점 - async getUserByEmail(email) { - const query = `SELECT * FROM users WHERE email = '${email}'`; // 위험한 문자열 연결 - // 실제 DB 쿼리 실행 코드는 생략 - return this.users.find(user => user.email === email); - } - - // 약한 패스워드 정책 - createUser(userData) { - if (!userData.password || userData.password.length < 3) { // 너무 약한 최소 길이 - throw new Error('Password too short'); - } - - // 패스워드 해싱 없이 저장 - const user = { - id: Date.now(), // 예측 가능한 ID - email: userData.email, - password: userData.password, // 평문 저장! - createdAt: new Date() - }; - - this.users.push(user); - return user; - } - - // 브루트포스 공격에 취약 - async login(email, password) { - const user = await this.getUserByEmail(email); - - if (!user) { - return null; // 시간 기반 공격 가능 - } - - // 평문 비교 - if (user.password === password) { - const token = jwt.sign({ userId: user.id }, this.secret); - return { token, user }; - } - - return null; - } - - // 권한 검증 누락 - deleteUser(userId) { - const index = this.users.findIndex(user => user.id == userId); // == 사용 - if (index !== -1) { - this.users.splice(index, 1); - return true; - } - return false; - } - - // 성능 문제: N+1 쿼리 패턴 - async getUsersWithProfiles() { - const users = await this.getAllUsers(); - const usersWithProfiles = []; - - for (const user of users) { - const profile = await this.getUserProfile(user.id); // 각 사용자마다 개별 쿼리 - usersWithProfiles.push({ - ...user, - profile - }); - } - - return usersWithProfiles; - } - - // 메모리 누수 위험 - cacheUserData(userId, data) { - if (!this.cache) { - this.cache = new Map(); - } - - this.cache.set(userId, data); // 캐시 만료나 정리 로직 없음 - } - - // 입력 검증 부족 - updateUser(userId, updateData) { - const user = this.users.find(u => u.id === userId); - if (user) { - Object.assign(user, updateData); // 모든 필드 업데이트 허용 - } - return user; - } - - // 비동기 처리 오류 - async sendEmailVerification(email) { - // await 없이 비동기 함수 호출 - this.emailService.sendEmail(email, 'Verify your account'); - console.log('Email sent'); // 실제로는 아직 전송되지 않았을 수 있음 - } -} - -// 환경변수 없이 민감한 정보 노출 -process.env.NODE_ENV !== 'production' && console.log('Database password: admin123'); - -module.exports = UserService; \ No newline at end of file diff --git a/test/additional-test.js b/test/additional-test.js deleted file mode 100644 index 513538f..0000000 --- a/test/additional-test.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 추가 테스트 코드 - PR 테스트용 - * 이 파일은 PR에서 Claude AI 코드 리뷰가 동작하는지 확인하기 위해 추가합니다. - */ - -// 의도적인 문제들이 포함된 코드 -function badFunction() { - // 문제 1: var 사용 - var x = 10; - - // 문제 2: == 대신 === 사용해야 함 - if (x == "10") { - console.log("Equal"); - } - - // 문제 3: 사용하지 않는 변수 - let unusedVar = "This is not used"; - - // 문제 4: 에러 처리 없음 - JSON.parse('{"invalid": json}'); - - return x; -} - -// 개선된 코드 -function goodFunction() { - const x = 10; - - if (x === 10) { - console.log("Equal"); - } - - try { - return JSON.parse('{"valid": "json"}'); - } catch (error) { - console.error('JSON parsing failed:', error); - return null; - } -} - -module.exports = { badFunction, goodFunction }; \ No newline at end of file diff --git a/test/sample-code.js b/test/sample-code.js deleted file mode 100644 index f7c6680..0000000 --- a/test/sample-code.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * 테스트용 샘플 코드 - * 이 파일은 Claude AI 코드 리뷰 액션이 제대로 동작하는지 테스트하기 위한 코드입니다. - * 의도적으로 몇 가지 문제점을 포함하고 있습니다. - */ - -// 문제 1: 사용하지 않는 변수 -const unusedVariable = "This variable is never used"; - -// 문제 2: 글로벌 변수 사용 -var globalVar = "Should avoid global variables"; - -// 문제 3: 안전하지 않은 eval 사용 (보안 문제) -function executeCode(code) { - return eval(code); // 보안 취약점 -} - -// 문제 4: 에러 처리 없는 비동기 함수 -async function fetchData(url) { - const response = await fetch(url); // 에러 처리 없음 - return response.json(); -} - -// 문제 5: 매직 넘버 사용 -function calculateDiscount(price) { - if (price > 100) { - return price * 0.1; // 매직 넘버 0.1 - } - return 0; -} - -// 문제 6: 깊은 중첩과 복잡한 조건문 -function complexFunction(data) { - if (data) { - if (data.length > 0) { - for (let i = 0; i < data.length; i++) { - if (data[i].active) { - if (data[i].type === 'premium') { - if (data[i].score > 80) { - console.log('Premium user with high score'); - } - } - } - } - } - } -} - -// 문제 7: 함수명이 명확하지 않음 -function doStuff(x, y) { - return x + y * 2 - 5; -} - -// 문제 8: 하드코딩된 값들 -const config = { - apiUrl: 'http://localhost:3000/api', // 하드코딩된 URL - timeout: 5000, - retries: 3 -}; - -// 문제 9: SQL 인젝션 가능성 (보안 문제) -function getUserData(userId) { - const query = `SELECT * FROM users WHERE id = ${userId}`; // SQL 인젝션 위험 - return database.query(query); -} - -// 문제 10: 메모리 누수 가능성 -let cache = {}; -function addToCache(key, value) { - cache[key] = value; // 캐시가 계속 증가하여 메모리 누수 가능 -} - -// 좋은 코드 예시 (Claude가 긍정적으로 평가할 부분) -/** - * 사용자 정보를 안전하게 가져오는 함수 - * @param {string} userId - 사용자 ID - * @returns {Promise} 사용자 정보 - */ -async function getUserInfoSafely(userId) { - try { - // 입력 검증 - if (!userId || typeof userId !== 'string') { - throw new Error('Invalid user ID'); - } - - // 안전한 파라미터화된 쿼리 - const query = 'SELECT * FROM users WHERE id = ?'; - const result = await database.query(query, [userId]); - - return result; - } catch (error) { - console.error('Error fetching user info:', error); - throw error; - } -} - -module.exports = { - executeCode, - fetchData, - calculateDiscount, - complexFunction, - doStuff, - getUserData, - addToCache, - getUserInfoSafely -}; \ No newline at end of file diff --git a/test/sample-security.py b/test/sample-security.py deleted file mode 100644 index ae29ab7..0000000 --- a/test/sample-security.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -보안 취약점 테스트용 Python 코드 -Claude AI가 보안 문제를 잘 찾아내는지 테스트하기 위한 샘플입니다. -""" - -import os -import subprocess -import pickle -import hashlib - -# 문제 1: 하드코딩된 비밀번호 -SECRET_KEY = "hardcoded_secret_123" -DATABASE_PASSWORD = "admin123" - -# 문제 2: 안전하지 않은 명령어 실행 -def execute_command(user_input): - # 사용자 입력을 직접 실행 - 명령어 인젝션 위험 - result = subprocess.run(user_input, shell=True, capture_output=True) - return result.stdout - -# 문제 3: 안전하지 않은 pickle 사용 -def load_data(file_path): - with open(file_path, 'rb') as f: - # pickle.load는 임의 코드 실행 가능 - return pickle.load(f) - -# 문제 4: 약한 해시 알고리즘 사용 -def hash_password(password): - # MD5는 더 이상 안전하지 않음 - return hashlib.md5(password.encode()).hexdigest() - -# 문제 5: 파일 경로 검증 없음 (Path Traversal) -def read_file(filename): - # ../../../etc/passwd 같은 경로로 시스템 파일 접근 가능 - with open(f"/uploads/{filename}", 'r') as f: - return f.read() - -# 문제 6: SQL 인젝션 위험 -def get_user(user_id): - query = f"SELECT * FROM users WHERE id = {user_id}" - # 파라미터화되지 않은 쿼리 - return database.execute(query) - -# 문제 7: 예외 처리에서 민감한 정보 노출 -def connect_database(): - try: - connection = database.connect( - host="192.168.1.100", - user="admin", - password=DATABASE_PASSWORD - ) - return connection - except Exception as e: - # 에러 메시지에 민감한 정보가 포함될 수 있음 - print(f"Database connection failed: {e}") - raise - -# 문제 8: 안전하지 않은 랜덤 생성 -import random -def generate_token(): - # random 모듈은 암호학적으로 안전하지 않음 - return ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=32)) - -# 좋은 코드 예시 -import secrets -import bcrypt -from pathlib import Path - -def hash_password_securely(password): - """비밀번호를 안전하게 해시화""" - # bcrypt 사용으로 안전한 해시 - salt = bcrypt.gensalt() - return bcrypt.hashpw(password.encode('utf-8'), salt) - -def generate_secure_token(): - """암호학적으로 안전한 토큰 생성""" - return secrets.token_urlsafe(32) - -def read_file_safely(filename): - """안전한 파일 읽기""" - # 경로 검증 - safe_path = Path("/uploads") / filename - if not str(safe_path).startswith("/uploads/"): - raise ValueError("Invalid file path") - - try: - with open(safe_path, 'r') as f: - return f.read() - except FileNotFoundError: - return None \ No newline at end of file