├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── CI.yml ├── .gitignore ├── BENCHMARKS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── architecture └── README.md ├── assets ├── 2pyscan-repository.png ├── flowchart.png ├── pyscan.png └── snake.png ├── osv_schema.json ├── pyproject.toml ├── python └── pyscan │ ├── __init__.py │ └── __main__.py └── src ├── display └── mod.rs ├── docker └── mod.rs ├── main.rs ├── parser ├── extractor.rs ├── mod.rs └── structs.rs ├── scanner ├── api.rs ├── mod.rs └── models.rs └── utils.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: aswinnnn # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v0.15.1 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - '*' 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | linux: 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | target: [x86_64] 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Setup Python 30 | uses: actions/setup-python@v3.1.4 31 | - name: Setup Rust 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | toolchain: stable 35 | components: cargo 36 | - name: Install maturin and build 37 | run: pip install maturin && maturin build --release --out dist 38 | windows: 39 | runs-on: windows-latest 40 | strategy: 41 | matrix: 42 | target: [x64, x86] 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Build wheels 46 | uses: PyO3/maturin-action@v1 47 | with: 48 | target: ${{ matrix.target }} 49 | args: --release --out dist 50 | sccache: 'true' 51 | - name: Upload wheels 52 | uses: actions/upload-artifact@v3 53 | with: 54 | name: wheels 55 | path: dist 56 | 57 | macos: 58 | runs-on: macos-latest 59 | strategy: 60 | matrix: 61 | target: [x86_64, aarch64] 62 | steps: 63 | - uses: actions/checkout@v3 64 | - name: Build wheels 65 | uses: PyO3/maturin-action@v1 66 | with: 67 | target: ${{ matrix.target }} 68 | args: --release --out dist 69 | sccache: 'true' 70 | - name: Upload wheels 71 | uses: actions/upload-artifact@v3 72 | with: 73 | name: wheels 74 | path: dist 75 | 76 | sdist: 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v3 80 | - name: Build sdist 81 | uses: PyO3/maturin-action@v1 82 | with: 83 | command: sdist 84 | args: --out dist 85 | - name: Upload sdist 86 | uses: actions/upload-artifact@v3 87 | with: 88 | name: wheels 89 | path: dist 90 | 91 | release: 92 | name: Release 93 | runs-on: ubuntu-latest 94 | if: "startsWith(github.ref, 'refs/tags/')" 95 | needs: [linux, windows, macos, sdist] 96 | steps: 97 | - uses: actions/download-artifact@v3 98 | with: 99 | name: wheels 100 | - name: Publish to PyPI 101 | uses: PyO3/maturin-action@v1 102 | env: 103 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 104 | with: 105 | command: upload 106 | args: --skip-existing * 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version 73 | out.txt 74 | requirements.txt 75 | ptest 76 | 77 | tests 78 | -------------------------------------------------------------------------------- /BENCHMARKS.md: -------------------------------------------------------------------------------- 1 | # 🚀 Benchmarks 2 | 3 | - performed on a `requirements.txt` containing **234** packages along with their versions. 4 | - I know a benchmark is usually done with something to compare against, but I couldn't find anything like pyscan, at least not yet. 5 | - Reccomend something that can be tested along with pyscan! 6 | - the benchmark has been performed, using [hyperfine](https://github.com/sharkdp/hyperfine) with the following command : 7 | 8 | ```bash 9 | hyperfine --runs 3 '.\target\release\pyscan.exe' --shell=none --export-markdown benchmarks.md --warmup 1 10 | ``` 11 | 12 | | Command | Mean [s] | Min [s] | Max [s] | Relative | 13 | |:---|---:|---:|---:|---:| 14 | | `'.\target\release\pyscan.exe'` | 23.345 ± 0.892 | 22.731 | 24.369 | 1.00 | 15 | 16 | 17 | - As pyscan mainly depends on making API calls, this benchmark is obviously almost variable. 18 | 19 | There will be consistent effort regarding the optimization of pyscan in the future. This benchmark was 6min 8s before I switched to a batched API and started using references instead of moving, imagine what it'll be in the coming months! Still learning, still growing. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.1 4 | 5 | - added package subcommand, here's a quick usage: 6 | 7 | ```bash 8 | pyscan package -n jinja2 -v 2.4.1 9 | ``` 10 | 11 | - slight logic improvments 12 | - notes for next release: 13 | - - if it detects toml but it doesnt find the dependencies table it panics, no idea how to err handle that for now 14 | - - I should probably start using the `anyhow` crate. 15 | - - `get_latest_package_version` should become its own function and be moved to `utils.rs` in the next version 16 | 17 | That's all for this release! 18 | 19 | ## 0.1.2 20 | 21 | - added docker subcommand, usage: 22 | ```bash 23 | > pyscan docker -n my-docker-image -p /path/inside/container/to/source 24 | ``` 25 | 26 | by "source" I mean `requirements.txt`, `pyproject.toml` or your python files. 27 | 28 | - pyscan will not be using [deps.dev](https://deps.dev) API anymore to retrive latest stable versions. Will be using `pip` instead to get the installed package version from the user. Should've thought of that sooner. [credits to @anotherbridge for [#1](https://github.com/aswinnnn/pyscan/issues/1)] 29 | 30 | - better error messages, though panics are the main way of displaying them. 31 | 32 | - This release was pretty rushed to fix that issue and get the docker feature on. I will be taking my sweet time with the next release to get: 33 | 34 | - - github actions integration 35 | - - make it easier for other tools to interact with pyscan 36 | - - code complexity analyzer (not doing a linter cuz any respectable python dev already has one) 37 | - - finally get to do tests, and lots of more ideas in my head. Thanks for the awesome support so far! 38 | 39 | ## 0.1.3 40 | 41 | - Fixed a grave error where docker command left remnants and did not perform a complete cleanup. 42 | - This release was made right after the previous release to fix this feature, however, the release page will contain both this message and the previous one so no one will miss out on the new stuff. 43 | 44 | ## 0.1.4 (the "big" update) 45 | 46 | ### Changes and New 47 | 48 | - BATCHED API! Pyscan is actually fast enough now. [#5] 49 | - Less panics and more user friendly errors. 50 | - Perfomance optimizations by some &s and better logic. 51 | - Support for constraints.txt [#4] 52 | - Introduced PipCache, which caches your pip package names and versions before the execution of the scanner to quickly lookup incase of a fallback 53 | - also, fallbacks! [#3] the order is: source > pip > pypi.org 54 | - it can be disabled with only sticking to `--pip` or `--pypi` or `--source` 55 | - exit non-zeros at vulns found and other important errors 56 | 57 | ### Notes 58 | - I actually wanted to include multi-threaded batched requests to increase perfomance even more 59 | - but had to rush the update because everyone was installing the pathetic previous one. It's like hiding a golden apple that you can't show anyone. (except people who noticed the alpha branch) 60 | - I will try not to rush updates and actually take things slow but thats hard when its recieving so much attention 61 | - [RealPython](realpython.com) featured this project on their podcast which was just amazing, and something that has never happened to me before. 62 | - Twitter and imageboards (the good ones) are giving pyscan so much love. 63 | - All the issue makers have led to some very awesome improvements, I fucking love open source. 64 | 65 | That's about it, check TODO for whats coming in the future. 66 | 67 | ## v0.1.6 (October 15, 2023) 68 | 69 | *v0.1.5 had a bugfix to fix a critical bug accidently deployed in v0.1.4, immediately. Thus, i dont think it deserves its own thingy.* 70 | 71 | ### New Features 72 | 73 | - implement parsing dependencies from `setup.py`,`setuptools`,`poetry`,`hatch`,`filt`, `pdm` 74 | - multithreaded requests for `> 100` dependencies 75 | - output options 76 | 77 | ### Fixes 78 | 79 | This version was focused on: 80 | 81 | - #13 [fixed] 82 | - #14 [fixed] 83 | - #11 - This will took some time as parsing of pyproject.toml is hard-coded to only support PEP 621, which means redesigning how pyproject.toml should be scanned entirely. [fixed] 84 | 85 | ### Notes 86 | 87 | Pyscan has some **very interesting developments** planned in the future. Checkout the PR. 88 | 89 | - [ ] the crate `pep-508` seems to be having trouble parsing embedded hash values in `requirements.txt` ( #16 ), which may or may not have a fix depending on the author of the lib. 90 | - [ ] (maybe) support for parsing SBOMs and KBOMs 91 | - [ ] (maybe) introduce displaying severity, along with a filter for known vuln IDs. 92 | 93 | 94 | ## v0.1.7 (December 24,2024) 95 | 96 | ### Notes 97 | 98 | - Includes critical bug fixes for #19 and #20 99 | - Fixes up the parsing logic a bit 100 | 101 | The PR and the "big" update is still an ongoing effort, slowed down due to my recent lack of time (college, part-time work). 102 | 103 | Consider **donating** if you *actually* use this tool, as I'm thinking about archiving it after some maintanence done. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | aswinsnair@protonmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | - Read the README.md file. This file will provide you with an overview of the project, its goals, and how to contribute. 4 | - Fork the repository. This will create a copy of the repository on your own GitHub account. 5 | - Create a branch for your changes. This will allow you to work on your changes without affecting the main branch of the repository. 6 | - Make your changes. Please follow the coding style of the project. 7 | - Test your changes. Run the tests to make sure your changes do not introduce any new bugs. 8 | - Submit a pull request. This will allow the project maintainers to review your changes and merge them into the main branch of the repository. 9 | 10 | Here are some additional guidelines that you may want to consider: 11 | 12 | - Use a consistent coding style. This will make it easier for other contributors to understand and work with your code. 13 | - Write unit tests for your changes. This will help to ensure that your changes do not introduce any new bugs. 14 | - Document your changes. This will help other contributors to understand what your changes do and how to use them. 15 | - Be respectful of other contributors. This is a community project, so please be respectful of the work of others. 16 | 17 | Thank you for your interest in contributing to this project! 18 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "aho-corasick" 18 | version = "1.0.1" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 21 | dependencies = [ 22 | "memchr", 23 | ] 24 | 25 | [[package]] 26 | name = "android_system_properties" 27 | version = "0.1.5" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 30 | dependencies = [ 31 | "libc", 32 | ] 33 | 34 | [[package]] 35 | name = "anstream" 36 | version = "0.3.2" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 39 | dependencies = [ 40 | "anstyle", 41 | "anstyle-parse", 42 | "anstyle-query", 43 | "anstyle-wincon", 44 | "colorchoice", 45 | "is-terminal", 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 54 | 55 | [[package]] 56 | name = "anstyle-parse" 57 | version = "0.2.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 60 | dependencies = [ 61 | "utf8parse", 62 | ] 63 | 64 | [[package]] 65 | name = "anstyle-query" 66 | version = "1.0.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 69 | dependencies = [ 70 | "windows-sys 0.48.0", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-wincon" 75 | version = "1.0.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 78 | dependencies = [ 79 | "anstyle", 80 | "windows-sys 0.48.0", 81 | ] 82 | 83 | [[package]] 84 | name = "autocfg" 85 | version = "1.1.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 88 | 89 | [[package]] 90 | name = "base64" 91 | version = "0.21.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 94 | 95 | [[package]] 96 | name = "bitflags" 97 | version = "1.3.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 100 | 101 | [[package]] 102 | name = "bumpalo" 103 | version = "3.12.2" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" 106 | 107 | [[package]] 108 | name = "bytes" 109 | version = "1.4.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 112 | 113 | [[package]] 114 | name = "cc" 115 | version = "1.0.79" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 118 | 119 | [[package]] 120 | name = "cfg-if" 121 | version = "1.0.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 124 | 125 | [[package]] 126 | name = "chrono" 127 | version = "0.4.24" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 130 | dependencies = [ 131 | "iana-time-zone", 132 | "js-sys", 133 | "num-integer", 134 | "num-traits", 135 | "time", 136 | "wasm-bindgen", 137 | "winapi", 138 | ] 139 | 140 | [[package]] 141 | name = "chumsky" 142 | version = "1.0.0-alpha.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "cc3172a80699de358070dd99f80ea8badc6cdf8ac2417cb5a96e6d81bf5fe06d" 145 | dependencies = [ 146 | "hashbrown 0.13.2", 147 | "stacker", 148 | ] 149 | 150 | [[package]] 151 | name = "clap" 152 | version = "4.2.7" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" 155 | dependencies = [ 156 | "clap_builder", 157 | "clap_derive", 158 | "once_cell", 159 | ] 160 | 161 | [[package]] 162 | name = "clap_builder" 163 | version = "4.2.7" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" 166 | dependencies = [ 167 | "anstream", 168 | "anstyle", 169 | "bitflags", 170 | "clap_lex", 171 | "strsim", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_derive" 176 | version = "4.2.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" 179 | dependencies = [ 180 | "heck", 181 | "proc-macro2", 182 | "quote", 183 | "syn", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_lex" 188 | version = "0.4.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" 191 | 192 | [[package]] 193 | name = "colorchoice" 194 | version = "1.0.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 197 | 198 | [[package]] 199 | name = "console" 200 | version = "0.15.5" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 203 | dependencies = [ 204 | "encode_unicode", 205 | "lazy_static", 206 | "libc", 207 | "unicode-width", 208 | "windows-sys 0.42.0", 209 | ] 210 | 211 | [[package]] 212 | name = "core-foundation" 213 | version = "0.9.3" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 216 | dependencies = [ 217 | "core-foundation-sys", 218 | "libc", 219 | ] 220 | 221 | [[package]] 222 | name = "core-foundation-sys" 223 | version = "0.8.4" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 226 | 227 | [[package]] 228 | name = "encode_unicode" 229 | version = "0.3.6" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 232 | 233 | [[package]] 234 | name = "encoding_rs" 235 | version = "0.8.32" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 238 | dependencies = [ 239 | "cfg-if", 240 | ] 241 | 242 | [[package]] 243 | name = "errno" 244 | version = "0.3.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 247 | dependencies = [ 248 | "errno-dragonfly", 249 | "libc", 250 | "windows-sys 0.48.0", 251 | ] 252 | 253 | [[package]] 254 | name = "errno-dragonfly" 255 | version = "0.1.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 258 | dependencies = [ 259 | "cc", 260 | "libc", 261 | ] 262 | 263 | [[package]] 264 | name = "fastrand" 265 | version = "1.9.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 268 | dependencies = [ 269 | "instant", 270 | ] 271 | 272 | [[package]] 273 | name = "fnv" 274 | version = "1.0.7" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 277 | 278 | [[package]] 279 | name = "foreign-types" 280 | version = "0.3.2" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 283 | dependencies = [ 284 | "foreign-types-shared", 285 | ] 286 | 287 | [[package]] 288 | name = "foreign-types-shared" 289 | version = "0.1.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 292 | 293 | [[package]] 294 | name = "form_urlencoded" 295 | version = "1.1.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 298 | dependencies = [ 299 | "percent-encoding", 300 | ] 301 | 302 | [[package]] 303 | name = "futures" 304 | version = "0.3.28" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 307 | dependencies = [ 308 | "futures-channel", 309 | "futures-core", 310 | "futures-executor", 311 | "futures-io", 312 | "futures-sink", 313 | "futures-task", 314 | "futures-util", 315 | ] 316 | 317 | [[package]] 318 | name = "futures-channel" 319 | version = "0.3.28" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 322 | dependencies = [ 323 | "futures-core", 324 | "futures-sink", 325 | ] 326 | 327 | [[package]] 328 | name = "futures-core" 329 | version = "0.3.28" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 332 | 333 | [[package]] 334 | name = "futures-executor" 335 | version = "0.3.28" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 338 | dependencies = [ 339 | "futures-core", 340 | "futures-task", 341 | "futures-util", 342 | ] 343 | 344 | [[package]] 345 | name = "futures-io" 346 | version = "0.3.28" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 349 | 350 | [[package]] 351 | name = "futures-macro" 352 | version = "0.3.28" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 355 | dependencies = [ 356 | "proc-macro2", 357 | "quote", 358 | "syn", 359 | ] 360 | 361 | [[package]] 362 | name = "futures-sink" 363 | version = "0.3.28" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 366 | 367 | [[package]] 368 | name = "futures-task" 369 | version = "0.3.28" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 372 | 373 | [[package]] 374 | name = "futures-util" 375 | version = "0.3.28" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 378 | dependencies = [ 379 | "futures-channel", 380 | "futures-core", 381 | "futures-io", 382 | "futures-macro", 383 | "futures-sink", 384 | "futures-task", 385 | "memchr", 386 | "pin-project-lite", 387 | "pin-utils", 388 | "slab", 389 | ] 390 | 391 | [[package]] 392 | name = "h2" 393 | version = "0.3.19" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" 396 | dependencies = [ 397 | "bytes", 398 | "fnv", 399 | "futures-core", 400 | "futures-sink", 401 | "futures-util", 402 | "http", 403 | "indexmap", 404 | "slab", 405 | "tokio", 406 | "tokio-util", 407 | "tracing", 408 | ] 409 | 410 | [[package]] 411 | name = "hashbrown" 412 | version = "0.12.3" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 415 | 416 | [[package]] 417 | name = "hashbrown" 418 | version = "0.13.2" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 421 | dependencies = [ 422 | "ahash", 423 | ] 424 | 425 | [[package]] 426 | name = "heck" 427 | version = "0.4.1" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 430 | 431 | [[package]] 432 | name = "hermit-abi" 433 | version = "0.2.6" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 436 | dependencies = [ 437 | "libc", 438 | ] 439 | 440 | [[package]] 441 | name = "hermit-abi" 442 | version = "0.3.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 445 | 446 | [[package]] 447 | name = "http" 448 | version = "0.2.9" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 451 | dependencies = [ 452 | "bytes", 453 | "fnv", 454 | "itoa", 455 | ] 456 | 457 | [[package]] 458 | name = "http-body" 459 | version = "0.4.5" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 462 | dependencies = [ 463 | "bytes", 464 | "http", 465 | "pin-project-lite", 466 | ] 467 | 468 | [[package]] 469 | name = "httparse" 470 | version = "1.8.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 473 | 474 | [[package]] 475 | name = "httpdate" 476 | version = "1.0.2" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 479 | 480 | [[package]] 481 | name = "hyper" 482 | version = "0.14.26" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" 485 | dependencies = [ 486 | "bytes", 487 | "futures-channel", 488 | "futures-core", 489 | "futures-util", 490 | "h2", 491 | "http", 492 | "http-body", 493 | "httparse", 494 | "httpdate", 495 | "itoa", 496 | "pin-project-lite", 497 | "socket2", 498 | "tokio", 499 | "tower-service", 500 | "tracing", 501 | "want", 502 | ] 503 | 504 | [[package]] 505 | name = "hyper-tls" 506 | version = "0.5.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 509 | dependencies = [ 510 | "bytes", 511 | "hyper", 512 | "native-tls", 513 | "tokio", 514 | "tokio-native-tls", 515 | ] 516 | 517 | [[package]] 518 | name = "iana-time-zone" 519 | version = "0.1.56" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" 522 | dependencies = [ 523 | "android_system_properties", 524 | "core-foundation-sys", 525 | "iana-time-zone-haiku", 526 | "js-sys", 527 | "wasm-bindgen", 528 | "windows", 529 | ] 530 | 531 | [[package]] 532 | name = "iana-time-zone-haiku" 533 | version = "0.1.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 536 | dependencies = [ 537 | "cc", 538 | ] 539 | 540 | [[package]] 541 | name = "idna" 542 | version = "0.3.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 545 | dependencies = [ 546 | "unicode-bidi", 547 | "unicode-normalization", 548 | ] 549 | 550 | [[package]] 551 | name = "indexmap" 552 | version = "1.9.3" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 555 | dependencies = [ 556 | "autocfg", 557 | "hashbrown 0.12.3", 558 | ] 559 | 560 | [[package]] 561 | name = "instant" 562 | version = "0.1.12" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 565 | dependencies = [ 566 | "cfg-if", 567 | ] 568 | 569 | [[package]] 570 | name = "io-lifetimes" 571 | version = "1.0.10" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" 574 | dependencies = [ 575 | "hermit-abi 0.3.1", 576 | "libc", 577 | "windows-sys 0.48.0", 578 | ] 579 | 580 | [[package]] 581 | name = "ipnet" 582 | version = "2.7.2" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" 585 | 586 | [[package]] 587 | name = "is-terminal" 588 | version = "0.4.7" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 591 | dependencies = [ 592 | "hermit-abi 0.3.1", 593 | "io-lifetimes", 594 | "rustix", 595 | "windows-sys 0.48.0", 596 | ] 597 | 598 | [[package]] 599 | name = "itoa" 600 | version = "1.0.6" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 603 | 604 | [[package]] 605 | name = "js-sys" 606 | version = "0.3.62" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" 609 | dependencies = [ 610 | "wasm-bindgen", 611 | ] 612 | 613 | [[package]] 614 | name = "lazy_static" 615 | version = "1.4.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 618 | 619 | [[package]] 620 | name = "lenient_semver" 621 | version = "0.4.2" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "de8de3f4f3754c280ce1c8c42ed8dd26a9c8385c2e5ad4ec5a77e774cea9c1ec" 624 | dependencies = [ 625 | "lenient_semver_parser", 626 | "lenient_version", 627 | "semver", 628 | ] 629 | 630 | [[package]] 631 | name = "lenient_semver_parser" 632 | version = "0.4.2" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "7f650c1d024ddc26b4bb79c3076b30030f2cf2b18292af698c81f7337a64d7d6" 635 | dependencies = [ 636 | "lenient_semver_version_builder", 637 | "semver", 638 | ] 639 | 640 | [[package]] 641 | name = "lenient_semver_version_builder" 642 | version = "0.4.2" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "9049f8ff49f75b946f95557148e70230499c8a642bf2d6528246afc7d0282d17" 645 | dependencies = [ 646 | "semver", 647 | ] 648 | 649 | [[package]] 650 | name = "lenient_version" 651 | version = "0.4.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "bad7b41cc0ad9b8a9f8d8fcb7c2ab6703a6da4b369cbb7e3a63ee0840769b4eb" 654 | dependencies = [ 655 | "lenient_semver_parser", 656 | "lenient_semver_version_builder", 657 | "semver", 658 | ] 659 | 660 | [[package]] 661 | name = "libc" 662 | version = "0.2.144" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 665 | 666 | [[package]] 667 | name = "linux-raw-sys" 668 | version = "0.3.7" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" 671 | 672 | [[package]] 673 | name = "log" 674 | version = "0.4.17" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 677 | dependencies = [ 678 | "cfg-if", 679 | ] 680 | 681 | [[package]] 682 | name = "memchr" 683 | version = "2.5.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 686 | 687 | [[package]] 688 | name = "mime" 689 | version = "0.3.17" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 692 | 693 | [[package]] 694 | name = "mio" 695 | version = "0.8.6" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 698 | dependencies = [ 699 | "libc", 700 | "log", 701 | "wasi 0.11.0+wasi-snapshot-preview1", 702 | "windows-sys 0.45.0", 703 | ] 704 | 705 | [[package]] 706 | name = "native-tls" 707 | version = "0.2.11" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 710 | dependencies = [ 711 | "lazy_static", 712 | "libc", 713 | "log", 714 | "openssl", 715 | "openssl-probe", 716 | "openssl-sys", 717 | "schannel", 718 | "security-framework", 719 | "security-framework-sys", 720 | "tempfile", 721 | ] 722 | 723 | [[package]] 724 | name = "num-integer" 725 | version = "0.1.45" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 728 | dependencies = [ 729 | "autocfg", 730 | "num-traits", 731 | ] 732 | 733 | [[package]] 734 | name = "num-traits" 735 | version = "0.2.15" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 738 | dependencies = [ 739 | "autocfg", 740 | ] 741 | 742 | [[package]] 743 | name = "num_cpus" 744 | version = "1.15.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 747 | dependencies = [ 748 | "hermit-abi 0.2.6", 749 | "libc", 750 | ] 751 | 752 | [[package]] 753 | name = "once_cell" 754 | version = "1.18.0" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 757 | 758 | [[package]] 759 | name = "openssl" 760 | version = "0.10.52" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" 763 | dependencies = [ 764 | "bitflags", 765 | "cfg-if", 766 | "foreign-types", 767 | "libc", 768 | "once_cell", 769 | "openssl-macros", 770 | "openssl-sys", 771 | ] 772 | 773 | [[package]] 774 | name = "openssl-macros" 775 | version = "0.1.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 778 | dependencies = [ 779 | "proc-macro2", 780 | "quote", 781 | "syn", 782 | ] 783 | 784 | [[package]] 785 | name = "openssl-probe" 786 | version = "0.1.5" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 789 | 790 | [[package]] 791 | name = "openssl-sys" 792 | version = "0.9.87" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" 795 | dependencies = [ 796 | "cc", 797 | "libc", 798 | "pkg-config", 799 | "vcpkg", 800 | ] 801 | 802 | [[package]] 803 | name = "pep-508" 804 | version = "0.3.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "daec7940032badfd65fe9a11705ecbea49c77269ca81934060f33952af010da3" 807 | dependencies = [ 808 | "chumsky", 809 | ] 810 | 811 | [[package]] 812 | name = "percent-encoding" 813 | version = "2.2.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 816 | 817 | [[package]] 818 | name = "pin-project-lite" 819 | version = "0.2.9" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 822 | 823 | [[package]] 824 | name = "pin-utils" 825 | version = "0.1.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 828 | 829 | [[package]] 830 | name = "pkg-config" 831 | version = "0.3.27" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 834 | 835 | [[package]] 836 | name = "proc-macro2" 837 | version = "1.0.57" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16" 840 | dependencies = [ 841 | "unicode-ident", 842 | ] 843 | 844 | [[package]] 845 | name = "psm" 846 | version = "0.1.21" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" 849 | dependencies = [ 850 | "cc", 851 | ] 852 | 853 | [[package]] 854 | name = "pyscan" 855 | version = "0.1.7" 856 | dependencies = [ 857 | "chrono", 858 | "clap", 859 | "console", 860 | "futures", 861 | "lazy_static", 862 | "lenient_semver", 863 | "once_cell", 864 | "pep-508", 865 | "regex", 866 | "reqwest", 867 | "semver", 868 | "serde", 869 | "serde_json", 870 | "tokio", 871 | "toml", 872 | ] 873 | 874 | [[package]] 875 | name = "quote" 876 | version = "1.0.27" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" 879 | dependencies = [ 880 | "proc-macro2", 881 | ] 882 | 883 | [[package]] 884 | name = "redox_syscall" 885 | version = "0.3.5" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 888 | dependencies = [ 889 | "bitflags", 890 | ] 891 | 892 | [[package]] 893 | name = "regex" 894 | version = "1.8.1" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" 897 | dependencies = [ 898 | "aho-corasick", 899 | "memchr", 900 | "regex-syntax", 901 | ] 902 | 903 | [[package]] 904 | name = "regex-syntax" 905 | version = "0.7.1" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" 908 | 909 | [[package]] 910 | name = "reqwest" 911 | version = "0.11.17" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" 914 | dependencies = [ 915 | "base64", 916 | "bytes", 917 | "encoding_rs", 918 | "futures-core", 919 | "futures-util", 920 | "h2", 921 | "http", 922 | "http-body", 923 | "hyper", 924 | "hyper-tls", 925 | "ipnet", 926 | "js-sys", 927 | "log", 928 | "mime", 929 | "native-tls", 930 | "once_cell", 931 | "percent-encoding", 932 | "pin-project-lite", 933 | "serde", 934 | "serde_json", 935 | "serde_urlencoded", 936 | "tokio", 937 | "tokio-native-tls", 938 | "tower-service", 939 | "url", 940 | "wasm-bindgen", 941 | "wasm-bindgen-futures", 942 | "web-sys", 943 | "winreg", 944 | ] 945 | 946 | [[package]] 947 | name = "rustix" 948 | version = "0.37.19" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" 951 | dependencies = [ 952 | "bitflags", 953 | "errno", 954 | "io-lifetimes", 955 | "libc", 956 | "linux-raw-sys", 957 | "windows-sys 0.48.0", 958 | ] 959 | 960 | [[package]] 961 | name = "ryu" 962 | version = "1.0.13" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 965 | 966 | [[package]] 967 | name = "schannel" 968 | version = "0.1.21" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 971 | dependencies = [ 972 | "windows-sys 0.42.0", 973 | ] 974 | 975 | [[package]] 976 | name = "security-framework" 977 | version = "2.9.0" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" 980 | dependencies = [ 981 | "bitflags", 982 | "core-foundation", 983 | "core-foundation-sys", 984 | "libc", 985 | "security-framework-sys", 986 | ] 987 | 988 | [[package]] 989 | name = "security-framework-sys" 990 | version = "2.9.0" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" 993 | dependencies = [ 994 | "core-foundation-sys", 995 | "libc", 996 | ] 997 | 998 | [[package]] 999 | name = "semver" 1000 | version = "1.0.17" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" 1003 | 1004 | [[package]] 1005 | name = "serde" 1006 | version = "1.0.163" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" 1009 | dependencies = [ 1010 | "serde_derive", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "serde_derive" 1015 | version = "1.0.163" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" 1018 | dependencies = [ 1019 | "proc-macro2", 1020 | "quote", 1021 | "syn", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "serde_json" 1026 | version = "1.0.96" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" 1029 | dependencies = [ 1030 | "itoa", 1031 | "ryu", 1032 | "serde", 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "serde_spanned" 1037 | version = "0.6.1" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" 1040 | dependencies = [ 1041 | "serde", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "serde_urlencoded" 1046 | version = "0.7.1" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1049 | dependencies = [ 1050 | "form_urlencoded", 1051 | "itoa", 1052 | "ryu", 1053 | "serde", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "slab" 1058 | version = "0.4.8" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1061 | dependencies = [ 1062 | "autocfg", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "socket2" 1067 | version = "0.4.9" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1070 | dependencies = [ 1071 | "libc", 1072 | "winapi", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "stacker" 1077 | version = "0.1.15" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" 1080 | dependencies = [ 1081 | "cc", 1082 | "cfg-if", 1083 | "libc", 1084 | "psm", 1085 | "winapi", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "strsim" 1090 | version = "0.10.0" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1093 | 1094 | [[package]] 1095 | name = "syn" 1096 | version = "2.0.16" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" 1099 | dependencies = [ 1100 | "proc-macro2", 1101 | "quote", 1102 | "unicode-ident", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "tempfile" 1107 | version = "3.5.0" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" 1110 | dependencies = [ 1111 | "cfg-if", 1112 | "fastrand", 1113 | "redox_syscall", 1114 | "rustix", 1115 | "windows-sys 0.45.0", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "time" 1120 | version = "0.1.45" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 1123 | dependencies = [ 1124 | "libc", 1125 | "wasi 0.10.0+wasi-snapshot-preview1", 1126 | "winapi", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "tinyvec" 1131 | version = "1.6.0" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1134 | dependencies = [ 1135 | "tinyvec_macros", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "tinyvec_macros" 1140 | version = "0.1.1" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1143 | 1144 | [[package]] 1145 | name = "tokio" 1146 | version = "1.28.1" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" 1149 | dependencies = [ 1150 | "autocfg", 1151 | "bytes", 1152 | "libc", 1153 | "mio", 1154 | "num_cpus", 1155 | "pin-project-lite", 1156 | "socket2", 1157 | "tokio-macros", 1158 | "windows-sys 0.48.0", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "tokio-macros" 1163 | version = "2.1.0" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" 1166 | dependencies = [ 1167 | "proc-macro2", 1168 | "quote", 1169 | "syn", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "tokio-native-tls" 1174 | version = "0.3.1" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1177 | dependencies = [ 1178 | "native-tls", 1179 | "tokio", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "tokio-util" 1184 | version = "0.7.8" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" 1187 | dependencies = [ 1188 | "bytes", 1189 | "futures-core", 1190 | "futures-sink", 1191 | "pin-project-lite", 1192 | "tokio", 1193 | "tracing", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "toml" 1198 | version = "0.7.3" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" 1201 | dependencies = [ 1202 | "serde", 1203 | "serde_spanned", 1204 | "toml_datetime", 1205 | "toml_edit", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "toml_datetime" 1210 | version = "0.6.1" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" 1213 | dependencies = [ 1214 | "serde", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "toml_edit" 1219 | version = "0.19.8" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" 1222 | dependencies = [ 1223 | "indexmap", 1224 | "serde", 1225 | "serde_spanned", 1226 | "toml_datetime", 1227 | "winnow", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "tower-service" 1232 | version = "0.3.2" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1235 | 1236 | [[package]] 1237 | name = "tracing" 1238 | version = "0.1.37" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1241 | dependencies = [ 1242 | "cfg-if", 1243 | "pin-project-lite", 1244 | "tracing-core", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "tracing-core" 1249 | version = "0.1.31" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 1252 | dependencies = [ 1253 | "once_cell", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "try-lock" 1258 | version = "0.2.4" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1261 | 1262 | [[package]] 1263 | name = "unicode-bidi" 1264 | version = "0.3.13" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1267 | 1268 | [[package]] 1269 | name = "unicode-ident" 1270 | version = "1.0.8" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 1273 | 1274 | [[package]] 1275 | name = "unicode-normalization" 1276 | version = "0.1.22" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1279 | dependencies = [ 1280 | "tinyvec", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "unicode-width" 1285 | version = "0.1.10" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1288 | 1289 | [[package]] 1290 | name = "url" 1291 | version = "2.3.1" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1294 | dependencies = [ 1295 | "form_urlencoded", 1296 | "idna", 1297 | "percent-encoding", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "utf8parse" 1302 | version = "0.2.1" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1305 | 1306 | [[package]] 1307 | name = "vcpkg" 1308 | version = "0.2.15" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1311 | 1312 | [[package]] 1313 | name = "version_check" 1314 | version = "0.9.4" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1317 | 1318 | [[package]] 1319 | name = "want" 1320 | version = "0.3.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1323 | dependencies = [ 1324 | "log", 1325 | "try-lock", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "wasi" 1330 | version = "0.10.0+wasi-snapshot-preview1" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1333 | 1334 | [[package]] 1335 | name = "wasi" 1336 | version = "0.11.0+wasi-snapshot-preview1" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1339 | 1340 | [[package]] 1341 | name = "wasm-bindgen" 1342 | version = "0.2.85" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" 1345 | dependencies = [ 1346 | "cfg-if", 1347 | "wasm-bindgen-macro", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "wasm-bindgen-backend" 1352 | version = "0.2.85" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" 1355 | dependencies = [ 1356 | "bumpalo", 1357 | "log", 1358 | "once_cell", 1359 | "proc-macro2", 1360 | "quote", 1361 | "syn", 1362 | "wasm-bindgen-shared", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "wasm-bindgen-futures" 1367 | version = "0.4.35" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" 1370 | dependencies = [ 1371 | "cfg-if", 1372 | "js-sys", 1373 | "wasm-bindgen", 1374 | "web-sys", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "wasm-bindgen-macro" 1379 | version = "0.2.85" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" 1382 | dependencies = [ 1383 | "quote", 1384 | "wasm-bindgen-macro-support", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "wasm-bindgen-macro-support" 1389 | version = "0.2.85" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" 1392 | dependencies = [ 1393 | "proc-macro2", 1394 | "quote", 1395 | "syn", 1396 | "wasm-bindgen-backend", 1397 | "wasm-bindgen-shared", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "wasm-bindgen-shared" 1402 | version = "0.2.85" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" 1405 | 1406 | [[package]] 1407 | name = "web-sys" 1408 | version = "0.3.62" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" 1411 | dependencies = [ 1412 | "js-sys", 1413 | "wasm-bindgen", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "winapi" 1418 | version = "0.3.9" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1421 | dependencies = [ 1422 | "winapi-i686-pc-windows-gnu", 1423 | "winapi-x86_64-pc-windows-gnu", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "winapi-i686-pc-windows-gnu" 1428 | version = "0.4.0" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1431 | 1432 | [[package]] 1433 | name = "winapi-x86_64-pc-windows-gnu" 1434 | version = "0.4.0" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1437 | 1438 | [[package]] 1439 | name = "windows" 1440 | version = "0.48.0" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 1443 | dependencies = [ 1444 | "windows-targets 0.48.0", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "windows-sys" 1449 | version = "0.42.0" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1452 | dependencies = [ 1453 | "windows_aarch64_gnullvm 0.42.2", 1454 | "windows_aarch64_msvc 0.42.2", 1455 | "windows_i686_gnu 0.42.2", 1456 | "windows_i686_msvc 0.42.2", 1457 | "windows_x86_64_gnu 0.42.2", 1458 | "windows_x86_64_gnullvm 0.42.2", 1459 | "windows_x86_64_msvc 0.42.2", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "windows-sys" 1464 | version = "0.45.0" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1467 | dependencies = [ 1468 | "windows-targets 0.42.2", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "windows-sys" 1473 | version = "0.48.0" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1476 | dependencies = [ 1477 | "windows-targets 0.48.0", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "windows-targets" 1482 | version = "0.42.2" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1485 | dependencies = [ 1486 | "windows_aarch64_gnullvm 0.42.2", 1487 | "windows_aarch64_msvc 0.42.2", 1488 | "windows_i686_gnu 0.42.2", 1489 | "windows_i686_msvc 0.42.2", 1490 | "windows_x86_64_gnu 0.42.2", 1491 | "windows_x86_64_gnullvm 0.42.2", 1492 | "windows_x86_64_msvc 0.42.2", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "windows-targets" 1497 | version = "0.48.0" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1500 | dependencies = [ 1501 | "windows_aarch64_gnullvm 0.48.0", 1502 | "windows_aarch64_msvc 0.48.0", 1503 | "windows_i686_gnu 0.48.0", 1504 | "windows_i686_msvc 0.48.0", 1505 | "windows_x86_64_gnu 0.48.0", 1506 | "windows_x86_64_gnullvm 0.48.0", 1507 | "windows_x86_64_msvc 0.48.0", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "windows_aarch64_gnullvm" 1512 | version = "0.42.2" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1515 | 1516 | [[package]] 1517 | name = "windows_aarch64_gnullvm" 1518 | version = "0.48.0" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1521 | 1522 | [[package]] 1523 | name = "windows_aarch64_msvc" 1524 | version = "0.42.2" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1527 | 1528 | [[package]] 1529 | name = "windows_aarch64_msvc" 1530 | version = "0.48.0" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1533 | 1534 | [[package]] 1535 | name = "windows_i686_gnu" 1536 | version = "0.42.2" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1539 | 1540 | [[package]] 1541 | name = "windows_i686_gnu" 1542 | version = "0.48.0" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1545 | 1546 | [[package]] 1547 | name = "windows_i686_msvc" 1548 | version = "0.42.2" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1551 | 1552 | [[package]] 1553 | name = "windows_i686_msvc" 1554 | version = "0.48.0" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1557 | 1558 | [[package]] 1559 | name = "windows_x86_64_gnu" 1560 | version = "0.42.2" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1563 | 1564 | [[package]] 1565 | name = "windows_x86_64_gnu" 1566 | version = "0.48.0" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1569 | 1570 | [[package]] 1571 | name = "windows_x86_64_gnullvm" 1572 | version = "0.42.2" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1575 | 1576 | [[package]] 1577 | name = "windows_x86_64_gnullvm" 1578 | version = "0.48.0" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1581 | 1582 | [[package]] 1583 | name = "windows_x86_64_msvc" 1584 | version = "0.42.2" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1587 | 1588 | [[package]] 1589 | name = "windows_x86_64_msvc" 1590 | version = "0.48.0" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1593 | 1594 | [[package]] 1595 | name = "winnow" 1596 | version = "0.4.6" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" 1599 | dependencies = [ 1600 | "memchr", 1601 | ] 1602 | 1603 | [[package]] 1604 | name = "winreg" 1605 | version = "0.10.1" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1608 | dependencies = [ 1609 | "winapi", 1610 | ] 1611 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyscan" 3 | version = "0.1.7" 4 | edition = "2021" 5 | authors = ["Aswin S "] 6 | license = "MIT" 7 | description = "python dependency vulnerability scanner" 8 | readme = "README.md" 9 | homepage = "https://github.com/aswinnnn/pyscan" 10 | repository = "https://github.com/aswinnnn/pyscan" 11 | keywords = ["cli", "python", "security", "vulnerability", "pentesting"] 12 | categories = ["command-line-utilities"] 13 | 14 | 15 | [dependencies] 16 | chrono = "0.4.24" 17 | clap = {version="4.2.1", features=["derive"]} 18 | console = "0.15.5" 19 | lazy_static = "1.4.0" 20 | once_cell = "1.18.0" 21 | pep-508 = "0.3.0" 22 | regex = "1.7.3" 23 | reqwest = {version="0.11.16"} 24 | serde = {version="1.0.160", features=["derive", "serde_derive"]} 25 | serde_json = "1.0.96" 26 | toml = "0.7.3" 27 | lenient_semver = { version = "0.4.2", features = [ "version_semver"] } 28 | semver = "1.0.17" 29 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 30 | futures = "0.3.28" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aswin S 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🐍 Pyscan

2 | 3 | ![CI](https://github.com/aswinnnn/pyscan/actions/workflows/CI.yml/badge.svg) ![Liscense](https://img.shields.io/github/license/aswinnnn/pyscan?color=ff64b4) [![PyPI](https://img.shields.io/pypi/v/pyscan-rs?color=ff69b4)](https://pypi.org/project/pyscan-rs) [![](https://img.shields.io/crates/v/pyscan?color=ff64b4)](https://crates.io/crates/pyscan) [![GitHub issues](https://img.shields.io/github/issues/aswinnnn/pyscan.svg?color=ff69b4)](https://GitHub.com/aswinnnn/pyscan/issues/) [![Top Language](https://img.shields.io/github/languages/top/aswinnnn/pyscan?color=ff69b4)](https://img.shields.io/github/languages/top/aswinnnn/pyscan) 4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 |
A dependency vulnerability scanner for your python projects, straight from the terminal.
14 | 15 | + can be used within large projects. (see [benchmarks](BENCHMARKS.md)) 16 | + automatically finds dependencies either from configuration files or within source code. 17 | + support for poetry,hatch,filt,pdm and can be integrated into existing build processes. 18 | + hasn't been battle-hardened yet. PRs and issue makers welcome. 19 | 20 | ## 🕊️ Install 21 | 22 | ```bash 23 | pip install pyscan-rs 24 | ``` 25 | **look out for the "-rs"** part 26 | or 27 | 28 | ```bash 29 | cargo install pyscan 30 | ``` 31 | 32 | 33 | 34 | ## 🐇 Usage 35 | 36 | Go to your python source directory (or wherever you keep your `requirements.txt`/`pyproject.toml`) and run: 37 | 38 | ```bash 39 | > pyscan 40 | ``` 41 | or 42 | ```bash 43 | > pyscan -d path/to/src 44 | ``` 45 | 46 | 58 | 59 |
60 | Pyscan will find any dependencies added through poetry, hatch, filt, pdm, etc. 61 | Here's the order of precedence for a source/config file: 62 | 63 | + `requirements.txt` 64 | + `pyproject.toml` 65 | + your source code (`.py`) 66 | 67 | Pyscan will use your `pip` to find unknown versions, otherwise [pypi.org](https://pypi.org) for the latest version. Still, **it is recommended to version-ize your requirements** and use proper [pep-508 syntax](https://peps.python.org/pep-0508/). 68 | 69 | ## Building 70 | 71 | pyscan requires a rust version of `< v1.70`, and might be unstable on previous releases. 72 | There's an overview of the codebase at [architecture](./architecture/). Grateful for all the contributions so far. 73 | 74 | ## 🦀 Note 75 | 76 | pyscan doesn't make sure your code is safe from everything. Use all resources available to you like [safety](https://pypi.org/project/safety/) Dependabot, [`pip-audit`](https://pypi.org/project/pip-audit/), trivy and the likes. 77 | 78 | ## 🐰 Todo 79 | 80 | As of December 24, 2024: 81 | 82 | - [ ] Gather time to work on it (incredible task as a ~~high schooler~~ college freshman) 83 | - [ ] Persistent state representation of a project's security. 84 | - [ ] Graphical analysis of dependencies and their dependencies, and so on. 85 | - [ ] Better display, search, filter of vulns 86 | - [ ] Finish the "big" update (All of the above is a part of PR #17) 87 | 88 | ## 🐹 Donate 89 | 90 | While not coding, I am a broke ~~high school~~ college student with nothing else to do. I appreciate all the help I can get. 91 | 92 | 93 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Z8Z74DCR4) 94 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.1.x | :white_check_mark: | 8 | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Open an issue! 13 | -------------------------------------------------------------------------------- /architecture/README.md: -------------------------------------------------------------------------------- 1 | # 🐍 Architecture / Codebase Overview 2 | 3 |

4 | 5 |

6 | 7 |
A very vague representation of how an ideal pyscan run works with no arguments given.
8 | 9 |
10 | 11 | Pyscan is coded in a psuedo-procedural manner where the top level works just like any procedural program (the functions are "chained" in a way) but the internals use structs and models/classes to an extent enough to call it OOP. It's a mix of both worlds. 12 | 13 | ## Important files to look at 14 | 15 | There's comments on almost anything comment-able and worthy. Feel free to look around. 16 | 17 | - [`parser.rs`](../src/parser/mod.rs) - top level look at the parser. Check out [`extractor.rs`](../src/parser/extractor.rs) to really see the extraction and file discovery being done. 18 | 19 | - [`scanner::api.rs`](../src/scanner/api.rs) - how the API stuff gets done using the struct `Osv`, look at `mod.rs` for a higher level view. 20 | 21 | - [`docker.rs`](../src/docker/mod.rs) - handles getting and doing stuff with Docker. [this one is buggy and might get deprecated because i dont really care about docker, just run the program inside the container or something] 22 | 23 | - [`display.rs`](../src/display/mod.rs) - some functions used to print to the screen, not all though. 24 | 25 | ## Notes for contributers 26 | 27 | - This thing will be updated every once in a while to detail how pyscan works in a much more articulate and better way, including subcommands and other arguments and quirks. 28 | 29 | - If you think the codebase is designed badly, I don't know, it might be. I have never made a CLI tool before so, there's that. Open an issue or make a PR and I'm more than willing to learn from you. 30 | 31 | - Please be descriptive and detailed in your PRs, comments and other decent things. It's very cool what the open source community has done for pyscan so far. 32 | -------------------------------------------------------------------------------- /assets/2pyscan-repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaswin/pyscan/959b39c8d025e4802eee7a30fef7a408186b7f9f/assets/2pyscan-repository.png -------------------------------------------------------------------------------- /assets/flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaswin/pyscan/959b39c8d025e4802eee7a30fef7a408186b7f9f/assets/flowchart.png -------------------------------------------------------------------------------- /assets/pyscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaswin/pyscan/959b39c8d025e4802eee7a30fef7a408186b7f9f/assets/pyscan.png -------------------------------------------------------------------------------- /assets/snake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaswin/pyscan/959b39c8d025e4802eee7a30fef7a408186b7f9f/assets/snake.png -------------------------------------------------------------------------------- /osv_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Open Source Vulnerability", 3 | "description": "A schema for describing a vulnerability in an open source package.", 4 | "type": "object", 5 | "properties": { 6 | "schema_version": { 7 | "type": "string" 8 | }, 9 | "id": { 10 | "type": "string" 11 | }, 12 | "modified": { 13 | "type": "string", 14 | "format": "date-time" 15 | }, 16 | "published": { 17 | "type": "string", 18 | "format": "date-time" 19 | }, 20 | "withdrawn": { 21 | "type": "string", 22 | "format": "date-time" 23 | }, 24 | "aliases": { 25 | "type": ["array", "null"], 26 | "items": { 27 | "type": "string" 28 | } 29 | }, 30 | "related": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | } 35 | }, 36 | "summary": { 37 | "type": "string" 38 | }, 39 | "details": { 40 | "type": "string" 41 | }, 42 | "severity": { 43 | "type": ["array", "null"], 44 | "items": { 45 | "type": "object", 46 | "properties": { 47 | "type": { 48 | "type": "string", 49 | "enum": [ 50 | "CVSS_V2", 51 | "CVSS_V3" 52 | ] 53 | }, 54 | "score": { 55 | "type": "string" 56 | } 57 | }, 58 | "required": [ 59 | "type", 60 | "score" 61 | ] 62 | } 63 | }, 64 | "affected": { 65 | "type": ["array", "null"], 66 | "items": { 67 | "type": "object", 68 | "properties": { 69 | "package": { 70 | "type": "object", 71 | "properties": { 72 | "ecosystem": { 73 | "type": "string" 74 | }, 75 | "name": { 76 | "type": "string" 77 | }, 78 | "purl": { 79 | "type": "string" 80 | } 81 | }, 82 | "required": [ 83 | "ecosystem", 84 | "name" 85 | ] 86 | }, 87 | "severity": { 88 | "type": ["array", "null"], 89 | "items": { 90 | "type": "object", 91 | "properties": { 92 | "type": { 93 | "type": "string", 94 | "enum": [ 95 | "CVSS_V2", 96 | "CVSS_V3" 97 | ] 98 | }, 99 | "score": { 100 | "type": "string" 101 | } 102 | }, 103 | "required": [ 104 | "type", 105 | "score" 106 | ] 107 | } 108 | }, 109 | "ranges": { 110 | "type": "array", 111 | "items": { 112 | "type": "object", 113 | "properties": { 114 | "type": { 115 | "type": "string", 116 | "enum": [ 117 | "GIT", 118 | "SEMVER", 119 | "ECOSYSTEM" 120 | ] 121 | }, 122 | "repo": { 123 | "type": "string" 124 | }, 125 | "events": { 126 | "type": "array", 127 | "contains": { 128 | "required": [ 129 | "introduced" 130 | ] 131 | }, 132 | "items": { 133 | "type": "object", 134 | "oneOf": [ 135 | { 136 | "type": "object", 137 | "properties": { 138 | "introduced": { 139 | "type": "string" 140 | } 141 | }, 142 | "required": [ 143 | "introduced" 144 | ] 145 | }, 146 | { 147 | "type": "object", 148 | "properties": { 149 | "fixed": { 150 | "type": "string" 151 | } 152 | }, 153 | "required": [ 154 | "fixed" 155 | ] 156 | }, 157 | { 158 | "type": "object", 159 | "properties": { 160 | "last_affected": { 161 | "type": "string" 162 | } 163 | }, 164 | "required": [ 165 | "last_affected" 166 | ] 167 | }, 168 | { 169 | "type": "object", 170 | "properties": { 171 | "limit": { 172 | "type": "string" 173 | } 174 | }, 175 | "required": [ 176 | "limit" 177 | ] 178 | } 179 | ] 180 | }, 181 | "minItems": 1 182 | }, 183 | "database_specific": { 184 | "type": "object" 185 | } 186 | }, 187 | "allOf": [ 188 | { 189 | "if": { 190 | "properties": { 191 | "type": { 192 | "const": "GIT" 193 | } 194 | } 195 | }, 196 | "then": { 197 | "required": [ 198 | "repo" 199 | ] 200 | } 201 | }, 202 | { 203 | "if": { 204 | "properties": { 205 | "events": { 206 | "contains": { 207 | "required": ["last_affected"] 208 | } 209 | } 210 | } 211 | }, 212 | "then": { 213 | "not": { 214 | "properties": { 215 | "events": { 216 | "contains": { 217 | "required": ["fixed"] 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | ], 225 | "required": [ 226 | "type", 227 | "events" 228 | ] 229 | } 230 | }, 231 | "versions": { 232 | "type": "array", 233 | "items": { 234 | "type": "string" 235 | } 236 | }, 237 | "ecosystem_specific": { 238 | "type": "object" 239 | }, 240 | "database_specific": { 241 | "type": "object" 242 | } 243 | } 244 | } 245 | }, 246 | "references": { 247 | "type": ["array", "null"], 248 | "items": { 249 | "type": "object", 250 | "properties": { 251 | "type": { 252 | "type": "string", 253 | "enum": [ 254 | "ADVISORY", 255 | "ARTICLE", 256 | "DETECTION", 257 | "DISCUSSION", 258 | "REPORT", 259 | "FIX", 260 | "INTRODUCED", 261 | "GIT", 262 | "PACKAGE", 263 | "EVIDENCE", 264 | "WEB" 265 | ] 266 | }, 267 | "url": { 268 | "type": "string", 269 | "format": "uri" 270 | } 271 | }, 272 | "required": [ 273 | "type", 274 | "url" 275 | ] 276 | } 277 | }, 278 | "credits": { 279 | "type": "array", 280 | "items": { 281 | "type": "object", 282 | "properties": { 283 | "name": { 284 | "type": "string" 285 | }, 286 | "contact": { 287 | "type": "array", 288 | "items": { 289 | "type": "string" 290 | } 291 | }, 292 | "type": { 293 | "type": "string", 294 | "enum": [ 295 | "FINDER", 296 | "REPORTER", 297 | "ANALYST", 298 | "COORDINATOR", 299 | "REMEDIATION_DEVELOPER", 300 | "REMEDIATION_REVIEWER", 301 | "REMEDIATION_VERIFIER", 302 | "TOOL", 303 | "SPONSOR", 304 | "OTHER" 305 | ] 306 | } 307 | }, 308 | "required": [ 309 | "name" 310 | ] 311 | } 312 | }, 313 | "database_specific": { 314 | "type": "object" 315 | } 316 | }, 317 | "required": [ 318 | "id", 319 | "modified" 320 | ] 321 | } 322 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=0.15"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "pyscan-rs" 7 | requires-python = ">=3.7" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | 14 | 15 | [tool.maturin] 16 | bindings = "bin" 17 | -------------------------------------------------------------------------------- /python/pyscan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaswin/pyscan/959b39c8d025e4802eee7a30fef7a408186b7f9f/python/pyscan/__init__.py -------------------------------------------------------------------------------- /python/pyscan/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import sysconfig 4 | from pathlib import Path 5 | 6 | 7 | def find_pyscan_bin() -> Path: 8 | """Return the pyscan binary path.""" 9 | 10 | pyscan_exe = "pyscan" + sysconfig.get_config_var("EXE") 11 | 12 | path = Path(sysconfig.get_path("bin")) / pyscan_exe 13 | if path.is_file(): 14 | return path 15 | 16 | if sys.version_info >= (3, 10): 17 | user_scheme = sysconfig.get_preferred_scheme("user") 18 | elif os.name == "nt": 19 | user_scheme = "nt_user" 20 | elif sys.platform == "darwin" and sys._framework: 21 | user_scheme = "osx_framework_user" 22 | else: 23 | user_scheme = "posix_user" 24 | 25 | path = Path(sysconfig.get_path("bin", scheme=user_scheme)) / pyscan_exe 26 | if path.is_file(): 27 | return path 28 | 29 | raise FileNotFoundError(path) 30 | 31 | 32 | if __name__ == "__main__": 33 | pyscan = find_pyscan_bin() 34 | sys.exit(os.spawnv(os.P_WAIT, pyscan, ["pyscan", *sys.argv[1:]])) -------------------------------------------------------------------------------- /src/display/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::structs::ScannedDependency; 2 | use console::{style, Term}; 3 | use once_cell::sync::Lazy; 4 | use std::{collections::HashMap, io, process::exit}; 5 | 6 | static CONS: Lazy = Lazy::new(Term::stdout); 7 | 8 | pub struct Progress { 9 | // this progress info only contains progress info about the found vulns. 10 | pub count: usize, 11 | current_displayed: usize, 12 | } 13 | 14 | impl Progress { 15 | pub fn new() -> Progress { 16 | Progress { 17 | count: 0, 18 | current_displayed: 0, 19 | } 20 | } 21 | pub fn display(&mut self) { 22 | if self.count > 1 { 23 | let _ = CONS.clear_last_lines(1); 24 | } 25 | 26 | if self.count > self.current_displayed { 27 | let _ = CONS.write_line( 28 | format!( 29 | "Found {} vulnerabilities so far", 30 | style(self.count).bold().bright().red() 31 | ) 32 | .as_str(), 33 | ); 34 | self.current_displayed = self.count; 35 | } 36 | } 37 | 38 | pub fn count_one(&mut self) { 39 | self.count += 1; 40 | } 41 | pub fn end(&mut self) { 42 | let _ = CONS.clear_last_lines(1); 43 | } 44 | } 45 | 46 | pub fn display_queried( 47 | collected: &Vec, 48 | imports_info: &mut HashMap, 49 | ) { 50 | // --- displaying query result starts here --- 51 | for dep in collected { 52 | let _ = CONS.write_line( 53 | format!( 54 | "|-| {} [{}]{:^5}", 55 | style(dep.name.as_str()).bold().bright().yellow(), 56 | style(dep.version.as_str()).bold().dim(), 57 | style(" -> Found vulnerabilities!").bold().bright().red() 58 | ) 59 | .as_str(), 60 | ); 61 | } // displays all the deps where vuln has been found 62 | 63 | // remove the the deps with vulns from import_info so what remains is the safe deps, which we can display as safe 64 | for d in collected.iter() { 65 | imports_info.remove(d.name.as_str()); 66 | } 67 | 68 | for (k, v) in imports_info.iter() { 69 | let _ = CONS.write_line( 70 | format!( 71 | "|-| {} [{}]{}", 72 | style(k.as_str()).bold().bright().yellow(), 73 | style(v.as_str()).bold().dim(), 74 | style(" -> No vulnerabilities found.") 75 | .bold() 76 | .bright() 77 | .green() 78 | ) 79 | .as_str(), 80 | ); 81 | } // display the safe deps 82 | let _ = display_summary(&collected); 83 | } 84 | 85 | pub fn display_summary(collected: &Vec) -> io::Result<()> { 86 | // thing is, collected only has vulnerable dependencies, if theres a case where no vulns have been found, it will just skip this entire thing. 87 | if !collected.is_empty() { 88 | // --- summary starts here --- 89 | CONS.write_line(&format!( 90 | "{}", 91 | style("SUMMARY").bold().yellow().underlined() 92 | ))?; 93 | for v in collected { 94 | for vuln in &v.vuln.vulns { 95 | // DEPENDENCY 96 | let name = format!( 97 | "Dependency: {}", 98 | style(v.name.clone()).bold().bright().red() 99 | ); 100 | 101 | CONS.write_line(name.as_str())?; 102 | CONS.flush()?; 103 | 104 | // ID 105 | let id = format!("ID: {}", style(vuln.id.as_str()).bold().bright().yellow()); 106 | CONS.write_line(id.as_str())?; 107 | CONS.flush()?; 108 | 109 | // DETAILS 110 | let details = format!("Details: {}", style(vuln.details.as_str()).italic()); 111 | CONS.write_line(details.as_str())?; 112 | CONS.flush()?; 113 | 114 | // VERSIONS AFFECTED from ... to 115 | let vers: Vec> = vuln 116 | .affected 117 | .iter() 118 | .map(|affected| { 119 | vec![ 120 | { 121 | if let Some(v) = &affected.versions { 122 | v.first().unwrap().to_string() 123 | } else { 124 | "This version".to_string() 125 | } 126 | }, 127 | { 128 | if let Some(v) = &affected.versions { 129 | v.last().unwrap().to_string() 130 | } else { 131 | "Unknown".to_string() 132 | } 133 | }, 134 | ] 135 | }) 136 | .collect(); 137 | // let vers: Vec> = vuln.affected.iter().map(|affected| {vec![affected.versions.first().unwrap().to_string(), affected.versions.last().unwrap().to_string()]}).collect(); 138 | 139 | let version = format!( 140 | "Versions affected: {} to {}", 141 | style( 142 | vers.first() 143 | .expect("No version found affected") 144 | .first() 145 | .unwrap() 146 | ) 147 | .dim() 148 | .underlined(), 149 | style( 150 | vers.last() 151 | .expect("No version found affected") 152 | .last() 153 | .unwrap() 154 | ) 155 | .dim() 156 | .underlined() 157 | ); 158 | 159 | println!(); 160 | 161 | CONS.write_line(version.as_str())?; 162 | CONS.flush()?; 163 | } 164 | } 165 | } else { 166 | println!("Finished scanning all found dependencies."); 167 | exit(0) 168 | } 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /src/docker/mod.rs: -------------------------------------------------------------------------------- 1 | // Import the std::process module to use Command 2 | use std::{path::{PathBuf, Path}, process::Command}; 3 | 4 | use crate::parser::scan_dir; 5 | 6 | // Define a custom error type that wraps a String message 7 | #[derive(Debug)] 8 | pub struct DockerError(String); 9 | 10 | // Implement the std::error::Error trait for DockerError 11 | impl std::error::Error for DockerError {} 12 | 13 | // Implement the std::fmt::Display trait for DockerError 14 | impl std::fmt::Display for DockerError { 15 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 16 | write!(f, "Docker error: {}", self.0) 17 | } 18 | } 19 | 20 | // Define a function that takes a docker image name as a parameter 21 | // and returns a result of either a vector of filenames or a DockerError 22 | pub async fn list_files_in_docker_image(image: &str, path: PathBuf) -> Result<(), DockerError> { 23 | // Create a Command object to run docker commands 24 | let mut cmd = Command::new("docker"); 25 | 26 | // Use the "create" subcommand to create a container from the image 27 | // without starting it 28 | cmd.arg("create").arg(image); 29 | 30 | // Execute the command and get the output 31 | let output = cmd.output().map_err(|e| DockerError(e.to_string()))?; 32 | 33 | // Check if the command was successful 34 | if !output.status.success() { 35 | // Return an error with the command's stderr 36 | return Err(DockerError( 37 | String::from_utf8_lossy(&output.stderr).to_string(), 38 | )); 39 | } 40 | 41 | // Get the container ID from the output 42 | let container_id = String::from_utf8(output.stdout) 43 | .map_err(|e| DockerError(e.to_string()))? 44 | .trim() 45 | .to_string(); 46 | 47 | // Create another Command object to run docker commands 48 | let mut cmd = Command::new("docker"); 49 | let cmd = cmd.current_dir("."); 50 | 51 | // Create a tmp folder to keep our docker-files 52 | create_tmp_folder(".") 53 | .expect("Could not create a temporary folder for the docker files. Try creating it yourself:\n./tmp/docker-files\n"); 54 | 55 | // Use the "cp" subcommand to copy all files from the container 56 | // to a temporary directory on the host 57 | cmd.arg("cp") 58 | .arg(format!( 59 | "{}:/{}", 60 | container_id, 61 | path.to_str().expect("Path contains non-unicode characters") 62 | )) 63 | .arg("./tmp/docker-files"); 64 | 65 | // Execute the command and get the output 66 | let output = cmd.output().map_err(|e| DockerError(e.to_string()))?; 67 | 68 | // Check if the command was successful 69 | if !output.status.success() { 70 | // Return an error with the command's stderr 71 | return Err(DockerError( 72 | String::from_utf8_lossy(&output.stderr).to_string(), 73 | )); 74 | } 75 | 76 | scan_dir(Path::new("./tmp/docker-files")).await; 77 | cleanup().map_err(|e| DockerError(e.to_string()) )?; 78 | 79 | // docker stop 80 | let mut cmd = Command::new("docker"); 81 | cmd.arg("stop") 82 | .arg(container_id.clone()); 83 | 84 | // Execute the command and get the output 85 | let _output = cmd.output().map_err(|e| DockerError(e.to_string()))?; 86 | 87 | // docker remove 88 | let mut cmd = Command::new("docker"); 89 | cmd.arg("rm") 90 | .arg(container_id); 91 | 92 | // Execute the command and get the output 93 | let _output = cmd.output().map_err(|e| DockerError(e.to_string()))?; 94 | Ok(()) 95 | 96 | 97 | // // Create another Command object to run shell commands 98 | // let mut cmd = Command::new("sh"); 99 | 100 | // // Use the "-c" argument to run a shell command that lists all files 101 | // // in the temporary directory and removes the directory prefix 102 | // cmd.arg("-c") 103 | // .arg(format!("cd ./tmp/docker-files/{} && ls -F", path.to_str().unwrap())); 104 | 105 | // // Execute the command and get the output 106 | // let output = cmd.output().map_err(|e| DockerError(e.to_string()))?; 107 | 108 | // // Check if the command was successful 109 | // if !output.status.success() { 110 | // // Return an error with the command's stderr 111 | // return Err(DockerError( 112 | // String::from_utf8_lossy(&output.stderr).to_string(), 113 | // )); 114 | // } 115 | 116 | // // Get the filenames from the output as a vector of strings 117 | // let filenames = String::from_utf8(output.stdout) 118 | // .map_err(|e| DockerError(e.to_string()))? 119 | // .lines() 120 | // .map(|s| s.to_string()) 121 | // .collect(); 122 | 123 | // // Return the filenames vector as Ok value 124 | // Ok(filenames) 125 | } 126 | 127 | fn create_tmp_folder(path: &str) -> std::io::Result<()> { 128 | let tmp_path = format!("{}/tmp/docker-files", path); 129 | std::fs::create_dir_all(tmp_path)?; 130 | Ok(()) 131 | } 132 | 133 | fn cleanup() -> Result<(), std::io::Error> { 134 | std::fs::remove_dir_all("./tmp/docker-files") 135 | } 136 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use console::style; 3 | use once_cell::sync::Lazy; 4 | use std::sync::OnceLock; 5 | use std::{path::PathBuf, process::exit}; 6 | use utils::{PipCache, SysInfo}; 7 | mod display; 8 | mod docker; 9 | mod parser; 10 | mod scanner; 11 | mod utils; 12 | use crate::{ 13 | parser::structs::{Dependency, VersionStatus}, 14 | utils::get_version, 15 | }; 16 | use std::env; 17 | use tokio::task; 18 | 19 | #[derive(Parser, Debug)] 20 | #[command( 21 | author = "aswinnnn", 22 | version = "0.1.7", 23 | about = "python dependency vulnerability scanner.\n\ndo 'pyscan [subcommand] --help' for specific help." 24 | )] 25 | struct Cli { 26 | /// path to source. (default: current directory) 27 | #[arg(long,short,default_value=None,value_name="DIRECTORY")] 28 | dir: Option, 29 | 30 | /// export the result to a desired format. [json] 31 | #[arg(long, short, required = false, value_name = "FILENAME")] 32 | output: Option, 33 | 34 | /// search for a single package. 35 | #[command(subcommand)] 36 | subcommand: Option, 37 | 38 | /// skip: skip the given databases 39 | /// ex. pyscan -s osv snyk 40 | /// hidden due to only having one database for now. 41 | #[arg( 42 | short, 43 | long, 44 | value_delimiter = ' ', 45 | value_name = "VAL1 VAL2 VAL3...", 46 | hide = true 47 | )] 48 | skip: Vec, 49 | 50 | /// show the version and information about a package from all available sources. (does not search for vulns, use 'package' subcommand for that). 51 | /// usage: pyscan show requests pyscan-rs lxml koda 52 | /// hidden due to unfinished 53 | #[arg( 54 | long, 55 | value_delimiter = ' ', 56 | value_name = "package1 package2 package3...", 57 | hide = true 58 | )] 59 | show: Vec, 60 | 61 | /// Uses pip to retrieve versions. if not provided it will use the source, falling back on pip if not, pypi.org. 62 | #[arg(long, required=false, action=clap::ArgAction::SetTrue)] 63 | pip: bool, 64 | 65 | /// Same as --pip except uses pypi.org to retrieve the latest version for the packages. 66 | #[arg(long, required=false,action=clap::ArgAction::SetTrue)] 67 | pypi: bool, 68 | 69 | /// turns off the caching of pip packages at the starting of execution. 70 | #[arg(long="cache-off", required=false,action=clap::ArgAction::SetTrue)] 71 | cache_off: bool, 72 | } 73 | 74 | #[derive(Subcommand, Debug, Clone)] 75 | enum SubCommand { 76 | /// query for a single python package 77 | Package { 78 | /// name of the package 79 | #[arg(long, short)] 80 | name: String, 81 | 82 | /// version of the package (defaults to latest if not provided) 83 | #[arg(long, short, default_value=None)] 84 | version: Option, 85 | }, 86 | 87 | /// scan inside a docker image 88 | Docker { 89 | /// name of the docker image 90 | #[arg(long, short)] 91 | name: String, 92 | 93 | /// path inside your docker container where requirements.txt is, or just the folder name where your Dockerfile (along with requirements.txt) is. 94 | #[arg(long, short, value_name = "DIRECTORY")] 95 | path: PathBuf, 96 | }, 97 | } 98 | 99 | static ARGS: Lazy> = Lazy::new(|| OnceLock::from(Cli::parse())); 100 | 101 | // Why is the args a static variable? Some arguments need to be seen by other files in the codebase 102 | // such as --pip or --pypi due to different use cases. Args only get wrote to once so it shouldn't pose a problem (Reason its OnceLock'ed). 103 | // Why is it Lazy? Something about a non-const fn in a const world. Pretty surprised to see the compiler recommend an outside crate for this issue. 104 | 105 | static PIPCACHE: Lazy = Lazy::new(|| utils::PipCache::init()); 106 | // is a hashmap of (package name, version) from 'pip list' 107 | // because calling 'pip show' everytime might get expensive if theres a lot of dependencies to check. 108 | 109 | #[tokio::main] 110 | async fn main() { 111 | match &ARGS.get().unwrap().subcommand { 112 | // subcommand package 113 | Some(SubCommand::Package { name, version }) => { 114 | // let osv = Osv::new().expect("Cannot access the API to get the latest package version."); 115 | let version = if let Some(v) = version { 116 | v.to_string() 117 | } else { 118 | utils::get_package_version_pypi(name.as_str()) 119 | .await 120 | .expect("Error in retrieving stable version from API") 121 | .to_string() 122 | }; 123 | 124 | let dep = Dependency { 125 | name: name.to_string(), 126 | version: Some(version), 127 | comparator: None, 128 | version_status: VersionStatus { 129 | pypi: false, 130 | pip: false, 131 | source: false, 132 | }, 133 | }; 134 | 135 | // start() from scanner only accepts Vec so 136 | let vdep = vec![dep]; 137 | 138 | let _res = scanner::start(vdep).await; 139 | exit(0) 140 | } 141 | Some(SubCommand::Docker { name, path }) => { 142 | println!( 143 | "{} {}\n{} {}", 144 | style("Docker image:").yellow().blink(), 145 | style(name.clone()).bold().green(), 146 | style("Path inside container:").yellow().blink(), 147 | style(path.to_string_lossy()).bold().green() 148 | ); 149 | println!("{}", 150 | style("--- Make sure you run the command with elevated permissions (sudo/administrator) as pyscan might have trouble accessing files inside docker containers ---").dim()); 151 | docker::list_files_in_docker_image(name, path.to_path_buf()) 152 | .await 153 | .expect("Error in scanning files from Docker image."); 154 | exit(0) 155 | } 156 | None => (), 157 | } 158 | 159 | println!( 160 | "pyscan v{} | by Aswin S (github.com/aswinnnn) | \x1b[90mConsider donating to a broke college student: https://ko-fi.com/aswinnnn \x1b[0m", 161 | get_version() 162 | ); 163 | 164 | let sys_info = SysInfo::new().await; 165 | // supposed to be a global static, cant atm because async closures are unstable. 166 | // has to be ran in diff thread due to underlying blocking functions, to be fixed soon. 167 | 168 | task::spawn(async move { 169 | // init pip cache, if cache-off is false or pip has been found 170 | if !&ARGS.get().unwrap().cache_off | sys_info.pip_found { 171 | let _ = PIPCACHE.lookup(" "); 172 | // since its in Lazy its first accesss would init the cache, the result is ignorable. 173 | } 174 | // has to be run on another thread to not block user functionality 175 | // it still blocks because i cant make pip_list() async or PIPCACHE would fail 176 | // as async closures aren't stable yet. 177 | // but it removes a 3s delay, for now. 178 | }); 179 | 180 | // --- giving control to parser starts here --- 181 | 182 | // if a directory path is provided 183 | if let Some(dir) = &ARGS.get().unwrap().dir { 184 | parser::scan_dir(dir.as_path()).await 185 | } 186 | // if not, use cwd 187 | else if let Ok(dir) = env::current_dir() { 188 | parser::scan_dir(dir.as_path()).await 189 | } else { 190 | eprintln!("the given directory is empty."); 191 | exit(1) 192 | }; // err when dir is empty 193 | } 194 | -------------------------------------------------------------------------------- /src/parser/extractor.rs: -------------------------------------------------------------------------------- 1 | use std::process::exit; 2 | 3 | /// for the parser module, extractor.rs is the backbone of all parsing 4 | /// it takes a String and a mutable reference to a Vec. 5 | /// String is the contents of a source file, while the mut ref vector will 6 | /// be used to collect the dependencies that we have extracted from the contents. 7 | use super::structs::{Dependency, VersionStatus}; 8 | 9 | use lazy_static::lazy_static; 10 | use pep_508::{self, Spec}; 11 | use regex::Regex; 12 | 13 | use toml::{de::Error, Table, Value}; 14 | 15 | pub fn extract_imports_python(text: String, imp: &mut Vec) { 16 | lazy_static! { 17 | static ref IMPORT_REGEX: Regex = 18 | Regex::new(r"^\s*(?:from|import)\s+(\w+(?:\s*,\s*\w+)*)").unwrap(); 19 | } 20 | 21 | for x in IMPORT_REGEX.find_iter(&text) { 22 | let mat = x.as_str().to_string(); 23 | let mat = mat.replacen("import", "", 1).trim().to_string(); 24 | 25 | imp.push(Dependency { 26 | name: mat, 27 | version: None, 28 | comparator: None, 29 | version_status: VersionStatus { 30 | pypi: false, 31 | pip: false, 32 | source: false, 33 | }, 34 | }) 35 | } 36 | } 37 | 38 | pub fn extract_imports_reqs(text: String, imp: &mut Vec) { 39 | // requirements.txt uses a PEP 508 parser to parse dependencies accordingly 40 | // you might think its just a text file, but I'm gonna decline reinventing the wheel 41 | // just to parse "requests >= 2.0.8" 42 | 43 | let parsed = pep_508::parse(text.as_str()); 44 | 45 | if let Ok(ref dep) = parsed { 46 | let dname = dep.name.to_string(); 47 | // println!("{:?}", parsed.clone()); 48 | if let Some(ver) = &dep.spec { 49 | if let Spec::Version(verspec) = ver { 50 | if let Some(v) = verspec.iter().next() { 51 | // pyscan only takes the first version spec found for the dependency 52 | let version = v.version.to_string(); 53 | let comparator = v.comparator; 54 | imp.push(Dependency { 55 | name: dname, 56 | version: Some(version), 57 | comparator: Some(comparator), 58 | version_status: VersionStatus { 59 | pypi: false, 60 | pip: false, 61 | source: true, 62 | }, 63 | }); 64 | } 65 | } 66 | } else { 67 | imp.push(Dependency { 68 | name: dname, 69 | version: None, 70 | comparator: None, 71 | version_status: VersionStatus { 72 | pypi: false, 73 | pip: false, 74 | source: false, 75 | }, 76 | }); 77 | } 78 | } else if let Err(e) = parsed { 79 | println!("{:#?}", e); 80 | } 81 | } 82 | 83 | // pub fn extract_imports_pyproject(f: String, imp: &mut Vec) { 84 | // let parsed = f.parse::(); 85 | // if let Ok(parsed) = parsed { 86 | // let project = &parsed["project"]; 87 | // let deps = &project["dependencies"]; 88 | // let deps = deps 89 | // .as_array() 90 | // .expect("Could not find the dependencies table in your pyproject.toml"); 91 | // for d in deps { 92 | // let d = d.as_str().unwrap(); 93 | // let parsed = pep_508::parse(d); 94 | // if let Ok(dep) = parsed { 95 | // let dname = dep.name.to_string(); 96 | // // println!("{:?}", dep.clone()); 97 | // if let Some(ver) = dep.spec { 98 | // if let Spec::Version(verspec) = ver { 99 | // for v in verspec { 100 | // // pyscan only takes the first version spec found for the dependency 101 | // // for now. 102 | // let version = v.version.to_string(); 103 | // let comparator = v.comparator; 104 | // imp.push(Dependency { 105 | // name: dname, 106 | // version: Some(version), 107 | // comparator: Some(comparator), 108 | // version_status: VersionStatus { 109 | // pypi: false, 110 | // pip: false, 111 | // source: true, 112 | // }, 113 | // }); 114 | // break; 115 | // } 116 | // } 117 | // } else { 118 | // imp.push(Dependency { 119 | // name: dname, 120 | // version: None, 121 | // comparator: None, 122 | // version_status: VersionStatus { 123 | // pypi: false, 124 | // pip: false, 125 | // source: false, 126 | // }, 127 | // }); 128 | // } 129 | // } 130 | // } 131 | // } 132 | // } 133 | 134 | pub fn extract_imports_setup_py(setup_py_content: &str, imp: &mut Vec) { 135 | let mut deps = Vec::new(); 136 | 137 | // regex for install_requires section 138 | let re = Regex::new(r"install_requires\s*=\s*\[([^\]]+)\]").expect("Invalid regex pattern"); 139 | 140 | for cap in re.captures_iter(setup_py_content) { 141 | if let Some(matched) = cap.get(1) { 142 | // Split the matched text by ',' and trim whitespace 143 | deps.extend( 144 | matched 145 | .as_str() 146 | .split(',') 147 | .map(|dep| dep.trim().replace("\"", "").replace("\\", "").to_string()), 148 | ); 149 | } 150 | } 151 | 152 | for d in deps { 153 | let d = d.as_str(); 154 | let parsed = pep_508::parse(d); 155 | if let Ok(dep) = parsed { 156 | let dname = dep.name.to_string(); 157 | if let Some(ver) = dep.spec { 158 | if let Spec::Version(verspec) = ver { 159 | if let Some(v) = verspec.first() { 160 | // pyscan only takes the first version spec found for the dependency 161 | // for now. 162 | let version = v.version.to_string(); 163 | let comparator = v.comparator; 164 | imp.push(Dependency { 165 | name: dname, 166 | version: Some(version), 167 | comparator: Some(comparator), 168 | version_status: VersionStatus { 169 | pypi: false, 170 | pip: false, 171 | source: true, 172 | }, 173 | }); 174 | } 175 | } 176 | } else { 177 | imp.push(Dependency { 178 | name: dname, 179 | version: None, 180 | comparator: None, 181 | version_status: VersionStatus { 182 | pypi: false, 183 | pip: false, 184 | source: false, 185 | }, 186 | }); 187 | } 188 | } 189 | } 190 | } 191 | 192 | pub fn extract_imports_pyproject( 193 | toml_content: String, 194 | imp: &mut Vec, 195 | ) -> Result<(), Error> { 196 | // Parse the toml content into a Value 197 | let toml_value: Value = toml::from_str(toml_content.as_str())?; 198 | // println!("{:#?}",toml_value); 199 | 200 | // Helper function to extract dependency values (version strings) including nested tables 201 | fn extract_dependencies( 202 | table: &toml::value::Table, 203 | poetry: Option, 204 | ) -> Result, Error> { 205 | let mut deps = Vec::new(); 206 | 207 | // for [project] in pyproject.toml, the insides require a different sort of parsing 208 | // for poetry you need both keys and values (as dependency name and version), 209 | // for [project] the values are just enough and the keys are in the vec below 210 | let projectlevel: Vec<&str> = vec![ 211 | "dependencies", 212 | "optional-dependencies.docs", 213 | "optional-dependencies", 214 | ]; 215 | 216 | for (key, version) in table { 217 | if projectlevel.contains(&key.as_str()) { 218 | match version { 219 | Value::String(version_str) => { 220 | deps.push(version_str.to_string()); 221 | } 222 | Value::Table(nested_table) => { 223 | if "optional-dependencies" == key { 224 | parse_opt_deps_pyproject(nested_table.clone(), &mut deps); 225 | } else { 226 | // Recursively extract dependencies from nested tables 227 | let nested_deps = extract_dependencies(nested_table, None)?; 228 | deps.extend(nested_deps); 229 | } 230 | } 231 | Value::Array(array) => { 232 | // Extract dependencies from an array (if any) 233 | for item in array { 234 | if let Value::String(item_str) = item { 235 | deps.push(item_str.to_string()); 236 | } 237 | } 238 | } 239 | _ => eprintln!("ERR: Invalid dependency syntax found while TOML parsing"), 240 | } 241 | } else if poetry.unwrap_or(false) { 242 | match version { 243 | Value::String(version_str) => { 244 | let verstr = version_str.to_string(); 245 | if verstr.contains('^') { 246 | let s = format!("{} >= {}", key, verstr.strip_prefix('^').unwrap()); 247 | deps.push(s); 248 | } else if verstr == "*" { 249 | deps.push(key.to_string()); 250 | } 251 | } 252 | Value::Table(nested_table) => { 253 | // Recursively extract dependencies from nested tables 254 | let nested_deps = extract_dependencies(nested_table, None)?; 255 | deps.extend(nested_deps); 256 | } 257 | Value::Array(array) => { 258 | // Extract dependencies from an array (if any) 259 | for item in array { 260 | if let Value::String(item_str) = item { 261 | deps.push(item_str.to_string()); 262 | } 263 | } 264 | } 265 | _ => eprintln!("ERR: Invalid dependency syntax found while TOML parsing"), 266 | } 267 | } 268 | } 269 | Ok(deps) 270 | } 271 | 272 | // Extract dependencies from different sections 273 | let mut all_dependencies = Vec::new(); 274 | 275 | // Look for keys like "dependencies" and "optional-dependencies" 276 | let keys_to_check = vec!["project", "optional-dependencies", "tool"]; 277 | 278 | for key in keys_to_check { 279 | if key.contains("tool") { 280 | if let Some(dependencies_table) = toml_value.get("tool") { 281 | if let Some(dependencies_table) = dependencies_table.get("poetry") { 282 | let poetrylevel: Vec<&str> = vec!["dependencies", "dev-dependencies"]; 283 | for k in poetrylevel.into_iter() { 284 | if let Some(dep) = dependencies_table.get(k) { 285 | match dep { 286 | Value::Table(table) => { 287 | all_dependencies 288 | .extend(extract_dependencies(table, Some(true))?); 289 | } 290 | // its definitely gonna be a table anyway, so... 291 | Value::String(_) => todo!(), 292 | Value::Integer(_) => todo!(), 293 | Value::Float(_) => todo!(), 294 | Value::Boolean(_) => todo!(), 295 | Value::Datetime(_) => todo!(), 296 | Value::Array(_) => todo!(), 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | // if its not poetry, check for [project] dependencies 304 | else if !key.contains("poetry") { 305 | if let Some(dependencies_table) = toml_value.get(key) { 306 | if let Some(dependencies) = dependencies_table.as_table() { 307 | all_dependencies.extend(extract_dependencies(dependencies, None)?); 308 | } 309 | } 310 | } else { 311 | eprintln!( 312 | "The pyproject.toml seen here is unlike of a python project. Please check and make 313 | sure you are in the right directory, or check the toml file." 314 | ); 315 | exit(1) 316 | } 317 | } 318 | // the toml might contain repeated dependencies 319 | // for different tools, dev tests, etc. 320 | all_dependencies.dedup(); 321 | 322 | for d in all_dependencies { 323 | let d = d.as_str(); 324 | let parsed = pep_508::parse(d); 325 | if let Ok(dep) = parsed { 326 | let dname = dep.name.to_string(); 327 | if let Some(ver) = dep.spec { 328 | if let Spec::Version(verspec) = ver { 329 | if let Some(v) = verspec.into_iter().next() { 330 | let version = v.version.to_string(); 331 | let comparator = v.comparator; 332 | imp.push(Dependency { 333 | name: dname.clone(), 334 | version: Some(version), 335 | comparator: Some(comparator), 336 | version_status: VersionStatus { 337 | pypi: false, 338 | pip: false, 339 | source: true, 340 | }, 341 | }); 342 | } 343 | } 344 | } else { 345 | imp.push(Dependency { 346 | name: dname.clone(), 347 | version: None, 348 | comparator: None, 349 | version_status: VersionStatus { 350 | pypi: false, 351 | pip: false, 352 | source: false, 353 | }, 354 | }); 355 | } 356 | } 357 | } 358 | Ok(()) 359 | } 360 | 361 | pub fn parse_opt_deps_pyproject(table: Table, deps: &mut Vec) { 362 | for v in table.values() { 363 | match v { 364 | Value::Array(a) => { 365 | for d in a { 366 | match d { 367 | Value::String(dependency) => { 368 | deps.push(dependency.to_owned()); 369 | } 370 | Value::Integer(_) => todo!(), 371 | Value::Float(_) => todo!(), 372 | Value::Boolean(_) => todo!(), 373 | Value::Datetime(datetime) => todo!(), 374 | Value::Array(vec) => todo!(), 375 | Value::Table(map) => todo!(), 376 | } 377 | } 378 | } 379 | Value::String(_) => todo!(), 380 | Value::Integer(_) => todo!(), 381 | Value::Float(_) => todo!(), 382 | Value::Boolean(_) => todo!(), 383 | Value::Datetime(datetime) => todo!(), 384 | Value::Table(map) => todo!(), 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{BufRead, BufReader}; 3 | use std::path::Path; 4 | use std::process::exit; 5 | use std::{ffi::OsString, fs::File}; 6 | mod extractor; 7 | pub mod structs; 8 | use super::scanner; 9 | use structs::{FileTypes, FoundFile, FoundFileResult}; 10 | 11 | pub async fn scan_dir(dir: &Path) { 12 | let mut result = FoundFileResult::new(); // contains found files 13 | 14 | if let Ok(entries) = fs::read_dir(dir) { 15 | for entry in entries.flatten() { 16 | let filename = entry.file_name(); 17 | let filext = if let Some(ext) = Path::new(&filename).extension() { 18 | ext.to_os_string() 19 | } else {"none".into()}; 20 | 21 | 22 | // setup.py check comes first otherwise it might cause issues with .py checker 23 | if *"setup.py" == filename.clone() { 24 | result.add(FoundFile { 25 | name: filename, 26 | filetype: FileTypes::SetupPy, 27 | path: OsString::from(entry.path()), 28 | }); 29 | result.setuppy(); 30 | } 31 | // check if .py 32 | // checking file extension straight up from filename caused some bugs. 33 | else if ".py" == filext { 34 | result.add(FoundFile { 35 | name: filename, 36 | filetype: FileTypes::Python, 37 | path: OsString::from(entry.path()), 38 | }); 39 | result.python(); // internal count of the file found 40 | } 41 | // requirements.txt 42 | else if *"requirements.txt" == filename.clone() { 43 | result.add(FoundFile { 44 | name: filename, 45 | filetype: FileTypes::Requirements, 46 | path: OsString::from(entry.path()), 47 | }); 48 | result.reqs(); 49 | } 50 | // constraints.txt 51 | else if *"constraints.txt" == filename.clone() { 52 | result.add(FoundFile { 53 | name: filename, 54 | filetype: FileTypes::Constraints, 55 | path: OsString::from(entry.path()), 56 | }); 57 | result.constraints(); 58 | } 59 | // pyproject.toml 60 | else if *"pyproject.toml" == filename.clone() { 61 | result.add(FoundFile { 62 | name: filename, 63 | filetype: FileTypes::Pyproject, 64 | path: OsString::from(entry.path()), 65 | }); 66 | result.pyproject(); 67 | } 68 | } 69 | } 70 | // println!("{:?}", result.clone()); 71 | 72 | // --- find_import takes the result --- 73 | 74 | find_import(result).await 75 | } 76 | 77 | /// A nice abstraction over different ways to find imports for different filetypes. 78 | async fn find_import(res: FoundFileResult) { 79 | let files = res.files; 80 | if res.reqs_found > res.pyproject_found { 81 | // if theres a requirements.txt and pyproject.toml isnt there 82 | find_reqs_imports(&files).await 83 | } else if res.reqs_found != 0 { 84 | // if both reqs and pyproject is present, go for reqs first 85 | find_reqs_imports(&files).await 86 | } else if res.constraints_found != 0 { 87 | // since constraints and requirements have the same syntax, its okay to use the same parser. 88 | find_reqs_imports(&files).await 89 | } else if res.pyproject_found != 0 { 90 | // use pyproject instead (if it exists) 91 | find_pyproject_imports(&files).await 92 | } else if res.setuppy_found != 0 { 93 | find_setuppy_imports(&files).await 94 | } else if res.py_found != 0 { 95 | // make sure theres atleast one python file, then use that 96 | find_python_imports(&files).await 97 | } else { 98 | eprintln!( 99 | "Could not find any requirements.txt, pyproject.toml or python files in this directory" 100 | ); exit(1) 101 | } 102 | } 103 | 104 | async fn find_setuppy_imports(f: &Vec) { 105 | let cons = console::Term::stdout(); 106 | cons.write_line("Using setup.py as source...") 107 | .unwrap(); 108 | 109 | let mut imports = Vec::new(); 110 | for file in f { 111 | if file.is_setuppy() { 112 | let readf = fs::read_to_string(file.path.clone()); 113 | if let Ok(f) = readf { 114 | extractor::extract_imports_setup_py(f.as_str(), &mut imports); 115 | } else { 116 | eprintln!("There was a problem reading your setup.py") 117 | } 118 | } 119 | } 120 | // println!("{:?}", imports.clone()); 121 | // cons.clear_last_lines(1).unwrap(); 122 | // --- pass the dependencies to the scanner/api --- 123 | scanner::start(imports).await.unwrap(); 124 | } 125 | async fn find_python_imports(f: &Vec) { 126 | let cons = console::Term::stdout(); 127 | cons.write_line("Using python file as source...").unwrap(); 128 | 129 | let mut imports = Vec::new(); // contains the Dependencies 130 | for file in f { 131 | if file.is_python() { 132 | if let Ok(fhandle) = File::open(file.path.clone()) { 133 | let reader = BufReader::new(fhandle); 134 | 135 | for line in reader.lines().flatten() { 136 | extractor::extract_imports_python(line, &mut imports); 137 | } 138 | } 139 | } 140 | } 141 | // println!("{:?}", imports.clone()); 142 | // cons.clear_last_lines(1).unwrap(); 143 | // --- pass the dependencies to the scanner/api --- 144 | scanner::start(imports).await.unwrap(); // unwrapping is ok since the return value doesnt matter. 145 | } 146 | 147 | async fn find_reqs_imports(f: &Vec) { 148 | let cons = console::Term::stdout(); 149 | cons.write_line("Using requirements.txt...") 150 | .unwrap(); 151 | 152 | let mut imports = Vec::new(); 153 | for file in f { 154 | if file.is_reqs() { 155 | if let Ok(fhandle) = File::open(file.path.clone()) { 156 | let reader = BufReader::new(fhandle); 157 | 158 | for line in reader.lines().flatten() { 159 | // pep-508 does not parse --hash embeds in requirements.txt 160 | // see (https://github.com/figsoda/pep-508/issues/2) 161 | extractor::extract_imports_reqs(line.trim().to_string(), &mut imports) 162 | } 163 | } 164 | } 165 | } 166 | // println!("{:?}", imports.clone()); 167 | 168 | // --- pass the dependencies to the scanner/api --- 169 | scanner::start(imports).await.unwrap(); 170 | } 171 | 172 | async fn find_pyproject_imports(f: &Vec) { 173 | let cons = console::Term::stdout(); 174 | cons.write_line("Using pyproject.toml as source...") 175 | .unwrap(); 176 | 177 | let mut imports = Vec::new(); 178 | for file in f { 179 | if file.is_pyproject() { 180 | let readf = fs::read_to_string(file.path.clone()); 181 | if let Ok(f) = readf { 182 | let _ = extractor::extract_imports_pyproject(f, &mut imports); 183 | } else { 184 | eprintln!("There was a problem reading your pyproject.toml") 185 | } 186 | } 187 | } 188 | // println!("{:?}", imports.clone()); 189 | // cons.clear_last_lines(1).unwrap(); 190 | // --- pass the dependencies to the scanner/api --- 191 | scanner::start(imports).await.unwrap(); 192 | } 193 | -------------------------------------------------------------------------------- /src/parser/structs.rs: -------------------------------------------------------------------------------- 1 | use console::style; 2 | use std::{ffi::OsString, process::exit}; 3 | 4 | use crate::{scanner::models::Query, utils, ARGS}; 5 | 6 | use super::scanner::models::Vulnerability; 7 | 8 | // struct Python; 9 | // struct Requirements; 10 | // struct Pyproject; 11 | 12 | #[derive(Debug, PartialEq, Eq, Clone)] 13 | pub enum FileTypes { 14 | Python, 15 | Requirements, 16 | Pyproject, 17 | Constraints, 18 | SetupPy, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct FoundFile { 23 | pub name: OsString, 24 | pub filetype: FileTypes, 25 | pub path: OsString, 26 | } 27 | 28 | impl FoundFile { 29 | pub fn is_python(&self) -> bool { 30 | self.filetype == FileTypes::Python 31 | } 32 | pub fn is_reqs(&self) -> bool { 33 | self.filetype == FileTypes::Requirements 34 | } 35 | pub fn is_pyproject(&self) -> bool { 36 | self.filetype == FileTypes::Pyproject 37 | } 38 | pub fn is_setuppy(&self) -> bool { 39 | self.filetype == FileTypes::SetupPy 40 | } 41 | } 42 | 43 | #[derive(Debug, Clone)] 44 | pub struct FoundFileResult { 45 | /// provides overall info about the files found (useful for proritising filetypes) 46 | pub files: Vec, 47 | pub py_found: u64, // no. of said files found 48 | pub reqs_found: u64, 49 | pub pyproject_found: u64, 50 | pub constraints_found: u64, 51 | pub setuppy_found: u64 52 | } 53 | 54 | impl FoundFileResult { 55 | pub fn new() -> FoundFileResult { 56 | FoundFileResult { 57 | files: Vec::new(), 58 | py_found: 0, 59 | reqs_found: 0, 60 | pyproject_found: 0, 61 | constraints_found: 0, 62 | setuppy_found: 0, 63 | } 64 | } 65 | pub fn add(&mut self, f: FoundFile) { 66 | self.files.push(f) 67 | } 68 | pub fn python(&mut self) { 69 | self.py_found += 1 70 | } 71 | pub fn reqs(&mut self) { 72 | self.reqs_found += 1 73 | } 74 | pub fn pyproject(&mut self) { 75 | self.pyproject_found += 1 76 | } 77 | pub fn constraints(&mut self) { 78 | self.constraints_found += 1 79 | } 80 | pub fn setuppy(&mut self) { 81 | self.setuppy_found += 1 82 | } 83 | } 84 | 85 | #[derive(Debug, Clone)] 86 | pub struct Dependency { 87 | pub name: String, 88 | pub version: Option, 89 | pub comparator: Option, 90 | pub version_status: VersionStatus, 91 | } 92 | 93 | impl Dependency { 94 | pub fn to_query(&self) -> Query { 95 | Query::new(self.version.as_ref().unwrap().as_str(), self.name.as_str()) 96 | } 97 | } 98 | 99 | #[derive(Debug, Clone)] 100 | pub struct VersionStatus { 101 | // pyscan may get version info from a lot of places. This keeps it in check. 102 | pub pypi: bool, 103 | pub pip: bool, 104 | pub source: bool, 105 | } 106 | 107 | /// implementation for VersionStatus which can get return versions while updating the status, also pick the one decided via arguments, a nice abstraction really. 108 | impl VersionStatus { 109 | /// retreives versions from pip and pypi.org in (pip, pypi) format. 110 | pub async fn _full_check(&mut self, name: &str) -> (String, String) { 111 | let pip = utils::get_python_package_version(name); 112 | let pip_v = if let Err(e) = pip { 113 | println!("An error occurred while retrieving version info from pip.\n{e}"); 114 | exit(1) 115 | } else { 116 | pip.unwrap() 117 | }; 118 | 119 | let pypi = utils::get_package_version_pypi(name).await; 120 | let pypi_v = if let Err(e) = pypi { 121 | println!("An error occurred while retrieving version info from pypi.org.\n{e}"); 122 | exit(1) 123 | } else { 124 | *pypi.unwrap() 125 | }; 126 | 127 | self.pip = true; 128 | self.pypi = true; 129 | 130 | (pip_v, pypi_v) 131 | } 132 | 133 | pub fn pip(name: &str) -> String { 134 | let pip = utils::get_python_package_version(name); 135 | 136 | if let Err(e) = pip { 137 | println!("An error occurred while retrieving version info from pip.\n{e}"); 138 | exit(1) 139 | } else { 140 | pip.unwrap() 141 | } 142 | } 143 | 144 | pub async fn pypi(name: &str) -> String { 145 | let pypi = utils::get_package_version_pypi(name).await; 146 | 147 | if let Err(e) = pypi { 148 | println!("An error occurred while retrieving version info from pypi.org.\n{e}"); 149 | exit(1) 150 | } else { 151 | *pypi.unwrap() 152 | } 153 | } 154 | 155 | /// returns the chosen version (from args or fallback) 156 | pub async fn choose(name: &str, dversion: &Option) -> String { 157 | if ARGS.get().unwrap().pip { 158 | VersionStatus::pip(name) 159 | } else if ARGS.get().unwrap().pypi { 160 | VersionStatus::pypi(name).await 161 | } else { 162 | // fallback begins here once made sure no arguments are provided 163 | let d_version = if let Some(provided) = dversion { 164 | Some(provided.to_string()) 165 | } else if let Ok(v) = utils::get_python_package_version(name) { 166 | println!("{} : {}",style(name).yellow().dim(), style("A version could not be detected in the source file, so retrieving version from pip instead.").dim()); 167 | Some(v) 168 | } else if let Ok(v) = utils::get_package_version_pypi(name).await { 169 | println!("{} : {}",style(name).red().dim(), style("A version could not be detected through source or pip, so retrieving latest version from pypi.org instead.").dim()); 170 | Some(v.to_string()) 171 | } else { 172 | eprintln!("A version could not be retrieved for {}. This should not happen as pyscan defaults pip or pypi.org, unless:\n1) Pip is not installed\n2) You don't have an internet connection\n3) You did not anticipate the consequences of not specifying a version for your dependency in the configuration files.\nReach out on github.com/aswinnnn/pyscan/issues if the above cases did not take place.", style(name).bright().red()); 173 | exit(1); 174 | }; 175 | d_version.unwrap() 176 | } 177 | } 178 | } 179 | 180 | #[derive(Debug, Clone)] 181 | pub struct ScannedDependency { 182 | pub name: String, 183 | pub version: String, 184 | pub vuln: Vulnerability, 185 | } 186 | -------------------------------------------------------------------------------- /src/scanner/api.rs: -------------------------------------------------------------------------------- 1 | use crate::{display, ARGS}; 2 | /// provides the functions needed to connect to various advisory sources. 3 | use crate::{parser::structs::Dependency, scanner::models::Vulnerability}; 4 | use crate::{ 5 | parser::structs::{ScannedDependency, VersionStatus}, 6 | scanner::models::Vuln, 7 | }; 8 | use reqwest::{self, Client, Method}; 9 | use futures::future; 10 | use std::{fs, env}; 11 | use std::process::exit; 12 | use super::{ 13 | super::utils, 14 | models::{Query, QueryBatched, QueryResponse}, 15 | }; 16 | 17 | /// OSV provides a distrubuted database for vulns, with a free API 18 | #[derive(Debug)] 19 | pub struct Osv { 20 | /// check if the host is online 21 | pub online: bool, 22 | /// time of last query 23 | pub last_queried: String, 24 | /// the Client which handles the API. 25 | client: Client, 26 | } 27 | 28 | impl Osv { 29 | pub async fn new() -> Result { 30 | let version = utils::get_version(); 31 | let pyscan_version = format!("pyscan {}", version); 32 | let client = reqwest::Client::builder() 33 | .user_agent(pyscan_version) 34 | .build(); 35 | 36 | if let Ok(client) = client { 37 | let res = client.get("https://osv.dev").send().await; 38 | 39 | if let Ok(_success) = res { 40 | Ok(Osv { 41 | online: true, 42 | last_queried: { utils::get_time() }, 43 | client, 44 | }) 45 | } else { 46 | eprintln!( 47 | "Could not connect to the OSV website. Check your internet or try again." 48 | ); exit(1) 49 | } 50 | } else { 51 | eprintln!( 52 | "Could not build the network client to connect to OSV. Report this at github.com/aswinnnn/pyscan/issues" 53 | ); exit(1) 54 | } 55 | } 56 | 57 | pub async fn _query(&self, d: Dependency) -> Option { 58 | // returns None if no vulns found 59 | // else Some(Vulnerability) 60 | 61 | let version = if d.version.is_some() { 62 | d.version 63 | } else { 64 | let res = utils::get_package_version_pypi(d.name.as_str()).await; 65 | if let Err(e) = res { 66 | eprintln!("PypiError:\n{}", e); 67 | exit(1); 68 | } else if let Ok(res) = res { 69 | Some(res.to_string()) 70 | } else { 71 | eprintln!("A very unexpected error occurred while retrieving version info from Pypi. Please report this on https://github.com/aswinnnn/pyscan/issues"); 72 | exit(1); 73 | } 74 | }; 75 | // println!("{:?}", self.get_latest_package_version(d.name.clone())); 76 | 77 | 78 | // println!("{:?}", res); 79 | 80 | self._get_json(d.name.as_str(), &version.unwrap()).await 81 | } 82 | 83 | pub async fn query_batched(&self, mut deps: Vec) -> Vec { 84 | // runs the batch API. Each dep is converted into JSON format here, POSTed, and the response of vuln IDs -> queried into Vec -> returned as Vec 85 | // The dep version conflicts are also solved over here. 86 | let _ = future::join_all(deps 87 | .iter_mut() 88 | .map(|d| async { 89 | d.version = if d.version.is_none() { 90 | Some(VersionStatus::choose(d.name.as_str(), &d.version).await) 91 | } else { 92 | d.version.clone() 93 | } 94 | })).await; 95 | 96 | // .collect::>(); 97 | let mut progress = display::Progress::new(); 98 | 99 | let mut imports_info = utils::vecdep_to_hashmap(&deps); 100 | 101 | let url = "https://api.osv.dev/v1/querybatch"; 102 | 103 | let queries: Vec = deps.iter().map(|d| d.to_query()).collect(); 104 | let batched = QueryBatched::new(queries); 105 | 106 | let body = serde_json::to_string(&batched).unwrap(); 107 | 108 | let res = self.client.request(Method::POST, url).body(body).send().await; 109 | if let Ok(response) = res { 110 | if response.status().is_client_error() { 111 | eprintln!("Failed connecting to OSV. [Client error]"); 112 | exit(1) 113 | } else if response.status().is_server_error() { 114 | eprintln!("Failed connecting to OSV. [Server error]"); 115 | exit(1) 116 | } 117 | 118 | let restext = response.text().await.unwrap(); 119 | 120 | let parsed: Result = serde_json::from_str(&restext); 121 | let mut scanneddeps: Vec = Vec::new(); 122 | if ARGS.get().unwrap().output.is_some() { 123 | // txt or json extention inference, custom output filename 124 | let filename = ARGS.get().unwrap().output.as_ref().unwrap(); 125 | if ".json" == &filename[{ filename.len() - 5 }..] { 126 | if let Ok(dir) = env::current_dir() { 127 | let r = fs::write(dir.join(filename), restext); 128 | if let Err(er) = r { 129 | eprintln!("Could not write output to file: {}", er.to_string()); 130 | exit(1) 131 | } 132 | else { 133 | exit(0) 134 | } 135 | } 136 | } 137 | } 138 | if let Ok(p) = parsed { 139 | for vres in p.results { 140 | if let Some(vulns) = vres.vulns { 141 | 142 | 143 | let mut vecvulns: Vec = Vec::new(); 144 | for qv in vulns.iter() { 145 | vecvulns.push(self.vuln_id(qv.id.as_str()).await) // retrives vuln info from API with a vuln ID 146 | } 147 | 148 | // has to be turnt to Vulnerability before becoming a scanned dependency 149 | let structvuln = Vulnerability {vulns: vecvulns}; 150 | progress.count_one(); progress.display(); // increment progress 151 | scanneddeps.push(structvuln.to_scanned_dependency(&imports_info)); 152 | 153 | } 154 | else {continue;} 155 | } 156 | if progress.count > 0 {progress.end()} // clear progress line 157 | 158 | // --- passing to display module starts here --- 159 | display::display_queried(&scanneddeps, &mut imports_info); 160 | scanneddeps 161 | } else { 162 | eprintln!("Invalid parse of API reponse at src/scanner/api.rs::query_batched\nThis is usually due to a unforeseen API response or a malformed source file."); 163 | exit(1); 164 | } 165 | } else { 166 | eprintln!("Could not fetch a response from osv.dev [scanner/api/query_batched]"); 167 | exit(1); 168 | } 169 | } 170 | 171 | /// get a Vuln from a vuln ID from OSV 172 | pub async fn vuln_id(&self, id: &str) -> Vuln { 173 | let url = format!("https://api.osv.dev/v1/vulns/{id}"); 174 | 175 | let res = self.client.request(Method::GET, url).send().await; 176 | 177 | // println!("{:?}", res); 178 | 179 | if let Ok(response) = res { 180 | if response.status().is_client_error() { 181 | eprintln!("Failed connecting to OSV. [Client error]") 182 | } else if response.status().is_server_error() { 183 | eprintln!("Failed connecting to OSV. [Server error]") 184 | } 185 | let restext = response.text().await.unwrap(); 186 | // println!("{:#?}", restext.clone()); 187 | let parsed: Result = serde_json::from_str(&restext); 188 | if let Ok(p) = parsed { 189 | p 190 | } else if let Err(e) = parsed { 191 | eprintln!("Invalid parse of API reponse at src/scanner/api.rs::vuln_id\n{}", e); 192 | exit(1); 193 | } 194 | else { 195 | eprintln!("Invalid parse of API reponse at src/scanner/api.rs(vuln_id)"); 196 | exit(1); 197 | } 198 | } else { 199 | eprintln!("Could not fetch a response from osv.dev [scanner/api/vulns_id]"); 200 | exit(1); 201 | } 202 | } 203 | 204 | pub async fn _get_json(&self, name: &str, version: &str) -> Option { 205 | let url = r"https://api.osv.dev/v1/query"; 206 | 207 | let body = Query::new(version, name); // struct implementation of query sent to OSV API. 208 | let body = serde_json::to_string(&body).unwrap(); 209 | 210 | // println!("{}", body.clone()); 211 | 212 | let res = self.client.request(Method::POST, url).body(body).send().await; 213 | 214 | // println!("{:?}", res); 215 | 216 | if let Ok(response) = res { 217 | if response.status().is_client_error() { 218 | eprintln!("Failed connecting to OSV. [Client error]") 219 | } else if response.status().is_server_error() { 220 | eprintln!("Failed connecting to OSV. [Server error]") 221 | } 222 | let restext = response.text().await.unwrap(); 223 | if !restext.len() < 3 { 224 | // check if vulns exist by char len of json 225 | // api returns '{}' if none found so this is easy 226 | 227 | let parsed: Result = 228 | serde_json::from_str(&restext); 229 | // println!("{:?}", parsed); 230 | if let Ok(v) = parsed { 231 | Some(v) 232 | } else { 233 | None 234 | } 235 | } else { 236 | None 237 | } 238 | } else { 239 | eprintln!("Could not fetch a response from osv.dev"); 240 | exit(1); 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/scanner/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod models; 3 | use std::process::exit; 4 | use super::parser::structs::Dependency; 5 | use console::{Term, style}; 6 | 7 | 8 | pub async fn start(imports: Vec) -> Result<(), std::io::Error> { 9 | let osv = api::Osv::new().await.unwrap(); // err handling done inside, unwrapping is safe 10 | let cons = Term::stdout(); 11 | let s = format!("Found {} dependencies", style(format!("{}", imports.len())) 12 | .bold() 13 | .green()); 14 | 15 | cons.write_line(&s)?; 16 | 17 | // collected contains the dependencies with found vulns. imports_info contains a name, version hashmap of all found dependencies so we can display for all imports if vulns have been found or not 18 | let collected = osv.query_batched(imports).await; 19 | // query_batched passes stuff onto display module after 20 | 21 | // if we collected vulns: 22 | if !collected.is_empty() { 23 | exit(1) 24 | } 25 | else { 26 | Ok(()) // if collected is zero means no vulns found, no need for a non-zero exit code. 27 | } 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/scanner/models.rs: -------------------------------------------------------------------------------- 1 | // automatically generated. do not change. 2 | 3 | use std::collections::HashMap; 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | use crate::parser::structs::ScannedDependency; 8 | 9 | 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct Vulnerability { 13 | #[serde(rename = "vulns")] 14 | pub vulns: Vec, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct Vuln { 19 | #[serde(rename = "id")] 20 | pub id: String, 21 | 22 | // #[serde(rename = "summary")] 23 | // pub summary: Option, 24 | 25 | #[serde(rename = "details")] 26 | pub details: String, 27 | 28 | // #[serde(rename = "aliases")] 29 | // pub aliases: Vec, 30 | 31 | // #[serde(rename = "modified")] 32 | // pub modified: String, 33 | 34 | // #[serde(rename = "published")] 35 | // pub published: String, 36 | 37 | // #[serde(rename = "database_specific")] 38 | // pub database_specific: Option, 39 | 40 | // #[serde(rename = "references")] 41 | // pub references: Vec, 42 | 43 | #[serde(rename = "affected")] 44 | pub affected: Vec, 45 | 46 | // #[serde(rename = "schema_version")] 47 | // pub schema_version: String, 48 | 49 | // #[serde(rename = "severity")] 50 | // pub severity: Option>, 51 | } 52 | 53 | #[derive(Debug, Clone, Serialize, Deserialize)] 54 | pub struct Affected { 55 | #[serde(rename = "package")] 56 | pub package: Package, 57 | 58 | // #[serde(rename = "ranges")] 59 | // pub ranges: Vec, 60 | 61 | #[serde(rename = "versions")] 62 | pub versions: Option>, 63 | 64 | // #[serde(rename = "database_specific")] 65 | // pub database_specific: AffectedDatabaseSpecific, 66 | 67 | // #[serde(rename = "ecosystem_specific")] 68 | // pub ecosystem_specific: Option, 69 | } 70 | 71 | #[derive(Debug, Clone, Serialize, Deserialize)] 72 | pub struct AffectedDatabaseSpecific { 73 | #[serde(rename = "source")] 74 | pub source: String, 75 | } 76 | 77 | #[derive(Debug, Clone, Serialize, Deserialize)] 78 | pub struct EcosystemSpecific { 79 | #[serde(rename = "affected_functions")] 80 | pub affected_functions: Vec, 81 | } 82 | 83 | #[derive(Debug, Clone, Serialize, Deserialize)] 84 | pub struct Package { 85 | #[serde(rename = "name")] 86 | pub name: String, 87 | 88 | #[serde(rename = "ecosystem")] 89 | pub ecosystem: String, 90 | 91 | #[serde(rename = "purl")] 92 | pub purl: String, 93 | } 94 | 95 | #[derive(Debug, Clone, Serialize, Deserialize)] 96 | pub struct Range { 97 | #[serde(rename = "type")] 98 | pub range_type: String, 99 | 100 | #[serde(rename = "events")] 101 | pub events: Vec, 102 | 103 | #[serde(rename = "repo")] 104 | pub repo: Option, 105 | } 106 | 107 | #[derive(Debug, Clone, Serialize, Deserialize)] 108 | pub struct Event { 109 | #[serde(rename = "introduced")] 110 | pub introduced: Option, 111 | 112 | #[serde(rename = "fixed")] 113 | pub fixed: Option, 114 | } 115 | 116 | #[derive(Debug, Clone, Serialize, Deserialize)] 117 | pub struct VulnDatabaseSpecific { 118 | #[serde(rename = "cwe_ids")] 119 | pub cwe_ids: Vec, 120 | 121 | #[serde(rename = "github_reviewed")] 122 | pub github_reviewed: bool, 123 | 124 | #[serde(rename = "severity")] 125 | pub severity: String, 126 | 127 | #[serde(rename = "github_reviewed_at")] 128 | pub github_reviewed_at: String, 129 | 130 | #[serde(rename = "nvd_published_at")] 131 | pub nvd_published_at: Option, 132 | } 133 | 134 | #[derive(Debug, Clone, Serialize, Deserialize)] 135 | pub struct Reference { 136 | #[serde(rename = "type")] 137 | pub reference_type: String, 138 | 139 | #[serde(rename = "url")] 140 | pub url: String, 141 | } 142 | 143 | #[derive(Debug, Clone, Serialize, Deserialize)] 144 | pub struct Severity { 145 | #[serde(rename = "type")] 146 | pub severity_type: String, 147 | 148 | #[serde(rename = "score")] 149 | pub score: String, 150 | } 151 | 152 | // --- pypi.org/pypi//json JSON repsonse --- 153 | 154 | #[derive(Debug, Clone, Serialize, Deserialize)] 155 | pub struct PypiResponse { 156 | // #[serde(rename = "info")] 157 | // pub info: Info, 158 | 159 | // #[serde(rename = "last_serial")] 160 | // pub last_serial: i64, 161 | 162 | #[serde(rename = "releases")] 163 | pub releases: HashMap>>, 164 | 165 | // #[serde(rename = "urls")] 166 | // pub urls: Vec, 167 | 168 | // #[serde(rename = "vulnerabilities")] 169 | // pub vulnerabilities: Vec>, 170 | } 171 | 172 | #[derive(Debug, Clone, Serialize, Deserialize)] 173 | pub struct Info { 174 | #[serde(rename = "author")] 175 | pub author: String, 176 | 177 | #[serde(rename = "author_email")] 178 | pub author_email: String, 179 | 180 | #[serde(rename = "bugtrack_url")] 181 | pub bugtrack_url: Option, 182 | 183 | #[serde(rename = "classifiers")] 184 | pub classifiers: Vec, 185 | 186 | #[serde(rename = "description")] 187 | pub description: String, 188 | 189 | #[serde(rename = "description_content_type")] 190 | pub description_content_type: String, 191 | 192 | #[serde(rename = "docs_url")] 193 | pub docs_url: Option, 194 | 195 | #[serde(rename = "download_url")] 196 | pub download_url: Option, 197 | 198 | #[serde(rename = "downloads")] 199 | pub downloads: Downloads, 200 | 201 | #[serde(rename = "home_page")] 202 | pub home_page: String, 203 | 204 | #[serde(rename = "keywords")] 205 | pub keywords: String, 206 | 207 | #[serde(rename = "license")] 208 | pub license: String, 209 | 210 | #[serde(rename = "maintainer")] 211 | pub maintainer: Option, 212 | 213 | #[serde(rename = "maintainer_email")] 214 | pub maintainer_email: Option, 215 | 216 | #[serde(rename = "name")] 217 | pub name: String, 218 | 219 | #[serde(rename = "package_url")] 220 | pub package_url: String, 221 | 222 | #[serde(rename = "platform")] 223 | pub platform: Option, 224 | 225 | #[serde(rename = "project_url")] 226 | pub project_url: String, 227 | 228 | #[serde(rename = "project_urls")] 229 | pub project_urls: ProjectUrls, 230 | 231 | #[serde(rename = "release_url")] 232 | pub release_url: String, 233 | 234 | #[serde(rename = "requires_dist")] 235 | pub requires_dist: Option, 236 | 237 | #[serde(rename = "requires_python")] 238 | pub requires_python: String, 239 | 240 | #[serde(rename = "summary")] 241 | pub summary: String, 242 | 243 | #[serde(rename = "version")] 244 | pub version: String, 245 | 246 | #[serde(rename = "yanked")] 247 | pub yanked: bool, 248 | 249 | #[serde(rename = "yanked_reason")] 250 | pub yanked_reason: Option, 251 | } 252 | 253 | #[derive(Debug, Clone, Serialize, Deserialize)] 254 | pub struct Downloads { 255 | #[serde(rename = "last_day")] 256 | pub last_day: i64, 257 | 258 | #[serde(rename = "last_month")] 259 | pub last_month: i64, 260 | 261 | #[serde(rename = "last_week")] 262 | pub last_week: i64, 263 | } 264 | 265 | #[derive(Debug, Clone, Serialize, Deserialize)] 266 | pub struct ProjectUrls { 267 | #[serde(rename = "Homepage")] 268 | pub homepage: String, 269 | 270 | #[serde(rename = "Source Code")] 271 | pub source_code: String, 272 | } 273 | 274 | #[derive(Debug, Clone, Serialize, Deserialize)] 275 | pub struct Url { 276 | // #[serde(rename = "comment_text")] 277 | // pub comment_text: Option, 278 | 279 | #[serde(rename = "digests")] 280 | pub digests: Option, 281 | 282 | #[serde(rename = "downloads")] 283 | pub downloads: Option, 284 | 285 | #[serde(rename = "filename")] 286 | pub filename: Option, 287 | 288 | #[serde(rename = "has_sig")] 289 | pub has_sig: Option, 290 | 291 | #[serde(rename = "md5_digest")] 292 | pub md5_digest: Option, 293 | 294 | #[serde(rename = "packagetype")] 295 | pub packagetype: Option, 296 | 297 | #[serde(rename = "python_version")] 298 | pub python_version: Option, 299 | 300 | #[serde(rename = "requires_python")] 301 | pub requires_python: Option, 302 | 303 | #[serde(rename = "size")] 304 | pub size: Option, 305 | 306 | #[serde(rename = "upload_time")] 307 | pub upload_time: Option, 308 | 309 | #[serde(rename = "upload_time_iso_8601")] 310 | pub upload_time_iso_8601: Option, 311 | 312 | #[serde(rename = "url")] 313 | pub url: Option, 314 | 315 | #[serde(rename = "yanked")] 316 | pub yanked: Option, 317 | 318 | // #[serde(rename = "yanked_reason")] 319 | // pub yanked_reason: Option, 320 | } 321 | 322 | #[derive(Debug, Clone, Serialize, Deserialize)] 323 | pub struct Digests { 324 | #[serde(rename = "blake2b_256")] 325 | pub blake2_b_256: String, 326 | 327 | #[serde(rename = "md5")] 328 | pub md5: String, 329 | 330 | #[serde(rename = "sha256")] 331 | pub sha256: String, 332 | } 333 | 334 | 335 | // BATCHED QUERY MODELS 336 | 337 | #[derive(Debug, Clone, Serialize, Deserialize)] 338 | pub struct QueryBatched { 339 | queries: Vec 340 | } 341 | 342 | #[derive(Debug, Clone, Serialize, Deserialize)] 343 | pub struct Query { 344 | 345 | pub version: String, 346 | 347 | pub package: QueryPackage, 348 | } 349 | 350 | #[derive(Debug, Clone, Serialize, Deserialize)] 351 | pub struct QueryPackage { 352 | pub name: String, 353 | 354 | pub ecosystem: String 355 | } 356 | 357 | // REPONSE FROM QUERY_BATCHED 358 | 359 | // #[derive(Debug, Clone, Serialize, Deserialize)] 360 | // pub struct QueryResponse { 361 | // pub results: Vec 362 | // } 363 | 364 | // #[derive(Debug, Clone, Serialize, Deserialize)] 365 | // pub struct QueryResponseVulns { 366 | // pub vulns: Vec> // each vec represents individual dependencies, which may or may not have vuln(s) present, therefore optioned. 367 | // } 368 | 369 | // #[derive(Debug, Clone, Serialize, Deserialize)] 370 | // pub struct QueryVulnInfo { 371 | // pub id: String, 372 | // pub modified: String 373 | // } 374 | 375 | #[derive(Debug, Clone, Serialize, Deserialize)] 376 | pub struct QueryResponse { 377 | pub results: Vec, 378 | } 379 | 380 | #[derive(Debug, Clone, Serialize, Deserialize)] 381 | pub struct QueryResult { 382 | pub vulns: Option>, 383 | } 384 | 385 | #[derive(Debug, Clone, Serialize, Deserialize)] 386 | pub struct QueryVuln { 387 | pub id: String, 388 | 389 | pub modified: String, 390 | } 391 | 392 | 393 | impl Query { 394 | pub fn new(version: &str, name: &str) -> Query { 395 | Query { 396 | version: version.to_string(), 397 | package: QueryPackage { name: name.to_string(), ecosystem: "PyPI".to_string() } 398 | } 399 | } 400 | } 401 | 402 | impl QueryBatched { 403 | pub fn new(q: Vec) -> QueryBatched { 404 | QueryBatched { queries: q } 405 | } 406 | } 407 | 408 | impl Vulnerability { 409 | pub fn to_scanned_dependency(&self, imports_info: &HashMap) -> ScannedDependency { 410 | // println!("{:#?}", imports_info); 411 | let name_from_v = if let Some(n) = self.vulns.first() { 412 | if !n.affected.is_empty() {n.affected.first().unwrap().package.name.clone()} 413 | else {"Name in Context".to_string()} 414 | } 415 | else {"Name In Context".to_string()}; 416 | 417 | let version_from_map = if let Some(v) = imports_info.get(&name_from_v) { 418 | v 419 | } else { 420 | &"parent package related to one of your dependencies".to_string() 421 | // this happens rarely but every once in a while a vulnerability 422 | // gets assigned to a different (usually) parent package so trying to 423 | // get the version from our map fails unfortunately. 424 | }; 425 | 426 | ScannedDependency { name: name_from_v, version: version_from_map.to_owned(), vuln: self.clone() } 427 | 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Timelike, Utc}; 2 | use reqwest::{ 3 | self, 4 | {Client, Response}, 5 | Method, 6 | }; 7 | use semver::Version; 8 | use std::{ 9 | boxed::Box, 10 | collections::HashMap, 11 | io::{self, Error, ErrorKind}, 12 | str::{self}, 13 | }; 14 | 15 | pub fn get_time() -> String { 16 | // get the current time in a stting format i like. 17 | let now = Utc::now(); 18 | let (is_pm, hour) = now.hour12(); 19 | { 20 | let time = format!( 21 | "{:02}:{:02}:{:02} {}", 22 | hour, 23 | now.minute(), 24 | now.second(), 25 | if is_pm { "PM" } else { "AM" } 26 | ); 27 | 28 | time 29 | } 30 | } 31 | 32 | pub fn get_version() -> String { 33 | "0.1.7".to_string() 34 | } 35 | 36 | pub async fn _reqwest_send(method: &str, url: String) -> Option { 37 | // for easily sending web requests 38 | 39 | let client = reqwest::Client::builder() 40 | .user_agent(format!("pyscan v{}", get_version())) 41 | .build(); 42 | 43 | if let Ok(client) = client { 44 | let method = match method { 45 | "get" => Method::GET, 46 | "post" => Method::POST, 47 | "put" => Method::PUT, 48 | "head" => Method::HEAD, 49 | "connect" => Method::CONNECT, 50 | "trace" => Method::TRACE, 51 | &_ => { 52 | println!("Didn't recognize that method so defaulting to GET"); 53 | Method::GET 54 | } 55 | }; 56 | let res = client.request(method, url).send().await; 57 | 58 | if let Ok(success) = res { 59 | Some(success) 60 | } else { 61 | eprintln!( 62 | "Could not establish an internet connection. Check your internet or try again." 63 | ); exit(1) 64 | } 65 | } else { 66 | eprintln!("Could not build the network client. Report this at https://github.com/aswinnnn/pyscan/issues"); 67 | None 68 | } 69 | } 70 | 71 | use std::process::{exit, Command}; 72 | 73 | use crate::{parser::structs::Dependency, scanner::models::PypiResponse, PIPCACHE}; 74 | // Define a custom error type that wraps a String message 75 | #[derive(Debug)] 76 | pub struct PipError(String); 77 | 78 | // Implement the std::error::Error trait for DockerError 79 | impl std::error::Error for PipError {} 80 | 81 | // Implement the std::fmt::Display trait for DockerError 82 | impl std::fmt::Display for PipError { 83 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 84 | write!(f, "Pip error: {}", self.0) 85 | } 86 | } 87 | 88 | pub fn get_python_package_version(package: &str) -> Result { 89 | // gets the version of a package from pip. 90 | 91 | // check cache first 92 | if PIPCACHE.cached { 93 | let version = PIPCACHE 94 | .lookup(package) 95 | .map_err(|e| PipError(e.to_string()))?; 96 | Ok(version) 97 | } else { 98 | let output = Command::new("pip") 99 | .arg("show") 100 | .arg(package) 101 | .output() 102 | .map_err(|e| PipError(e.to_string()))?; 103 | 104 | let output = output.stdout; 105 | let output = String::from_utf8(output).map_err(|e| PipError(e.to_string()))?; 106 | 107 | let version = output 108 | .lines() 109 | .find(|line| line.starts_with("Version: ")) 110 | .map(|line| line[9..].to_string()); 111 | 112 | if let Some(v) = version { 113 | Ok(v) 114 | } else { 115 | Err(PipError( 116 | "could not retrive package version from Pip".to_string(), 117 | )) 118 | } 119 | } 120 | } 121 | 122 | #[derive(Debug)] 123 | pub struct PypiError(String); 124 | 125 | // Implement the std::error::Error trait for PypiError 126 | impl std::error::Error for PypiError {} 127 | 128 | // Implement the std::fmt::Display trait for PypiError 129 | impl std::fmt::Display for PypiError { 130 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 131 | write!(f, "pypi.org error: {}\n\n(note: this might usually happen when the dependency does not exist on pypi [check spelling, typos, etc] or when there's problems accessing the website.)", self.0) 132 | } 133 | } 134 | 135 | impl From for PypiError { 136 | fn from(item: reqwest::Error) -> Self { 137 | PypiError(item.to_string()) 138 | } 139 | } 140 | 141 | pub async fn get_package_version_pypi<'a>(package: &str) -> Result, PypiError> { 142 | let url = format!("https://pypi.org/pypi/{package}/json"); 143 | 144 | let client = Client::new(); 145 | let res = client.get(url).send().await?.error_for_status(); 146 | 147 | let version = if let Err(e) = res { 148 | eprintln!("Failed to make a request to pypi.org:\n{}", e); 149 | Err(PypiError(e.to_string())) 150 | } else if let Ok(r) = res { 151 | let restext = r.text().await; 152 | let restext = if let Ok(r) = restext { 153 | r 154 | } else { 155 | return Err(PypiError("Failed to connect to pypi.org".to_string())); 156 | }; 157 | // println!("{:#?}", restext.clone()); 158 | 159 | let parsed: Result = serde_json::from_str(restext.trim()); 160 | 161 | let version = if let Err(e) = parsed { 162 | eprintln!("Failed to parse reponse from pypi.org:\n{}", e); 163 | Err(PypiError(e.to_string())) 164 | } else if let Ok(pypi) = parsed { 165 | let strvers: Vec = pypi.releases.into_keys().collect(); // versions in string 166 | let mut somever: Vec = semver_parse(strvers); 167 | somever.sort(); 168 | Ok(somever.last().unwrap().to_owned()) 169 | } else { 170 | Err(PypiError("pypi.org response error".to_string())) 171 | }; 172 | version 173 | } else if res.is_err() { 174 | let _ = res.map_err(|e| PypiError(e.to_string())); 175 | exit(1) 176 | } else { 177 | exit(1) 178 | }; 179 | 180 | Ok(Box::new(if let Err(e) = version { 181 | eprintln!("{e}"); 182 | exit(1) 183 | } else { 184 | version.unwrap().to_string() 185 | })) 186 | } 187 | 188 | // creates a hashmap of package name,version from pip list. 189 | pub fn pip_list() -> io::Result> { 190 | let output = Command::new("pip") 191 | .arg("list") 192 | .output() 193 | .map_err(|_| io::Error::new(ErrorKind::Other, "Failed to execute 'pip list' command. pyscan caches the dependencies from pip with versions to be faster and it could not run 'pip list'. You can turn this off via just using --cache-off [note: theres a chance pyscan might still fallback to using pip]"))?; 194 | 195 | let output_str = str::from_utf8(&output.stdout) 196 | .map_err(|_| io::Error::new(ErrorKind::InvalidData, "Output from 'pip list' was not valid UTF-8. pyscan caches the dependencies from pip with versions to be faster and the output it recieved was not valid UTF-8. You can turn this off via just using --cache-off [note: theres a chance pyscan might still fallback to using pip]"))?; 197 | 198 | let mut pip_list: HashMap = HashMap::new(); 199 | 200 | for line in output_str.lines().skip(2) { 201 | // Skip the first two lines 202 | let split: Vec<&str> = line.split_whitespace().collect(); 203 | if split.len() >= 2 { 204 | pip_list.insert(split[0].to_string(), split[1].to_string()); 205 | } 206 | } 207 | 208 | Ok(pip_list) 209 | } 210 | 211 | pub fn semver_parse(v: Vec) -> Vec { 212 | let mut cache: Vec = Vec::new(); 213 | for x in v { 214 | let version = lenient_semver::Version::parse(x.as_str()).unwrap(); 215 | let b = Version::from(version); 216 | cache.push(b) 217 | } 218 | cache 219 | } 220 | 221 | /// returns a hashmap of (dependency name, version) 222 | pub fn vecdep_to_hashmap(v: &[Dependency]) -> HashMap { 223 | let mut importmap: HashMap = HashMap::new(); 224 | 225 | v.iter().for_each(|d| { 226 | importmap.insert(d.name.clone(), d.version.as_ref().unwrap().clone()); 227 | }); 228 | 229 | importmap 230 | } 231 | /// caches package name, version data from 'pip list' in a hashmap for efficient lookup later. 232 | pub struct PipCache { 233 | cache: HashMap, 234 | cached: bool, 235 | } 236 | 237 | impl PipCache { 238 | // initializes the cache, caches and returns itself. 239 | pub fn init() -> PipCache { 240 | let pip_list = pip_list(); 241 | if let Ok(pl) = pip_list { 242 | PipCache { 243 | cache: pl, 244 | cached: true, 245 | } 246 | } else if let Err(e) = pip_list { 247 | eprintln!("{e}"); 248 | exit(1) 249 | } else { 250 | exit(1) 251 | } 252 | } 253 | 254 | // clears if cached, otherwise does nothing 255 | pub fn _clear_cache(&mut self) { 256 | if !self.cached { 257 | } else { 258 | self.cache.clear() 259 | } 260 | } 261 | 262 | // Function to look up a package by name in cache 263 | pub fn lookup(&self, package_name: &str) -> io::Result { 264 | match self.cache.get(package_name) { 265 | Some(version) => Ok(version.to_string()), 266 | None => Err(Error::new(ErrorKind::NotFound, "Package not found in pip")), 267 | } 268 | } 269 | } 270 | 271 | // useful info to have during the entire execution of the program. 272 | pub struct SysInfo { 273 | pub pip_found: bool, 274 | pub pypi_found: bool, 275 | } 276 | 277 | impl SysInfo { 278 | pub async fn new() -> SysInfo { 279 | let pip_found: bool = pip_list().is_ok(); 280 | let pypi_found: bool = check_pypi_status().await; 281 | 282 | SysInfo { 283 | pip_found, 284 | pypi_found, 285 | } 286 | } 287 | } 288 | 289 | pub async fn check_pypi_status() -> bool { 290 | let r = _reqwest_send("get", "https://pypi.org".to_string()).await.ok_or(()); 291 | if let Ok(res) = r { 292 | res.status().is_success() 293 | } 294 | else { 295 | false 296 | } 297 | } --------------------------------------------------------------------------------