├── .gitignore ├── README.md ├── config.ini.example ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # 配置文件 2 | config.ini 3 | .env 4 | 5 | # Python 缓存 6 | __pycache__/ 7 | *.pyc 8 | *.pyo 9 | *.pyd 10 | 11 | # 虚拟环境 12 | venv/ 13 | .venv/ 14 | 15 | # IDE/编辑器特定文件 16 | .vscode/ 17 | .idea/ 18 | 19 | # 报告文件 20 | *.html 21 | reports/ 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI-Powered JavaScript Security Analyzer 2 | 3 | 这是一个使用 Google Gemini API 对网站的 JavaScript 文件进行安全分析的 Python 工具。它可以帮助开发者和安全研究人员快速识别 JS 代码中潜在的安全漏洞。 4 | 5 | ## 核心功能 6 | 7 | - **URL 分析**: 输入一个网站 URL,工具会自动提取页面中所有的 JavaScript 文件链接。 8 | - **交互式选择**: 在分析前,您可以从提取的 JS 文件列表中选择一个、多个或全部文件进行分析。 9 | - **深度 AI 分析**: 10 | - **智能分块**: 自动将大型 JS 文件分割成小块,以适应 API 的输入限制。 11 | - **两阶段总结**: 在对代码块进行初步分析后,会再次调用 AI 对所有分析结果进行最终总结,生成一份全面、连贯的报告。 12 | - **精确定位**: 最终报告会包含具体的代码片段和行号(如果可用),方便快速定位问题。 13 | - **精美报告**: 分析结果会自动生成为带有 CSS 样式的 HTML 页面,并保存在 `reports/` 目录下,同时自动在浏览器中打开。 14 | - **高度可配置**: 15 | - **健壮的网络支持**: 16 | - 通过为 gRPC 设置 `https_proxy` 环境变量,实现了对 Gemini API 调用的可靠代理 17 | - 自动忽略SSL证书验证问题,支持自签证书和过期证书的网站分析 18 | - 解决了在各种复杂网络环境下的连接问题 19 | - **完全自定义**: 可以在 `config.ini` 中自定义 API 密钥、模型、代理、分块大小以及用于不同分析阶段的提示词。 20 | 21 | ## 文件结构 22 | 23 | ``` 24 | . 25 | ├── .gitignore # Git 忽略文件,保护敏感信息和生成的文件 26 | ├── config.ini # 您的个人配置文件 (需要自行创建或从 example 复制) 27 | ├── config.ini.example # 配置文件模板 28 | ├── main.py # 主程序脚本 29 | ├── README.md # 项目说明文件 30 | └── requirements.txt # Python 依赖库 31 | ``` 32 | 33 | ## 安装与设置 34 | 35 | 1. **克隆仓库**: 36 | ```bash 37 | git clone https://github.com/Xc1Ym/js_analysis 38 | cd js_analysis 39 | ``` 40 | 41 | 2. **安装依赖**: 42 | 建议在 Python 虚拟环境中进行安装。 43 | ```bash 44 | pip install -r requirements.txt 45 | ``` 46 | 47 | 3. **创建配置文件**: 48 | 复制 `config.ini.example` 文件并将其重命名为 `config.ini`。 49 | ```bash 50 | cp config.ini.example config.ini 51 | ``` 52 | 53 | 4. **编辑配置文件**: 54 | 打开 `config.ini` 文件,并填入您的个人信息: 55 | - `api_key`: 您的 Google Gemini API 密钥。 56 | - `[Proxy]`: 如果您需要通过代理访问 Gemini API,请配置 `type`, `host`, 和 `port`。如果不需要,请将 `type` 留空。 57 | 58 | ## 使用方法 59 | 60 | 在项目根目录下运行以下命令: 61 | 62 | ```bash 63 | python main.py 64 | ``` 65 | 66 | 程序将提示您输入要分析的网站 URL。之后,按照屏幕上的指示选择要分析的 JS 文件即可。分析完成后,HTML 报告会自动在您的默认浏览器中打开。 67 | 68 | ## 配置选项 (`config.ini`) 69 | 70 | - **[Gemini]** 71 | - `api_key`: (必需) 你的 Gemini API 密钥。 72 | - `model`: 使用的 Gemini 模型,推荐 `gemini-1.5-flash`。 73 | - `max_chunk_size`: 发送给 API 的单个代码块的最大字符数。 74 | 75 | - **[Proxy]** 76 | - `type`: 代理类型, "http"。**如果不需要代理,请将此项留空或删除此项**。 77 | - `host`: 代理服务器地址。(不使用代理时可留空) 78 | - `port`: 代理服务器端口。(不使用代理时可留空) 79 | 80 | ### 代理配置说明: 81 | 82 | **不使用代理(直连)**: 83 | ```ini 84 | [Proxy] 85 | type = 86 | host = 87 | port = 88 | ``` 89 | 或者直接将 `type` 项留空: 90 | ```ini 91 | [Proxy] 92 | type = 93 | ``` 94 | 95 | **使用HTTP代理**: 96 | ```ini 97 | [Proxy] 98 | type = http 99 | host = 127.0.0.1 100 | port = 7890 101 | ``` 102 | 103 | - **[Prompt]** 104 | - `custom_prompt`: 用于分析第一个(或唯一一个)代码块的提示词。 105 | - `chunk_prompt`: 用于分析后续代码块的提示词。 106 | - `summary_prompt`: 用于对所有分块分析结果进行最终总结的提示词。 107 | -------------------------------------------------------------------------------- /config.ini.example: -------------------------------------------------------------------------------- 1 | [Gemini] 2 | api_key = YOUR_GEMINI_API_KEY_HERE 3 | model = gemini-2.5-flash 4 | # 单个代码块发送给Gemini的最大字符数 5 | max_chunk_size = 100000 6 | 7 | [Proxy] 8 | # 代理类型, "http" 或 "https"。如果不需要代理,请留空。 9 | # 注意:Gemini API (gRPC) 通常通过 HTTP/HTTPS 代理工作,而不是 SOCKS5。 10 | type = http 11 | host = 127.0.0.1 12 | port = 1080 13 | 14 | [Prompt] 15 | # 在这里定义你的自定义提示词。 16 | # 使用 {js_code} 作为占位符,它将被替换为实际的 JavaScript 代码。 17 | custom_prompt = "你是一个资深前端安全专家,请分析以下JavaScript代码,并找出其中可能存在的安全漏洞、敏感信息泄露、以及恶意行为。这是代码的第一部分(或全部)。请以中文返回分析结果。代码如下:\n\n{js_code}" 18 | # 当代码被分块时,用于后续块的提示词。 19 | chunk_prompt = "这是同一个JavaScript文件的后续部分,请继续你的分析,使用中文回答。代码片段如下:\n\n{js_code}" 20 | # 当所有分块分析完成后,用于最终总结的提示词。 21 | summary_prompt = "你是一个资深安全报告撰写专家。以下是针对同一个JavaScript文件的多个部分进行的独立安全分析报告。你的任务是:\n1. 将这些分散的报告内容汇总并提炼成一份全面、连贯的最终安全分析报告。\n2. **关键要求**:对于每一个发现的安全漏洞或问题,你必须清晰地引用导致该问题的具体代码片段,并如果原始报告中提到了行号,也必须一并保留和展示。\n3. 移除重复的分析内容,但要确保所有独特的漏洞都被包含在内。\n4. 最终报告应结构清晰,将问题描述、有问题的代码示例、以及潜在的修复建议明确地对应起来。要使用中文回答。\n\n原始分析报告如下:\n\n{analysis_reports}" 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import requests 3 | from bs4 import BeautifulSoup 4 | import google.generativeai as genai 5 | from urllib.parse import urljoin 6 | import webbrowser 7 | import markdown2 8 | import time 9 | import os 10 | import urllib3 11 | from urllib3.exceptions import InsecureRequestWarning 12 | import random 13 | 14 | # 禁用SSL警告 - 注意:这会降低安全性,仅在开发/测试环境使用 15 | urllib3.disable_warnings(InsecureRequestWarning) 16 | 17 | # 常见的用户代理列表,模拟真实浏览器 18 | USER_AGENTS = [ 19 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 20 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 21 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 22 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0', 23 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', 24 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0 Safari/537.36' 25 | ] 26 | 27 | def get_random_headers(): 28 | """生成随机的HTTP请求头,模拟真实浏览器""" 29 | return { 30 | 'User-Agent': random.choice(USER_AGENTS), 31 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 32 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 33 | 'Accept-Encoding': 'gzip, deflate, br', 34 | 'DNT': '1', 35 | 'Connection': 'keep-alive', 36 | 'Upgrade-Insecure-Requests': '1', 37 | 'Sec-Fetch-Dest': 'document', 38 | 'Sec-Fetch-Mode': 'navigate', 39 | 'Sec-Fetch-Site': 'none', 40 | 'Sec-Fetch-User': '?1', 41 | 'Cache-Control': 'max-age=0' 42 | } 43 | 44 | def load_config(filename="config.ini"): 45 | """从 .ini 文件加载配置""" 46 | config = configparser.ConfigParser() 47 | # 指定UTF-8编码以正确读取包含中文字符的配置文件 48 | config.read(filename, encoding='utf-8') 49 | return config 50 | 51 | def format_file_size(size_bytes): 52 | """将字节数转换为人类可读的格式""" 53 | if size_bytes == 0: 54 | return "0 B" 55 | 56 | size_names = ["B", "KB", "MB", "GB", "TB"] 57 | i = 0 58 | size = float(size_bytes) 59 | 60 | while size >= 1024.0 and i < len(size_names) - 1: 61 | size /= 1024.0 62 | i += 1 63 | 64 | if i == 0: # 字节数 65 | return f"{int(size)} {size_names[i]}" 66 | else: 67 | return f"{size:.1f} {size_names[i]}" 68 | 69 | def get_js_file_size(url): 70 | """获取JavaScript文件的大小""" 71 | try: 72 | # 使用HEAD请求先尝试获取Content-Length 73 | response = requests.head(url, timeout=10, verify=False) 74 | content_length = response.headers.get('content-length') 75 | if content_length: 76 | return int(content_length) 77 | 78 | # 如果HEAD请求没有返回Content-Length,则使用GET请求 79 | response = requests.get(url, timeout=10, verify=False) 80 | response.raise_for_status() 81 | return len(response.content) 82 | except Exception as e: 83 | print(f"获取文件大小时出错 ({url}): {e}") 84 | return 0 85 | 86 | def get_js_urls_from_page(url): 87 | """从给定的URL中提取所有JavaScript文件的URL,并获取文件大小""" 88 | try: 89 | headers = get_random_headers() 90 | 91 | # 使用HEAD请求先尝试获取Content-Length 92 | response = requests.head(url, timeout=10, verify=False, headers=headers) 93 | content_length = response.headers.get('content-length') 94 | if content_length: 95 | return int(content_length) 96 | 97 | # 如果HEAD请求没有返回Content-Length,则使用GET请求 98 | # 添加随机延迟,模拟人类行为 99 | time.sleep(random.uniform(0.5, 2.0)) 100 | response = requests.get(url, timeout=10, verify=False, headers=headers) 101 | response.raise_for_status() 102 | return len(response.content) 103 | except Exception as e: 104 | print(f"获取文件大小时出错 ({url}): {e}") 105 | return 0 106 | 107 | def get_js_urls_from_page(url): 108 | """从给定的URL中提取所有JavaScript文件的URL,并获取文件大小""" 109 | try: 110 | headers = get_random_headers() 111 | 112 | # 禁用SSL证书验证,忽略自签证书和过期证书问题,使用随机User-Agent 113 | response = requests.get(url, timeout=15, verify=False, headers=headers) 114 | response.raise_for_status() 115 | soup = BeautifulSoup(response.text, 'html.parser') 116 | js_files = [] 117 | for script_tag in soup.find_all('script', src=True): 118 | js_url = script_tag.get('src') 119 | # 确保js_url是字符串 120 | if isinstance(js_url, str): 121 | # 将相对URL转换为绝对URL 122 | absolute_js_url = urljoin(url, js_url) 123 | print(f" 正在获取文件大小: {absolute_js_url}") 124 | file_size = get_js_file_size(absolute_js_url) 125 | js_files.append({ 126 | 'url': absolute_js_url, 127 | 'size': file_size, 128 | 'size_formatted': format_file_size(file_size) 129 | }) 130 | return js_files 131 | except requests.RequestException as e: 132 | print(f"获取页面内容时出错: {e}") 133 | return [] 134 | 135 | def get_js_content(url): 136 | """获取单个JS文件的内容,使用反爬虫措施""" 137 | try: 138 | headers = get_random_headers() 139 | 140 | # 添加随机延迟,模拟人类行为 141 | time.sleep(random.uniform(1.0, 3.0)) 142 | 143 | # 禁用SSL证书验证,忽略自签证书和过期证书问题,使用随机User-Agent 144 | response = requests.get(url, timeout=15, verify=False, headers=headers) 145 | response.raise_for_status() 146 | return response.text 147 | except requests.RequestException as e: 148 | print(f"下载JS文件时出错 ({url}): {e}") 149 | return None 150 | 151 | def chunk_string(string, length): 152 | """将字符串按指定长度分割""" 153 | return (string[0+i:length+i] for i in range(0, len(string), length)) 154 | 155 | def analyze_js_with_gemini(config, js_code): 156 | """使用Gemini API分析JavaScript代码,支持分块""" 157 | proxy_set = False 158 | try: 159 | # 为gRPC设置代理 (google-generativeai 使用 gRPC) 160 | if config.has_section('Proxy') and config.get('Proxy', 'type', fallback=''): 161 | proxy_type = config.get('Proxy', 'type') 162 | host = config.get('Proxy', 'host') 163 | port = config.get('Proxy', 'port') 164 | # grpcio 库会识别 'https_proxy' (全小写) 环境变量 165 | proxy_url = f"{proxy_type}://{host}:{port}" 166 | os.environ['https_proxy'] = proxy_url 167 | proxy_set = True 168 | 169 | # 禁用gRPC的SSL验证以忽略证书问题 170 | os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' 171 | os.environ['PYTHONHTTPSVERIFY'] = '0' 172 | 173 | # 配置Gemini 174 | api_key = config.get('Gemini', 'api_key') 175 | model_name = config.get('Gemini', 'model') 176 | max_chunk_size = config.getint('Gemini', 'max_chunk_size', fallback=15000) 177 | prompt_template = config.get('Prompt', 'custom_prompt') 178 | chunk_prompt_template = config.get('Prompt', 'chunk_prompt') 179 | 180 | if not api_key or api_key == "YOUR_GEMINI_API_KEY": 181 | print("错误: 请在 config.ini 文件中配置您的Gemini API key。") 182 | return None 183 | 184 | genai.configure(api_key=api_key) 185 | model = genai.GenerativeModel(model_name) 186 | 187 | # 检查是否需要分块 188 | if len(js_code) <= max_chunk_size: 189 | # 不分块 190 | prompt = prompt_template.format(js_code=js_code) 191 | response = model.generate_content(prompt) 192 | return response.text 193 | else: 194 | # 分块处理 195 | print(f" 代码过长 ({len(js_code)} 字符),将分块发送...") 196 | chunks = list(chunk_string(js_code, max_chunk_size)) 197 | full_analysis = [] 198 | 199 | # 处理第一块 200 | print(f" 正在分析第 1/{len(chunks)} 块...") 201 | first_prompt = prompt_template.format(js_code=chunks[0]) 202 | response = model.generate_content(first_prompt) 203 | full_analysis.append(response.text) 204 | 205 | # 处理后续块 206 | for i, chunk in enumerate(chunks[1:], start=2): 207 | print(f" 正在分析第 {i}/{len(chunks)} 块...") 208 | next_prompt = chunk_prompt_template.format(js_code=chunk) 209 | response = model.generate_content(next_prompt) 210 | full_analysis.append(response.text) 211 | 212 | # 对所有分块结果进行最终总结 213 | print(" 所有分块分析完成,正在进行最终总结...") 214 | summary_prompt_template = config.get('Prompt', 'summary_prompt') 215 | combined_reports = "\n\n--- 单独报告分割线 ---\n\n".join(full_analysis) 216 | summary_prompt = summary_prompt_template.format(analysis_reports=combined_reports) 217 | 218 | summary_response = model.generate_content(summary_prompt) 219 | return summary_response.text 220 | 221 | except Exception as e: 222 | print(f"调用Gemini API时出错: {e}") 223 | return None 224 | finally: 225 | # 清理环境变量,以免影响其他可能的网络调用 226 | if proxy_set: 227 | del os.environ['https_proxy'] 228 | # 清理SSL相关环境变量 229 | if 'GRPC_SSL_CIPHER_SUITES' in os.environ: 230 | del os.environ['GRPC_SSL_CIPHER_SUITES'] 231 | if 'PYTHONHTTPSVERIFY' in os.environ: 232 | del os.environ['PYTHONHTTPSVERIFY'] 233 | 234 | def main(): 235 | """主函数""" 236 | config = load_config() 237 | 238 | target_url = input("请输入要分析的网站URL: ") 239 | 240 | print(f"\n[1] 正在从 {target_url} 提取JS文件链接...") 241 | js_files = get_js_urls_from_page(target_url) 242 | 243 | if not js_files: 244 | print("未能找到任何JS文件链接。") 245 | return 246 | 247 | print(f"找到 {len(js_files)} 个JS文件链接:") 248 | for i, js_file in enumerate(js_files, 1): 249 | print(f" {i}. {js_file['url']} [{js_file['size_formatted']}]") 250 | 251 | # 让用户选择要分析的文件 252 | while True: 253 | choice = input("\n请输入要分析的JS文件编号 (用逗号分隔, 或输入 'all' 分析全部): ").strip().lower() 254 | if choice == 'all': 255 | selected_indices = range(len(js_files)) 256 | break 257 | else: 258 | try: 259 | selected_indices = [int(i.strip()) - 1 for i in choice.split(',')] 260 | if all(0 <= i < len(js_files) for i in selected_indices): 261 | break 262 | else: 263 | print("错误: 输入的编号超出范围,请重新输入。") 264 | except ValueError: 265 | print("错误: 输入无效,请输入数字编号。") 266 | 267 | # 分析选定的文件 268 | for index in selected_indices: 269 | js_file = js_files[index] 270 | js_url = js_file['url'] 271 | print(f"\n[2] 正在分析选定的文件: {js_url} [{js_file['size_formatted']}]") 272 | js_content = get_js_content(js_url) 273 | 274 | if js_content: 275 | print("[3] 已获取JS代码,正在发送到Gemini进行分析...") 276 | analysis_result = analyze_js_with_gemini(config, js_content) 277 | if analysis_result: 278 | print("[4] 分析完成,正在生成HTML报告...") 279 | html_content = markdown2.markdown(analysis_result, extras=["fenced-code-blocks", "tables"]) 280 | 281 | # 添加一些CSS样式 282 | html_template = f""" 283 | 284 | 285 |
286 | 287 |