├── .gitignore ├── PypiScan.py ├── README.md ├── db ├── .gitkeep └── pypi │ └── .gitkeep ├── lib ├── __init__.py ├── consle_width.py ├── pypi_package.py ├── rule │ ├── __init__.py │ ├── content.py │ └── start.py ├── scan_python.py └── threads.py ├── log └── log.txt ├── pic └── 111.png ├── requirements.txt ├── test ├── Acqusition-4.4.2.tar.gz ├── dark-magic-0.1.2.tar.gz ├── jeIlyfish-0.7.0.tar.gz ├── libpeshnx-0.1.tar.gz ├── reols-0.1.tar.gz └── req-tools-0.4.tar.gz └── tmp └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | .idea/ 4 | -------------------------------------------------------------------------------- /PypiScan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 作者:咚咚呛 4 | # 版本:v0.1 5 | # 功能:本程序旨在为扫描整个pypi库的恶意代码特征,包含:后门类、挖矿类、勒索类、信息盗取类等 6 | 7 | from lib.threads import * 8 | import optparse 9 | 10 | if __name__ == "__main__": 11 | parser = optparse.OptionParser() 12 | parser.add_option("--thread", dest="thread", type="int", default=10, help=u"线程数") 13 | 14 | options, _ = parser.parse_args() 15 | 16 | pypi = Threads_Scan(thread=options.thread, systempath=os.path.dirname(os.path.abspath(__file__))) 17 | pypi.run() 18 | pypi.outfile.flush() 19 | pypi.outfile.close() 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PypiScan 0.1 2 | 3 | 这个脚本主要提供对pypi供应链的源头进行安全扫描研究,扫描并发现未知的恶意包情况。 4 | 5 | ## 作者 ## 6 | 7 | 咚咚呛 8 | 9 | 如有其他建议,可联系微信280495355 10 | 11 | ## 技术细节 ## 12 | 技术细节如下 13 | 14 | 1、脚本采取多线程方式爬取pypi所有包信息,默认10个线程,根据主机和带宽的配置,建议增加。 15 | 2、每个项目包含多个版本包,releases包存在两种类型,whl和tar,whl类型实质为zip压缩。 16 | 3、由于文件数量过大,硬盘存储有限,故采取下载/扫描完毕后会删除原始包,但会保存恶意文件到指定目录。 17 | 4、扫描以静态扫描为住,扫描特征行包括:网络链接行为、特定文件操作、命令执行行为、特定编码行为 18 | 5、作者执行了一次全量扫描,项目数量:21W+、包数量:150W+,用时10天+,目前误报较多,脚本主要用于研究使用,如要生产环境使用,请识别规则 19 | 20 | ## 程序使用 ## 21 | 22 | > root# git clone https://github.com/grayddq/PypiScan.git 23 | > 24 | > root# cd PypiScan 25 | > 26 | > root# sudo pip install -r requirements.txt 27 | > 28 | > root# python python PypiScan.py --thread 100 29 | 30 | ## 运行截图 ## 31 | 32 | ![Screenshot](pic/111.png) 33 | 34 | 35 | ## 历史风险参考 ## 36 | 37 | 历史pypi恶意包 38 | 39 | https://snyk.io/vuln/SNYK-PYTHON-JEILYFISH-536726 40 | https://snyk.io/vuln/SNYK-PYTHON-PYTHON3DATEUTIL-536644 41 | https://snyk.io/vuln/SNYK-PYTHON-LIBARI-460155 42 | https://snyk.io/vuln/SNYK-PYTHON-LIBPESH-460156 43 | https://snyk.io/vuln/SNYK-PYTHON-LIBPESHNX-460157 44 | https://snyk.io/vuln/SNYK-PYTHON-DAJNGO-72531 45 | https://snyk.io/vuln/SNYK-PYTHON-DIANGO-72529 46 | https://snyk.io/vuln/SNYK-PYTHON-DJAGO-72530 47 | https://snyk.io/vuln/SNYK-PYTHON-MYBIUBIUBIU-72532 48 | https://snyk.io/vuln/SNYK-PYTHON-PKGUTIL-72527 49 | https://snyk.io/vuln/SNYK-PYTHON-SMPLEJSON-72526 50 | https://snyk.io/vuln/SNYK-PYTHON-TIMEIT-72528 51 | https://snyk.io/vuln/SNYK-PYTHON-COLOURAMA-72537 52 | https://snyk.io/vuln/SNYK-PYTHON-PYCONAUFUNTIMES-72536 53 | https://snyk.io/vuln/SNYK-PYTHON-DJANGA-72533 54 | https://snyk.io/vuln/SNYK-PYTHON-EASYINSTALL-72534 55 | https://snyk.io/vuln/SNYK-PYTHON-LIBPESHKA-72535 56 | https://snyk.io/vuln/SNYK-PYTHON-SSHDECORATE-40786 57 | https://snyk.io/vuln/SNYK-PYTHON-ACQUSITION-40662 58 | https://snyk.io/vuln/SNYK-PYTHON-APIDEVCOOP-40663 59 | https://snyk.io/vuln/SNYK-PYTHON-BZIP-40664 60 | https://snyk.io/vuln/SNYK-PYTHON-CRYPT-40665 61 | https://snyk.io/vuln/SNYK-PYTHON-DJANGOSERVER-40666 62 | https://snyk.io/vuln/SNYK-PYTHON-PWD-40667 63 | https://snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-40668 64 | https://snyk.io/vuln/SNYK-PYTHON-TELNET-40669 65 | https://snyk.io/vuln/SNYK-PYTHON-URLIB3-40670 66 | https://snyk.io/vuln/SNYK-PYTHON-URLLIB-40671 67 | 68 | 文章参考链接: 69 | 70 | https://github.com/dateutil/dateutil/issues/984 71 | https://blog.reversinglabs.com/blog/suppy-chain-malware-detecting-malware-in-package-manager-repositories 72 | https://medium.com/@bertusk/detecting-cyber-attacks-in-the-python-package-index-pypi-61ab2b585c67 73 | https://medium.com/@bertusk/cryptocurrency-clipboard-hijacker-discovered-in-pypi-repository-b66b8a534a8 74 | https://www.bleepingcomputer.com/news/security/backdoored-python-library-caught-stealing-ssh-credentials/ 75 | https://www.bleepingcomputer.com/news/security/ten-malicious-libraries-found-on-pypi-python-package-index/ 76 | -------------------------------------------------------------------------------- /db/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/pypi/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/lib/__init__.py -------------------------------------------------------------------------------- /lib/consle_width.py: -------------------------------------------------------------------------------- 1 | """ getTerminalSize() 2 | - get width and height of console 3 | - works on linux,os x,windows,cygwin(windows) 4 | """ 5 | 6 | __all__=['getTerminalSize'] 7 | 8 | 9 | def getTerminalSize(): 10 | import platform 11 | current_os = platform.system() 12 | tuple_xy=None 13 | if current_os == 'Windows': 14 | tuple_xy = _getTerminalSize_windows() 15 | if tuple_xy is None: 16 | tuple_xy = _getTerminalSize_tput() 17 | # needed for window's python in cygwin's xterm! 18 | if current_os == 'Linux' or current_os == 'Darwin' or current_os.startswith('CYGWIN'): 19 | tuple_xy = _getTerminalSize_linux() 20 | if tuple_xy is None: 21 | print "default" 22 | tuple_xy = (80, 25) # default value 23 | return tuple_xy 24 | 25 | def _getTerminalSize_windows(): 26 | res=None 27 | try: 28 | from ctypes import windll, create_string_buffer 29 | 30 | # stdin handle is -10 31 | # stdout handle is -11 32 | # stderr handle is -12 33 | 34 | h = windll.kernel32.GetStdHandle(-12) 35 | csbi = create_string_buffer(22) 36 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 37 | except: 38 | return None 39 | if res: 40 | import struct 41 | (bufx, bufy, curx, cury, wattr, 42 | left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) 43 | sizex = right - left + 1 44 | sizey = bottom - top + 1 45 | return sizex, sizey 46 | else: 47 | return None 48 | 49 | def _getTerminalSize_tput(): 50 | # get terminal width 51 | # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window 52 | try: 53 | import subprocess 54 | proc=subprocess.Popen(["tput", "cols"],stdin=subprocess.PIPE,stdout=subprocess.PIPE) 55 | output=proc.communicate(input=None) 56 | cols=int(output[0]) 57 | proc=subprocess.Popen(["tput", "lines"],stdin=subprocess.PIPE,stdout=subprocess.PIPE) 58 | output=proc.communicate(input=None) 59 | rows=int(output[0]) 60 | return (cols,rows) 61 | except: 62 | return None 63 | 64 | 65 | def _getTerminalSize_linux(): 66 | def ioctl_GWINSZ(fd): 67 | try: 68 | import fcntl, termios, struct, os 69 | cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,'1234')) 70 | except: 71 | return None 72 | return cr 73 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 74 | if not cr: 75 | try: 76 | fd = os.open(os.ctermid(), os.O_RDONLY) 77 | cr = ioctl_GWINSZ(fd) 78 | os.close(fd) 79 | except: 80 | pass 81 | if not cr: 82 | try: 83 | cr = (env['LINES'], env['COLUMNS']) 84 | except: 85 | return None 86 | return int(cr[1]), int(cr[0]) 87 | 88 | if __name__ == "__main__": 89 | sizex,sizey=getTerminalSize() 90 | print 'width =',sizex,'height =',sizey -------------------------------------------------------------------------------- /lib/pypi_package.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import requests, os, tarfile, shutil, zipfile 4 | from bs4 import BeautifulSoup 5 | from scan_python import scan_python 6 | 7 | 8 | class Pypi_Scan: 9 | def __init__(self, systempath): 10 | self.systempath = systempath 11 | # 文件目录和解压目录 12 | self.tmppath = systempath + "/tmp/" 13 | # 数据存放目录 14 | self.dbpath = systempath + "/db/pypi/" 15 | # 日志存放目录 16 | self.logpath = systempath + "/log/log.txt" 17 | # pypi网站地址 18 | self.PypiURL = "https://pypi.org" 19 | # pypi包路径 20 | self.PypiURLSimple = "https://pypi.org/simple/" 21 | 22 | # 获取pypi有多少项目,项目的名称和链接 23 | def pypi_packages_projects(self): 24 | packagesProjects = [] 25 | response = requests.get(self.PypiURLSimple) 26 | if response.status_code != 200: return packagesProjects 27 | soup = BeautifulSoup(response.text, "html.parser") 28 | for a in soup.find_all('a'): 29 | packagesProjects.append({"name": a.string, "url": self.PypiURL + a['href']}) 30 | 31 | return packagesProjects 32 | 33 | def get_file_extension(self, filename): 34 | return filename.split(".")[-1] 35 | 36 | # 通过项目信息,获取项目安装文件和链接信息 37 | def get_packages_project_files(self, project): 38 | info = project 39 | response = requests.get(project["url"]) 40 | if response.status_code != 200: return 41 | soup = BeautifulSoup(response.text, "html.parser") 42 | projectInfo = [] 43 | for a in soup.find_all('a'): 44 | if self.get_file_extension(a.string) == "whl": 45 | projectInfo.append({"name": a.string, "url": a['href'], "type": "whl"}) 46 | else: 47 | projectInfo.append({"name": a.string, "url": a['href'], "type": "tar"}) 48 | 49 | info["versioninfo"] = projectInfo 50 | 51 | return info 52 | 53 | # 下载包文件 54 | def download_package_file(self, url_file, filename): 55 | try: 56 | r = requests.get(url_file, stream=True) 57 | f = open(filename, "wb") 58 | for chunk in r.iter_content(chunk_size=512): 59 | if chunk: 60 | f.write(chunk) 61 | return True 62 | except Exception as e: 63 | return False 64 | 65 | # 删除某目录下所有文件 66 | def del_file(self, tardir, filename): 67 | try: 68 | os.remove(filename) 69 | shutil.rmtree(tardir) 70 | except Exception as e: 71 | return False 72 | 73 | # 解压文件 74 | def untar(self, filename, path, type): 75 | try: 76 | if type == "whl": 77 | t = zipfile.ZipFile(filename) 78 | else: 79 | t = tarfile.open(filename) 80 | t.extractall(path=path) 81 | t.close() 82 | return True 83 | except Exception as e: 84 | return False 85 | 86 | # 拷贝恶意文件到指定留存目录 87 | def copyfile(self, projectname, filepath): 88 | try: 89 | path = self.dbpath + projectname 90 | if not os.path.exists(path): 91 | os.makedirs(path) 92 | shutil.copy(filepath, path) 93 | except Exception as e: 94 | print(e) 95 | return False 96 | 97 | # 扫描目录文件内容的安全性 98 | def scan_security(self, project, path): 99 | try: 100 | filepath = scan_python(self.systempath,path) 101 | if not filepath: return False 102 | self.copyfile(project, filepath) 103 | return True 104 | except Exception as e: 105 | return False 106 | 107 | # 扫描pypi包的安全性 108 | # 参数:项目名称、包地址、文件下载到本地文件路径、解压路径、包类型 109 | # 处理流程: 110 | # 1、下载安全包 111 | # 2、解压安全包 112 | # 3、扫描恶意内容 113 | # 4、存储恶意文件 114 | # 5、删除临时文件 115 | def scan_package(self, project, url, filename, tardir, type): 116 | security = False 117 | # 下载文件 118 | self.download_package_file(url, filename) 119 | # 解压文件 120 | if self.untar(filename, tardir, type): 121 | # 扫描文件内容 122 | if self.scan_security(project, tardir): 123 | security = True 124 | # 删除临时文件目录 125 | self.del_file(tardir, filename) 126 | return security 127 | 128 | 129 | if __name__ == "__main__": 130 | # Pypi_Scan("/Users/grayddq/Grayddq/01.mygit/21.PypiScan/").scan_security("a","/Users/grayddq/Grayddq/01.mygit/21.PypiScan/tmp/libpeshnx-0.1") 131 | if Pypi_Scan("/Users/grayddq/Grayddq/01.mygit/21.PypiScan/").scan_package("0121", 132 | "https://files.pythonhosted.org/packages/99/b1/8329b44e81c794ebe8772531fbb94df3afb107102d183c1b0a17abb49471/0121-0.0.1.tar.gz#sha256=c340f511c652c50e67fac4e85528064f4253f5850446c9258949574a5d541f92", 133 | "/Users/grayddq/Grayddq/01.mygit/21.PypiScan/tmp/0121-0.0.1.tar.gz", 134 | "/Users/grayddq/Grayddq/01.mygit/21.PypiScan/tmp/0121/", 135 | "gz"): 136 | print("True") 137 | -------------------------------------------------------------------------------- /lib/rule/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/lib/rule/__init__.py -------------------------------------------------------------------------------- /lib/rule/content.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # 解码类 5 | # rule1 6 | encry_keywords = ["base64.b64decode("] 7 | 8 | # rule2 9 | # 网络传输类 10 | net_keywords = ["socket.socket", 11 | "urllib.request", 12 | "urllib.urlopen", 13 | "urllib2.urlopen", 14 | "requests.get", 15 | "requests.post" 16 | ] 17 | 18 | # rule3 19 | # 命令执行类 20 | exec_keywords = ["os.system", 21 | "os.popen", 22 | "commands.getstatusoutput", 23 | "commands.getoutput", 24 | "commands.getstatus", 25 | "os.chmod", 26 | " exec(" 27 | ] 28 | 29 | # rule4 30 | # 本地敏感文件 31 | local_keywords = [".bashrc", 32 | "/.ssh/id_rsa" 33 | ] 34 | 35 | # rule5 36 | # 敏感特征 37 | malice_keywords = ["exec(base64.b64decode("] 38 | 39 | 40 | # 规则,符合如下 41 | # rule2 + rule1 42 | # rule2 + rule3 43 | # rule2 + rule4 44 | # rule5 45 | 46 | 47 | def Check(filestr,filename): 48 | filestr = filestr.lower() 49 | rule1, rule2, rule3, rule4, rule5 = False, False, False, False, False 50 | 51 | if filename == "setup.py": 52 | if "base64.b64decode(" in filestr: 53 | return True 54 | 55 | for key in encry_keywords: 56 | if key in filestr: 57 | rule1 = True 58 | for key in net_keywords: 59 | if key in filestr: 60 | rule2 = True 61 | for key in exec_keywords: 62 | if key in filestr: 63 | rule3 = True 64 | for key in local_keywords: 65 | if key in filestr: 66 | rule4 = True 67 | for key in malice_keywords: 68 | if key in filestr: 69 | rule5 = True 70 | 71 | if rule1 & rule2: 72 | return True 73 | elif rule1 & rule3: 74 | return True 75 | elif rule2 & rule3: 76 | return True 77 | elif rule2 & rule4: 78 | return True 79 | elif rule5: 80 | return True 81 | else: 82 | return False 83 | -------------------------------------------------------------------------------- /lib/rule/start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # 启动项类 5 | start_keywords = ["Software\Microsoft\Windows\CurrentVersion\Run"] 6 | 7 | 8 | def Check(filestr, filename): 9 | filestr = filestr.lower() 10 | 11 | for key in start_keywords: 12 | if key in filestr: 13 | return True 14 | 15 | return False 16 | -------------------------------------------------------------------------------- /lib/scan_python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | 5 | 6 | def load_rule(systempath): 7 | rules = [] 8 | for root, dirs, files in os.walk(systempath + "/lib/rule"): 9 | for filespath in files: 10 | if filespath[-3:] == '.py': 11 | rulename = filespath[:-3] 12 | if rulename == '__init__': 13 | continue 14 | __import__('lib.rule.' + rulename) 15 | rules.append(rulename) 16 | return rules 17 | 18 | 19 | def scan_python(systempath, path): 20 | rules = load_rule(systempath) 21 | for root, dirs, files in os.walk(path): 22 | for filename in files: 23 | if filename.split(".")[-1] != 'py': continue 24 | filepath = os.path.join(root, filename) 25 | if os.path.getsize(filepath) < 500000: 26 | for rule in rules: 27 | file = open(filepath, "rb") 28 | filestr = file.read() 29 | file.close() 30 | if sys.modules['lib.rule.' + rule].Check(filestr,filename): 31 | return filepath 32 | return "" 33 | 34 | 35 | if __name__ == "__main__": 36 | print scan_python("/Users/grayddq/Grayddq/01.mygit/21.PypiScan/test/libpeshnx-0.1") 37 | -------------------------------------------------------------------------------- /lib/threads.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time, threading, Queue, sys, os 4 | from consle_width import getTerminalSize 5 | from pypi_package import * 6 | 7 | 8 | class Threads_Scan: 9 | def __init__(self, thread=10, systempath=os.path.dirname(os.path.abspath(__file__))): 10 | # 线程数 11 | self.thread = thread 12 | # 系统目录 13 | self.system_path = systempath 14 | # 日志存放目录 15 | self.logpath = systempath + "/log/log.txt" 16 | # 文件目录和解压目录 17 | self.tmppath = systempath + "/tmp/" 18 | # 日志存储 19 | self.outfile = open(self.logpath, 'w') 20 | # 多线程框架 21 | self.thread_count = 0 22 | self.scan_count = self.found_count = 0 23 | self.lock = threading.Lock() 24 | self.console_width = getTerminalSize()[0] - 2 25 | self.msg_queue = Queue.Queue() 26 | self.STOP_ME = False 27 | threading.Thread(target=self._print_msg).start() 28 | self._init_queue() 29 | 30 | def _print_msg(self): 31 | while not self.STOP_ME: 32 | try: 33 | _msg = self.msg_queue.get(timeout=0.1) 34 | except: 35 | continue 36 | 37 | if _msg == 'status': 38 | msg = '%s Found| %s projects| %s scanned in %.1f seconds| %s threads' % ( 39 | self.found_count, self.queue.qsize(), self.scan_count, time.time() - self.start_time, 40 | self.thread_count) 41 | sys.stdout.write('\r' + ' ' * (self.console_width - len(msg)) + msg) 42 | else: 43 | sys.stdout.write('\r' + _msg + ' ' * (self.console_width - len(_msg)) + '\n') 44 | sys.stdout.flush() 45 | 46 | def _update_scan_count(self): 47 | self.last_scanned = time.time() 48 | self.scan_count += 1 49 | 50 | def _update_found_count(self): 51 | self.found_count += 1 52 | 53 | # 获取项目信息,并加入队列中 54 | def _init_queue(self): 55 | self.msg_queue.put('[+] Initializing, get pypi package...') 56 | self.queue = Queue.Queue() 57 | 58 | project = Pypi_Scan(self.system_path).pypi_packages_projects() 59 | # 加入队列 60 | for info in project: 61 | self.queue.put(info) 62 | self.msg_queue.put('[+] Found pypi project %d in total' % len(project)) 63 | 64 | self.outfile.write('[+] Found pypi project %d in total\n' % len(project)) 65 | self.outfile.flush() 66 | 67 | # 开始多线程扫描 68 | def _scan(self): 69 | self.lock.acquire() 70 | self.thread_count += 1 71 | self.lock.release() 72 | 73 | pypiScan = Pypi_Scan(self.system_path) 74 | 75 | while not self.STOP_ME: 76 | try: 77 | lst_info = self.queue.get(timeout=0.1) 78 | except Queue.Empty: 79 | break 80 | 81 | while not self.STOP_ME: 82 | self._update_scan_count() 83 | self.msg_queue.put('status') 84 | 85 | # 获取project项目的包信息 86 | info = pypiScan.get_packages_project_files(lst_info) 87 | for t in info["versioninfo"]: 88 | # 扫描项目中的包信息 89 | # 项目名称、包地址、文件下载到本地文件路径、解压路径、包类型 90 | if pypiScan.scan_package(info["name"], t["url"], self.tmppath + t["name"], 91 | self.tmppath + info["name"], t["type"]): 92 | # 检测到恶意代码 93 | self._update_found_count() 94 | msg = ("Malicious project,project: %s, version: %s" % (info["name"], t["name"])).ljust(30) 95 | self.msg_queue.put(msg) 96 | self.msg_queue.put('status') 97 | self.outfile.write(msg + '\n') 98 | self.outfile.flush() 99 | break 100 | break 101 | 102 | self.lock.acquire() 103 | self.thread_count -= 1 104 | self.lock.release() 105 | self.msg_queue.put('status') 106 | 107 | def run(self): 108 | self.msg_queue.put('[+] start scan projects ...') 109 | self.start_time = time.time() 110 | for i in range(self.thread): 111 | try: 112 | t = threading.Thread(target=self._scan, name=str(i)) 113 | t.setDaemon(True) 114 | t.start() 115 | except: 116 | pass 117 | while self.thread_count > 0: 118 | try: 119 | time.sleep(1.0) 120 | except KeyboardInterrupt, e: 121 | msg = '[WARNING] User aborted, wait all slave threads to exit...' 122 | sys.stdout.write('\r' + msg + ' ' * (self.console_width - len(msg)) + '\n\r') 123 | sys.stdout.flush() 124 | self.STOP_ME = True 125 | self.STOP_ME = True 126 | -------------------------------------------------------------------------------- /log/log.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/log/log.txt -------------------------------------------------------------------------------- /pic/111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/pic/111.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | beautifulsoup4==4.8.2 3 | -------------------------------------------------------------------------------- /test/Acqusition-4.4.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/test/Acqusition-4.4.2.tar.gz -------------------------------------------------------------------------------- /test/dark-magic-0.1.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/test/dark-magic-0.1.2.tar.gz -------------------------------------------------------------------------------- /test/jeIlyfish-0.7.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/test/jeIlyfish-0.7.0.tar.gz -------------------------------------------------------------------------------- /test/libpeshnx-0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/test/libpeshnx-0.1.tar.gz -------------------------------------------------------------------------------- /test/reols-0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/test/reols-0.1.tar.gz -------------------------------------------------------------------------------- /test/req-tools-0.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/test/req-tools-0.4.tar.gz -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grayddq/PypiScan/843b66d37327fb6c3bb5c262bc8e68fb8c70b349/tmp/.gitkeep --------------------------------------------------------------------------------