From b316d8ed4562fc7ea4f50bafc2876b3e6440f252 Mon Sep 17 00:00:00 2001 From: Ky0toFu <102402668+Ky0toFu@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:32:00 +0800 Subject: [PATCH] Add files via upload --- Mirror Flowers/README.MD | 157 +++ .../backend/__pycache__/app.cpython-313.pyc | Bin 0 -> 47785 bytes Mirror Flowers/backend/app.py | 1042 +++++++++++++++++ Mirror Flowers/backend/static/index.html | 924 +++++++++++++++ .../core/analyzers/taint_analyzer.py | 59 + Mirror Flowers/core/parsers/java_parser.py | 12 + Mirror Flowers/core/parsers/php_parser.py | 40 + Mirror Flowers/docker/Dockerfile | 10 + Mirror Flowers/frontend/src/App.vue | 168 +++ Mirror Flowers/project_structure | 8 + Mirror Flowers/requirements.txt | 10 + 11 files changed, 2430 insertions(+) create mode 100644 Mirror Flowers/README.MD create mode 100644 Mirror Flowers/backend/__pycache__/app.cpython-313.pyc create mode 100644 Mirror Flowers/backend/app.py create mode 100644 Mirror Flowers/backend/static/index.html create mode 100644 Mirror Flowers/core/analyzers/taint_analyzer.py create mode 100644 Mirror Flowers/core/parsers/java_parser.py create mode 100644 Mirror Flowers/core/parsers/php_parser.py create mode 100644 Mirror Flowers/docker/Dockerfile create mode 100644 Mirror Flowers/frontend/src/App.vue create mode 100644 Mirror Flowers/project_structure create mode 100644 Mirror Flowers/requirements.txt diff --git a/Mirror Flowers/README.MD b/Mirror Flowers/README.MD new file mode 100644 index 0000000..9df4d6e --- /dev/null +++ b/Mirror Flowers/README.MD @@ -0,0 +1,157 @@ +# Mirror Flowers (镜花) + +![image-20250205181045094](C:\Users\lu0r3\AppData\Roaming\Typora\typora-user-images\image-20250205181045094.png) + +基于 AI 的代码安全审计工具,支持多种编程语言的代码分析,可以帮助开发者快速发现代码中的潜在安全漏洞。支持DeepSeek-R1,ChatGPT-4o等多种大模型。 + +## 支持的API接口 + +FREEGPTAPI:https://github.com/popjane/free_chatgpt_api +SiliconFlow(硅基流动):https://siliconflow.cn/ + +如需要使用GPT大模型则使用FREEGPTAPI,使用DeepSeek-R1大模型则使用SiliconFlow API。 + +SiliconFlow(硅基流动)注册可免费领取14元使用额度,可通过SMS接码平台注册账号,理论可无限免费使用API KEY。 + +## 功能特点 + +- 支持单文件和项目文件夹审计 +- 支持多种编程语言 (PHP, Java, JavaScript, Python) +- 实时进度显示 +- 深度代码分析 +- 漏洞详细报告 +- 支持亮色/暗色主题切换 +- 支持自定义 API 配置 + +## 支持的文件类型 + +- PHP (.php) +- Java (.java) +- JavaScript (.js) +- Python (.py) + +## 快速开始 + +### 环境要求 + +- Python 3.8+ +- FastAPI +- Node.js (可选,用于前端开发) + +### 安装步骤 + +1. 克隆项目 +```bash +git clone https://github.com/yourusername/code-audit-tool.git +cd code-audit-tool +``` + +2. 安装依赖 +```bash +pip install -r requirements.txt +``` + +3. 配置环境变量 +创建 `.env` 文件并配置以下参数: +```env +OPENAI_API_KEY=your_api_key_here +OPENAI_API_BASE=your_api_base_url +OPENAI_MODEL=your_preferred_model +``` + +4. 启动服务 +```bash +cd backend +uvicorn app:app --reload +``` + +5. 访问工具 +打开浏览器访问 `http://localhost:8000` + +## 使用说明 + +### 单文件审计 + +1. 在界面上选择"单文件审计" +2. 点击选择文件,上传需要审计的源代码文件 +3. 点击"开始审计"按钮 +4. 等待分析完成,查看审计结果 + +### 项目文件夹审计 + +1. 在界面上选择"项目文件夹审计" +2. 点击选择文件夹,选择需要审计的项目文件夹 +3. 系统会自动过滤支持的文件类型 +4. 点击"开始审计"按钮 +5. 等待分析完成,查看完整的项目审计报告 + +### 自定义 API 配置 + +1. 在页面顶部的 API 配置区域输入: + - OpenAI API Key + - API Base URL(可选) + - 选择模型(可选) +2. 点击"更新配置"保存设置 + +### 主题切换 + +- 点击右上角的主题切换按钮可以在亮色/暗色主题之间切换 +- 主题选择会被保存在本地 + +## 审计报告说明 + +审计报告包含以下内容: + +1. 漏洞分析 + - 漏洞类型 + - 漏洞位置 + - 严重程度 + - 详细描述 + - 影响范围 + - 修复建议 + +2. 上下文分析 + - 代码结构分析 + - 数据流分析 + - 相关函数调用 + +3. 相关文件 + - 受影响的相关文件列表 + - 文件依赖关系 + +## 注意事项 + +1. 文件大小限制:项目文件夹总大小不能超过 10MB +2. 支持的文件类型有限,不支持的文件类型会被自动过滤 +3. API Key 请妥善保管,不要泄露 +4. 分析结果仅供参考,建议结合人工审查 + +## 常见问题 + +1. Q: 为什么上传文件后按钮仍然禁用? + A: 请确保上传的文件类型是支持的文件类型之一。 + +2. Q: 如何处理大型项目? + A: 建议分模块上传,每次上传的文件总大小不要超过 10MB。 + +3. Q: 分析过程中中断了怎么办? + A: 可以刷新页面重新上传文件进行分析。 + +## 技术栈 + +- 后端:Python + FastAPI +- 前端:HTML + JavaScript + Bootstrap +- AI:OpenAI API +- 其他:JSZip, Bootstrap Icons + +## 贡献指南 + +欢迎提交 Issue 和 Pull Request 来帮助改进这个工具。 + +## 许可证 + +MIT License + +## 联系方式 + +如有问题或建议,请通过 Issue 与我联系。 \ No newline at end of file diff --git a/Mirror Flowers/backend/__pycache__/app.cpython-313.pyc b/Mirror Flowers/backend/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79202a774d0e7b79cc8ecb10bf364e4824e8e8e8 GIT binary patch literal 47785 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 z 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 Dict[str, List[str]]: + """从API获取可用模型列表""" + try: + async with aiohttp.ClientSession() as session: + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + async with session.get(f"{api_base}/models", headers=headers) as response: + if response.status == 200: + data = await response.json() + models = data.get("data", []) + + # 按类型分组模型 + grouped_models = { + "Chat": [], + "Embedding": [], + "Image": [], + "Audio": [], + "Pro": [] + } + + for model in models: + model_id = model.get("id") + if not model_id: + continue + + # 根据模型ID分类 + if model_id.startswith("360GPT"): + grouped_models["Chat"].append(model_id) + elif "embedding" in model_id.lower(): + grouped_models["Embedding"].append(model_id) + elif any(img_key in model_id.lower() for img_key in ["diffusion", "dall-e"]): + grouped_models["Image"].append(model_id) + elif any(audio_key in model_id.lower() for audio_key in ["speech", "voice", "audio"]): + grouped_models["Audio"].append(model_id) + elif model_id.startswith("Pro/"): + grouped_models["Pro"].append(model_id) + else: + grouped_models["Chat"].append(model_id) + + # 移除空类别 + return {k: v for k, v in grouped_models.items() if v} + else: + error_data = await response.text() + logger.error(f"获取模型列表失败: {error_data}") + return self.get_default_models(api_base) + + except Exception as e: + logger.error(f"获取模型列表时出错: {str(e)}") + return self.get_default_models(api_base) + + def get_default_models(self, api_base: str) -> Dict[str, List[str]]: + """根据API地址获取默认模型列表""" + try: + parsed_url = urlparse(api_base) + domain = parsed_url.netloc.lower() + + # 提取基本域名 + base_domain = '.'.join(domain.split('.')[-2:]) + + # 尝试直接匹配域名 + if domain in self.AVAILABLE_MODELS: + logger.info(f"直接匹配到域名: {domain}") + return self.AVAILABLE_MODELS[domain] + + # 尝试匹配基本域名 + if base_domain in self.AVAILABLE_MODELS: + logger.info(f"匹配到基本域名: {base_domain}") + return self.AVAILABLE_MODELS[base_domain] + + # 尝试部分匹配 + for configured_domain in self.AVAILABLE_MODELS.keys(): + if (configured_domain in domain or + domain in configured_domain or + configured_domain in base_domain or + base_domain in configured_domain): + logger.info(f"部分匹配到域名: {configured_domain}") + return self.AVAILABLE_MODELS[configured_domain] + + # 如果是特定域名,返回对应配置 + if "v36.cm" in domain: + logger.info("匹配到 v36.cm 域名") + return self.AVAILABLE_MODELS["v36.cm"] + if "360.com" in domain: + logger.info("匹配到 360.com 域名") + return self.AVAILABLE_MODELS["360.com"] + if "siliconflow.cn" in domain: + logger.info("匹配到 siliconflow.cn 域名") + return self.AVAILABLE_MODELS["api.siliconflow.cn"] + + logger.warning(f"未找到匹配的域名配置: {domain}") + return {"Chat": ["gpt-3.5-turbo"]} + + except Exception as e: + logger.error(f"获取默认模型列表时出错: {str(e)}") + return {"Chat": ["gpt-3.5-turbo"]} + + async def get_models_for_api(self, api_base: str, api_key: str = None) -> Dict[str, List[str]]: + """获取指定API地址支持的模型列表""" + try: + # 尝试从API获取模型列表 + if api_key: + try: + models = await self.fetch_models_from_api(api_base, api_key) + if models: + logger.info(f"从API获取到模型列表: {models}") + return models + except Exception as e: + logger.warning(f"从API获取模型列表失败: {str(e)}") + + # 如果API获取失败,使用预配置的模型列表 + return self.get_default_models(api_base) + + except Exception as e: + logger.error(f"获取模型列表时出错: {str(e)}") + return {"Chat": ["gpt-3.5-turbo"]} + + class Config: + env_file = ".env" + case_sensitive = True + +settings = Settings() + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = FastAPI(title="代码审计工具API") + +# 配置CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 挂载静态文件目录(如果前端文件在static目录下) +static_dir = os.path.join(os.path.dirname(__file__), "static") +if os.path.exists(static_dir): + app.mount("/static", StaticFiles(directory=static_dir), name="static") + +# 添加新的数据模型 +class ProjectAuditResult(BaseModel): + file_path: str + language: str + vulnerabilities: List[dict] + related_files: List[str] + context_analysis: str + +class ProjectAnalysis: + def __init__(self): + self.file_dependencies: Dict[str, Set[str]] = {} # 文件依赖关系 + self.shared_variables: Dict[str, Set[str]] = {} # 共享变量 + self.function_calls: Dict[str, Set[str]] = {} # 函数调用关系 + self.vulnerability_context: Dict[str, List[dict]] = {} # 漏洞上下文 + + def add_dependency(self, file: str, depends_on: str): + if file not in self.file_dependencies: + self.file_dependencies[file] = set() + self.file_dependencies[file].add(depends_on) + + def add_shared_variable(self, file: str, variable: str): + if file not in self.shared_variables: + self.shared_variables[file] = set() + self.shared_variables[file].add(variable) + + def add_function_call(self, source_file: str, target_file: str): + if source_file not in self.function_calls: + self.function_calls[source_file] = set() + self.function_calls[source_file].add(target_file) + + def get_related_files(self, file: str) -> Set[str]: + """获取与指定文件相关的所有文件""" + related = set() + if file in self.file_dependencies: + related.update(self.file_dependencies[file]) + if file in self.function_calls: + related.update(self.function_calls[file]) + return related + +class CodeAuditService: + def __init__(self): + """初始化服务""" + self.client = None + self.openai_api_key = None + self.api_base = None + self.model = None + # 初始化时设置基本配置,但不进行异步操作 + self._init_config(settings.OPENAI_API_KEY, settings.OPENAI_API_BASE) + self.project_analysis = ProjectAnalysis() + self.supported_extensions = {'.php', '.java', '.js', '.py'} + + def _init_config(self, api_key: str, api_base: str): + """初始化基本配置""" + self.openai_api_key = api_key + self.api_base = api_base.rstrip('/') + '/v1' if not api_base.endswith('/v1') else api_base + self.model = "gpt-3.5-turbo" # 设置默认模型 + + async def ensure_initialized(self): + """确保服务完全初始化""" + if not self.client: + await self.configure_openai(self.openai_api_key, self.api_base) + + async def configure_openai(self, api_key: str = None, api_base: str = None, model: str = None): + """配置OpenAI API设置""" + if not api_key and not self.openai_api_key: + raise ValueError("未设置OPENAI_API_KEY") + + # 更新配置 + if api_key: + self.openai_api_key = api_key + if api_base: + api_base = api_base.rstrip('/') + if not api_base.endswith('/v1'): + api_base = api_base + '/v1' + self.api_base = api_base + elif not self.api_base: + self.api_base = settings.OPENAI_API_BASE + + # 获取当前API地址支持的模型列表 + available_models = await settings.get_models_for_api(self.api_base, self.openai_api_key) + + # 设置模型 + if model: + model_found = False + for category_models in available_models.values(): + if model in category_models: + self.model = model + model_found = True + break + if not model_found: + raise ValueError(f"该API地址不支持模型: {model}") + elif not self.model: + if "Chat" in available_models and available_models["Chat"]: + self.model = available_models["Chat"][0] + else: + first_category = next(iter(available_models)) + if available_models[first_category]: + self.model = available_models[first_category][0] + else: + self.model = "gpt-3.5-turbo" + + try: + self.client = AsyncOpenAI( + api_key=self.openai_api_key, + base_url=self.api_base, + timeout=120.0, # 增加超时时间到120秒 + max_retries=5 # 增加重试次数 + ) + logger.info(f"OpenAI API已配置: {self.api_base}, 使用模型: {self.model}") + except Exception as e: + logger.error(f"OpenAI客户端配置失败: {str(e)}") + raise ValueError(f"API配置失败: {str(e)}") + + async def analyze_code(self, code: str, language: str, api_key: str = None, api_base: str = None) -> dict: + """分析代码,支持自定义API设置""" + try: + # 如果提供了新的API设置,重新配置 + if api_key or api_base: + self.configure_openai(api_key, api_base) + + # 第一轮AI分析 + logger.info(f"开始第一轮{language}代码分析") + first_response = await self._get_openai_response( + self._generate_first_prompt(code, language) + ) + + # 第二轮AI验证 + logger.info("开始第二轮验证分析") + second_response = await self._get_openai_response( + self._generate_second_prompt(code, first_response) + ) + + return { + "first_analysis": first_response, + "second_analysis": second_response + } + except Exception as e: + logger.error(f"代码分析过程中发生错误: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + def _generate_first_prompt(self, code: str, language: str) -> str: + return f"""请分析以下{language}代码中的安全漏洞: + {code} + 请详细说明每个潜在的安全问题,包括: + 1. 漏洞类型 + 2. 漏洞位置 + 3. 可能的影响 + 4. 修复建议""" + + def _generate_second_prompt(self, code: str, first_response: str) -> str: + return f"""请验证以下代码审计结果的准确性,并提供可能的payload: + {first_response} + 代码: + {code}""" + + async def _get_openai_response(self, prompt: str) -> str: + if not self.client: + raise ValueError("OpenAI客户端未初始化") + + try: + logger.info(f"正在发送请求到: {self.api_base}, 使用模型: {self.model}") + + # 添加重试逻辑 + max_retries = 3 + retry_count = 0 + retry_delay = 1 # 初始延迟1秒 + + while retry_count < max_retries: + try: + response = await self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": "你是一个专业的代码安全审计专家。"}, + {"role": "user", "content": prompt} + ], + temperature=0.7, + max_tokens=2000 + ) + + if hasattr(response, 'choices') and len(response.choices) > 0: + content = response.choices[0].message.content + logger.debug(f"收到响应: {content[:100]}...") + return content + else: + raise ValueError("API响应格式错误") + + except Exception as e: + retry_count += 1 + if retry_count >= max_retries: + raise + + logger.warning(f"请求失败,正在进行第{retry_count}次重试: {str(e)}") + await asyncio.sleep(retry_delay) + retry_delay *= 2 # 指数退避 + + except Exception as e: + error_msg = str(e) + logger.error(f"OpenAI API调用失败: {error_msg}") + + if "401" in error_msg: + raise HTTPException(status_code=401, detail="API密钥无效或未授权") + elif "timeout" in error_msg.lower(): + raise HTTPException(status_code=504, detail="API请求超时,请稍后重试") + elif "404" in error_msg: + raise HTTPException(status_code=404, detail="API端点不存在,请检查API基础URL") + else: + raise HTTPException(status_code=500, detail=f"AI分析服务错误: {error_msg}") + + async def analyze_project(self, zip_file: UploadFile) -> Dict[str, ProjectAuditResult]: + """分析整个项目代码""" + results = {} + + # 创建临时目录 + with tempfile.TemporaryDirectory() as temp_dir: + # 保存并解压ZIP文件 + zip_path = Path(temp_dir) / "project.zip" + with open(zip_path, "wb") as f: + content = await zip_file.read() + f.write(content) + + # 解压文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # 首先进行项目结构分析 + await self._analyze_project_structure(temp_dir) + + # 分析每个文件 + for file_path in Path(temp_dir).rglob('*'): + if file_path.suffix in self.supported_extensions: + rel_path = str(file_path.relative_to(temp_dir)) + try: + result = await self._analyze_file_with_context( + file_path, + self.project_analysis.get_related_files(rel_path) + ) + results[rel_path] = result + except Exception as e: + logger.error(f"分析文件 {rel_path} 时出错: {str(e)}") + + # 进行交叉验证和上下文关联分析 + await self._cross_validate_results(results) + + return results + + async def _analyze_project_structure(self, project_dir: str): + """分析项目结构,建立依赖关系""" + for file_path in Path(project_dir).rglob('*'): + if file_path.suffix not in self.supported_extensions: + continue + + rel_path = str(file_path.relative_to(project_dir)) + content = file_path.read_text(errors='ignore') + + # 分析文件依赖 + await self._analyze_dependencies(rel_path, content) + + # 分析共享变量 + await self._analyze_shared_variables(rel_path, content) + + # 分析函数调用 + await self._analyze_function_calls(rel_path, content) + + async def _analyze_file_with_context( + self, + file_path: Path, + related_files: Set[str] + ) -> ProjectAuditResult: + """分析单个文件,考虑上下文""" + content = file_path.read_text(errors='ignore') + language = file_path.suffix[1:] # 移除点号 + + # 构建包含上下文的提示 + context_prompt = f"""请分析以下{language}代码,重点关注安全漏洞。请提供详细的JSON格式分析结果: + +代码内容: +{content} + +相关文件: +{', '.join(related_files)} + +请提供以下格式的分析结果: +{{ + "vulnerabilities": [ + {{ + "type": "漏洞类型(如SQL注入、XSS等)", + "location": "具体代码行号和代码片段", + "severity": "严重程度(高/中/低)", + "description": "详细的漏洞描述", + "impact": "潜在影响", + "fix": "修复建议", + "related_context": "相关的上下文信息" + }} + ], + "context_analysis": "整体代码安全性分析", + "related_files": {{ + "dependencies": ["相关的依赖文件"], + "includes": ["包含的文件"], + "functions": ["调用的函数"], + "affected_by": ["受影响的文件"], + "affects": ["可能影响的文件"] + }} +}} + +请特别注意: +1. 详细分析每个可能的漏洞 +2. 提供具体的代码位置 +3. 给出可行的修复建议 +4. 分析代码与其他文件的关联 +5. 考虑整体的安全影响 +""" + + # 获取AI分析结果 + analysis_result = await self._get_openai_response(context_prompt) + + # 解析结果 + try: + result_dict = json.loads(analysis_result) + + # 确保结果包含所有必要字段 + if 'vulnerabilities' not in result_dict: + result_dict['vulnerabilities'] = [] + if 'context_analysis' not in result_dict: + result_dict['context_analysis'] = "未提供分析结果" + if 'related_files' not in result_dict: + result_dict['related_files'] = { + "dependencies": [], + "includes": [], + "functions": [], + "affected_by": [], + "affects": [] + } + + # 转换相关文件格式 + related_files_list = [] + for category, files in result_dict['related_files'].items(): + if files: + related_files_list.extend([f"{category}: {file}" for file in files]) + + return ProjectAuditResult( + file_path=str(file_path), + language=language, + vulnerabilities=result_dict['vulnerabilities'], + related_files=related_files_list, + context_analysis=result_dict['context_analysis'] + ) + except json.JSONDecodeError: + # 如果结果不是JSON格式,返回基本结构 + return ProjectAuditResult( + file_path=str(file_path), + language=language, + vulnerabilities=[], + related_files=[], + context_analysis=analysis_result + ) + + async def _cross_validate_results(self, results: Dict[str, ProjectAuditResult]): + """交叉验证分析结果""" + # 收集所有漏洞 + all_vulnerabilities = [] + for result in results.values(): + all_vulnerabilities.extend(result.vulnerabilities) + + # 生成交叉验证提示 + validation_prompt = f"""请验证以下项目漏洞分析结果的准确性和完整性: + +发现的漏洞: +{json.dumps(all_vulnerabilities, indent=2, ensure_ascii=False)} + +请考虑: +1. 漏洞之间的关联性 +2. 漏洞的优先级 +3. 误报可能性 +4. 修复建议的可行性 +""" + + # 获取验证结果 + validation_result = await self._get_openai_response(validation_prompt) + + # 更新结果 + for result in results.values(): + result.context_analysis += f"\n\n交叉验证结果:\n{validation_result}" + + async def _analyze_dependencies(self, file_path: str, content: str): + """分析文件依赖关系""" + try: + language = Path(file_path).suffix[1:] + + # 根据不同语言分析依赖 + if language == 'php': + await self._analyze_php_dependencies(file_path, content) + elif language == 'java': + await self._analyze_java_dependencies(file_path, content) + elif language == 'py': + await self._analyze_python_dependencies(file_path, content) + elif language == 'js': + await self._analyze_js_dependencies(file_path, content) + + except Exception as e: + logger.error(f"分析文件依赖时出错 {file_path}: {str(e)}") + + async def _analyze_php_dependencies(self, file_path: str, content: str): + """分析PHP文件依赖""" + import re + patterns = [ + r'(?:include|require|include_once|require_once)\s*[\'"]([^\'"]+)[\'"]', + r'use\s+([^;]+)', + r'namespace\s+([^;{\s]+)' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + dependency = match.group(1) + self.project_analysis.add_dependency(file_path, dependency) + + async def _analyze_java_dependencies(self, file_path: str, content: str): + """分析Java文件依赖""" + import re + patterns = [ + r'import\s+([^;]+)', + r'extends\s+([^\s{]+)', + r'implements\s+([^{]+)' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + dependency = match.group(1) + self.project_analysis.add_dependency(file_path, dependency) + + async def _analyze_python_dependencies(self, file_path: str, content: str): + """分析Python文件依赖""" + import re + patterns = [ + r'(?:from|import)\s+([^\s]+)', + r'__import__\([\'"]([^\'"]+)[\'"]\)' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + dependency = match.group(1) + self.project_analysis.add_dependency(file_path, dependency) + + async def _analyze_js_dependencies(self, file_path: str, content: str): + """分析JavaScript文件依赖""" + import re + patterns = [ + r'(?:import|require)\s*\([\'"]([^\'"]+)[\'"]\)', + r'import\s+.*\s+from\s+[\'"]([^\'"]+)[\'"]', + r'import\s+[\'"]([^\'"]+)[\'"]' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + dependency = match.group(1) + self.project_analysis.add_dependency(file_path, dependency) + + async def _analyze_shared_variables(self, file_path: str, content: str): + """分析共享变量""" + try: + language = Path(file_path).suffix[1:] + + # 根据不同语言分析共享变量 + if language == 'php': + await self._analyze_php_shared_vars(file_path, content) + elif language == 'java': + await self._analyze_java_shared_vars(file_path, content) + elif language == 'py': + await self._analyze_python_shared_vars(file_path, content) + elif language == 'js': + await self._analyze_js_shared_vars(file_path, content) + + except Exception as e: + logger.error(f"分析共享变量时出错 {file_path}: {str(e)}") + + async def _analyze_php_shared_vars(self, file_path: str, content: str): + """分析PHP共享变量""" + import re + patterns = [ + r'\$GLOBALS\[[\'"](\w+)[\'"]\]', + r'\$_(?:GET|POST|REQUEST|SESSION|COOKIE)\[[\'"](\w+)[\'"]\]', + r'global\s+\$(\w+)' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + var_name = match.group(1) + self.project_analysis.add_shared_variable(file_path, var_name) + + async def _analyze_java_shared_vars(self, file_path: str, content: str): + """分析Java共享变量""" + import re + patterns = [ + r'static\s+(?:final\s+)?(?:\w+)\s+(\w+)', + r'public\s+(?:static\s+)?(?:\w+)\s+(\w+)' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + var_name = match.group(1) + self.project_analysis.add_shared_variable(file_path, var_name) + + async def _analyze_python_shared_vars(self, file_path: str, content: str): + """分析Python共享变量""" + import re + patterns = [ + r'global\s+(\w+)', + r'(\w+)\s*=\s*[^=]' # 简单的全局变量定义 + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + var_name = match.group(1) + self.project_analysis.add_shared_variable(file_path, var_name) + + async def _analyze_js_shared_vars(self, file_path: str, content: str): + """分析JavaScript共享变量""" + import re + patterns = [ + r'(?:var|let|const)\s+(\w+)\s*=', + r'window\.(\w+)\s*=', + r'global\.(\w+)\s*=' + ] + + for pattern in patterns: + matches = re.finditer(pattern, content) + for match in matches: + var_name = match.group(1) + self.project_analysis.add_shared_variable(file_path, var_name) + + async def _analyze_function_calls(self, file_path: str, content: str): + """分析函数调用关系""" + try: + language = Path(file_path).suffix[1:] + + # 根据不同语言分析函数调用 + if language == 'php': + await self._analyze_php_function_calls(file_path, content) + elif language == 'java': + await self._analyze_java_function_calls(file_path, content) + elif language == 'py': + await self._analyze_python_function_calls(file_path, content) + elif language == 'js': + await self._analyze_js_function_calls(file_path, content) + + except Exception as e: + logger.error(f"分析函数调用时出错 {file_path}: {str(e)}") + + async def _analyze_php_function_calls(self, file_path: str, content: str): + """分析PHP函数调用""" + import re + pattern = r'(?:function\s+(\w+)|(\w+)\s*\()' + + matches = re.finditer(pattern, content) + for match in matches: + func_name = match.group(1) or match.group(2) + # 在项目中查找调用此函数的文件 + await self._find_function_callers(file_path, func_name) + + async def _analyze_java_function_calls(self, file_path: str, content: str): + """分析Java函数调用""" + import re + pattern = r'(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\([^)]*\)' + + matches = re.finditer(pattern, content) + for match in matches: + func_name = match.group(1) + await self._find_function_callers(file_path, func_name) + + async def _analyze_python_function_calls(self, file_path: str, content: str): + """分析Python函数调用""" + import re + pattern = r'def\s+(\w+)\s*\(' + + matches = re.finditer(pattern, content) + for match in matches: + func_name = match.group(1) + await self._find_function_callers(file_path, func_name) + + async def _analyze_js_function_calls(self, file_path: str, content: str): + """分析JavaScript函数调用""" + import re + pattern = r'(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function)' + + matches = re.finditer(pattern, content) + for match in matches: + func_name = match.group(1) or match.group(2) + await self._find_function_callers(file_path, func_name) + + async def _find_function_callers(self, source_file: str, function_name: str): + """查找调用指定函数的文件""" + # 这里可以实现更复杂的函数调用分析 + # 当前简单记录函数定义所在文件 + self.project_analysis.add_function_call(source_file, function_name) + +code_audit_service = CodeAuditService() + +# 添加请求体模型 +class ConfigureRequest(BaseModel): + api_key: str + api_base: Optional[str] = None + model: Optional[str] = None # 添加模型选择 + +# 添加获取可用模型的API +@app.get("/api/models") +async def get_available_models(): + """获取当前API地址支持的模型列表""" + try: + # 确保服务已初始化 + await code_audit_service.ensure_initialized() + + api_base = code_audit_service.api_base or settings.OPENAI_API_BASE + available_models = await settings.get_models_for_api( + api_base, + code_audit_service.openai_api_key or settings.OPENAI_API_KEY + ) + current_model = code_audit_service.model or settings.OPENAI_MODEL + + logger.info(f"当前API地址: {api_base}") + logger.info(f"可用模型: {available_models}") + logger.info(f"当前使用的模型: {current_model}") + + return { + "models": available_models, + "current_model": current_model + } + except Exception as e: + logger.error(f"获取模型列表失败: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +# 修改配置路由 +@app.post("/api/configure") +async def configure_api(config: ConfigureRequest): + """配置OpenAI API设置""" + try: + await code_audit_service.configure_openai( + config.api_key, + config.api_base, + config.model + ) + return { + "status": "success", + "message": "API配置已更新", + "model": code_audit_service.model + } + except Exception as e: + logger.error(f"配置更新失败: {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) + +@app.post("/api/audit") +async def audit_code( + file: UploadFile = File(...), + api_key: str = None, + api_base: str = None +): + """审计代码,支持自定义API设置""" + try: + # 确保服务已初始化 + await code_audit_service.ensure_initialized() + + content = await file.read() + code = content.decode() + + file_extension = file.filename.split('.')[-1].lower() + if file_extension not in ['php', 'java']: + raise HTTPException(status_code=400, detail="仅支持PHP和Java文件") + + language = "php" if file_extension == "php" else "java" + logger.info(f"开始分析{file.filename}") + + result = await code_audit_service.analyze_code(code, language, api_key, api_base) + return result + except UnicodeDecodeError: + raise HTTPException(status_code=400, detail="文件编码错误") + except Exception as e: + logger.error(f"处理文件时发生错误: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/audit/project") +async def audit_project( + project: UploadFile = File(...), + api_key: str = None, + api_base: str = None +): + """审计整个项目代码""" + try: + # 确保服务已初始化 + await code_audit_service.ensure_initialized() + + # 验证文件类型 + if not project.filename.endswith('.zip'): + raise HTTPException(status_code=400, detail="请上传ZIP格式的项目文件") + + # 分析项目 + results = await code_audit_service.analyze_project(project) + + return { + "status": "success", + "results": results + } + except Exception as e: + logger.error(f"项目审计过程中发生错误: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +@app.get("/") +async def root(): + """ + 根路径处理程序,返回API基本信息 + """ + return { + "name": "代码审计工具API", + "version": "1.0.0", + "status": "running", + "endpoints": { + "audit": "/api/audit", + "configure": "/api/configure", + "health": "/health", + "docs": "/docs" + } + } + +@app.exception_handler(Exception) +async def global_exception_handler(request, exc): + """ + 全局异常处理 + """ + error_msg = str(exc) + logger.error(f"发生错误: {error_msg}") + return JSONResponse( + status_code=500, + content={ + "error": "内部服务器错误", + "detail": error_msg + } + ) + +@app.middleware("http") +async def log_requests(request, call_next): + """ + 请求日志中间件 + """ + logger.info(f"收到请求: {request.method} {request.url}") + response = await call_next(request) + logger.info(f"响应状态码: {response.status_code}") + return response + +@app.get("/ui") +async def serve_spa(): + """ + 服务前端单页应用 + """ + return FileResponse(os.path.join(static_dir, "index.html")) \ No newline at end of file diff --git a/Mirror Flowers/backend/static/index.html b/Mirror Flowers/backend/static/index.html new file mode 100644 index 0000000..dbcd7ec --- /dev/null +++ b/Mirror Flowers/backend/static/index.html @@ -0,0 +1,924 @@ + + + + + + Mirror Flowers + + + + + + + + + +
+
+

Mirror Flowers

+
镜花 · 代码安全审计工具
+
+ + +
+
+
API配置
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
代码审计
+
+
+
+ +
+ +
+ +
+
+ + +
+
+
+ + + +
+ + +
+
+ + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/Mirror Flowers/core/analyzers/taint_analyzer.py b/Mirror Flowers/core/analyzers/taint_analyzer.py new file mode 100644 index 0000000..8d9c0b3 --- /dev/null +++ b/Mirror Flowers/core/analyzers/taint_analyzer.py @@ -0,0 +1,59 @@ +class TaintAnalyzer: + def __init__(self): + self.sources = set([ + 'GET', 'POST', 'REQUEST', 'FILES', 'COOKIE', + 'file_get_contents', 'fgets', 'fread' + ]) + self.sinks = set([ + 'eval', 'exec', 'system', 'shell_exec', + 'passthru', 'popen', 'proc_open' + ]) + self.sanitizers = set([ + 'htmlspecialchars', 'htmlentities', 'strip_tags', + 'addslashes', 'escapeshellarg', 'escapeshellcmd' + ]) + + def analyze(self, ast_tree): + """ + 执行污点分析 + """ + vulnerabilities = [] + + # 遍历AST寻找污点传播路径 + for node in ast_tree.traverse(): + if self._is_source(node): + taint = self._track_taint(node) + if taint: + vulnerabilities.append(taint) + + return vulnerabilities + + def _is_source(self, node): + """检查节点是否为污点源""" + # 实现基本的污点源检查 + if hasattr(node, 'name'): + return str(node.name) in self.sources + return False + + def _track_taint(self, node): + """追踪污点传播""" + if not node: + return None + + # 基本的污点追踪实现 + if hasattr(node, 'children'): + for child in node.children: + if self._is_sink(child): + return { + 'type': 'taint_flow', + 'source': str(node), + 'sink': str(child), + 'severity': 'high' + } + return None + + def _is_sink(self, node): + """检查节点是否为危险函数""" + if hasattr(node, 'name'): + return str(node.name) in self.sinks + return False \ No newline at end of file diff --git a/Mirror Flowers/core/parsers/java_parser.py b/Mirror Flowers/core/parsers/java_parser.py new file mode 100644 index 0000000..7147b56 --- /dev/null +++ b/Mirror Flowers/core/parsers/java_parser.py @@ -0,0 +1,12 @@ +import javalang + +class JavaParser: + def parse(self, code: str): + """ + 解析Java代码生成AST + """ + try: + ast = javalang.parse.parse(code) + return ast + except Exception as e: + raise Exception(f"Java解析错误: {str(e)}") \ No newline at end of file diff --git a/Mirror Flowers/core/parsers/php_parser.py b/Mirror Flowers/core/parsers/php_parser.py new file mode 100644 index 0000000..2a7bd0a --- /dev/null +++ b/Mirror Flowers/core/parsers/php_parser.py @@ -0,0 +1,40 @@ +import ast +import subprocess +import tempfile +import os + +class PHPParser: + def parse(self, code: str): + """ + 解析PHP代码生成AST + 使用 php -l 进行语法检查 + """ + try: + # 创建临时文件存储PHP代码 + with tempfile.NamedTemporaryFile(suffix='.php', mode='w', delete=False) as tmp: + tmp.write(code) + tmp_path = tmp.name + + # 使用 PHP 命令行进行语法检查 + result = subprocess.run(['php', '-l', tmp_path], + capture_output=True, + text=True) + + # 清理临时文件 + os.unlink(tmp_path) + + if "No syntax errors detected" not in result.stdout: + raise Exception(result.stderr) + + # 这里可以添加更详细的AST分析 + # 目前先返回简单的语法检查结果 + return { + "type": "php_file", + "syntax_valid": True, + "content": code + } + + except subprocess.CalledProcessError as e: + raise Exception(f"PHP解析错误: {str(e)}") + except Exception as e: + raise Exception(f"PHP解析错误: {str(e)}") \ No newline at end of file diff --git a/Mirror Flowers/docker/Dockerfile b/Mirror Flowers/docker/Dockerfile new file mode 100644 index 0000000..29b5198 --- /dev/null +++ b/Mirror Flowers/docker/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Mirror Flowers/frontend/src/App.vue b/Mirror Flowers/frontend/src/App.vue new file mode 100644 index 0000000..d55f332 --- /dev/null +++ b/Mirror Flowers/frontend/src/App.vue @@ -0,0 +1,168 @@ + + + + + \ No newline at end of file diff --git a/Mirror Flowers/project_structure b/Mirror Flowers/project_structure new file mode 100644 index 0000000..a339f0b --- /dev/null +++ b/Mirror Flowers/project_structure @@ -0,0 +1,8 @@ +code-audit-tool/ +├── frontend/ # Vue.js前端 +├── backend/ # Python FastAPI后端 +├── core/ # 核心审计逻辑 +│ ├── analyzers/ # 各种分析器 +│ ├── parsers/ # 代码解析器 +│ └── ai/ # AI分析模块 +└── docker/ # Docker配置文件 \ No newline at end of file diff --git a/Mirror Flowers/requirements.txt b/Mirror Flowers/requirements.txt new file mode 100644 index 0000000..c5f293b --- /dev/null +++ b/Mirror Flowers/requirements.txt @@ -0,0 +1,10 @@ +fastapi>=0.68.0 +uvicorn>=0.15.0 +python-multipart>=0.0.5 +openai>=1.0.0 +javalang>=0.13.0 +aiohttp>=3.8.1 +python-dotenv>=0.19.0 +pydantic>=2.0.0 +pydantic-settings>=2.0.0 +php-ast>=1.1.0 \ No newline at end of file