From 5a8b9c04aa947096d35cd555e8f7e58878366634 Mon Sep 17 00:00:00 2001 From: Amine Raouane Date: Mon, 21 Apr 2025 12:00:05 +0100 Subject: [PATCH 01/10] feat: implement typosquatting detection for package names Adds a new security analysis feature to detect potential typosquatting in package names. Compares the package name against a list of popular packages using the Jaro-Winkler similarity algorithm. Packages exceeding a configurable threshold are flagged. Includes a default popular package list and an option for a custom list via configuration. Signed-off-by: Amine --- src/macaron/__main__.py | 11 + src/macaron/config/global_config.py | 5 + .../pypi_heuristics/heuristics.py | 3 + .../metadata/typosquatting_presence.py | 249 + src/macaron/resources/popular_packages.txt | 5000 +++++++++++++++++ .../checks/detect_malicious_metadata_check.py | 8 + 6 files changed, 5276 insertions(+) create mode 100644 src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py create mode 100644 src/macaron/resources/popular_packages.txt diff --git a/src/macaron/__main__.py b/src/macaron/__main__.py index 03549db7f..8fc3290e4 100644 --- a/src/macaron/__main__.py +++ b/src/macaron/__main__.py @@ -367,6 +367,16 @@ def main(argv: list[str] | None = None) -> None: help="The directory where Macaron looks for already cloned repositories.", ) + main_parser.add_argument( + "-pp", + "--popular-packages-path", + required=False, + type=str, + default=None, + help="The path to the popular packages file used for typosquatting detection.", + dest="popular_packages_path", + ) + # Add sub parsers for each action. sub_parser = main_parser.add_subparsers(dest="action", help="Run macaron --help for help") @@ -579,6 +589,7 @@ def main(argv: list[str] | None = None) -> None: build_log_path=os.path.join(args.output_dir, "build_log"), debug_level=log_level, local_repos_path=args.local_repos_path, + popular_packages_path=args.popular_packages_path, resources_path=os.path.join(macaron.MACARON_PATH, "resources"), ) diff --git a/src/macaron/config/global_config.py b/src/macaron/config/global_config.py index 8befb4045..805e9e7fa 100644 --- a/src/macaron/config/global_config.py +++ b/src/macaron/config/global_config.py @@ -49,6 +49,9 @@ class GlobalConfig: #: The path to the local .m2 Maven repository. This attribute is None if there is no available .m2 directory. local_maven_repo: str | None = None + #: The path to the popular packages file. + popular_packages_path: str | None = None + def load( self, macaron_path: str, @@ -57,6 +60,7 @@ def load( debug_level: int, local_repos_path: str, resources_path: str, + popular_packages_path: str, ) -> None: """Initiate the GlobalConfig object. @@ -81,6 +85,7 @@ def load( self.debug_level = debug_level self.local_repos_path = local_repos_path self.resources_path = resources_path + self.popular_packages_path = popular_packages_path def load_expectation_files(self, exp_path: str) -> None: """ diff --git a/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py b/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py index bd829a0f1..a5024b6ce 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py @@ -37,6 +37,9 @@ class Heuristics(str, Enum): #: Indicates that the package has an unusually large version number for a single release. ANOMALOUS_VERSION = "anomalous_version" + #: Indicates that the package name is similar to a popular package. + TYPOSQUATTING_PRESENCE = "typosquatting_presence" + class HeuristicResult(str, Enum): """Result type indicating the outcome of a heuristic.""" diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py new file mode 100644 index 000000000..71ea90988 --- /dev/null +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py @@ -0,0 +1,249 @@ +# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""Analyzer checks if there is typosquatting presence in the package name.""" +import logging +import os + +from macaron.config.global_config import global_config +from macaron.json_tools import JsonType +from macaron.malware_analyzer.pypi_heuristics.base_analyzer import BaseHeuristicAnalyzer +from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult, Heuristics +from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset + +logger = logging.getLogger(__name__) + + +class TyposquattingPresenceAnalyzer(BaseHeuristicAnalyzer): + """Check whether the PyPI package has typosquatting presence.""" + + def __init__(self) -> None: + super().__init__( + name="typosquatting_presence_analyzer", heuristic=Heuristics.TYPOSQUATTING_PRESENCE, depends_on=None + ) + self.popular_packages_path = os.path.join(global_config.resources_path, "popular_packages.txt") + self.distance_ratio_threshold = 0.95 + self.cost = 1 + self.scaling = 0.15 + self.keyboard = 0.8 + self.keyboard_layout = { + "1": (-1, 0), + "2": (-1, 1), + "3": (-1, 2), + "4": (-1, 3), + "5": (-1, 4), + "6": (-1, 5), + "7": (-1, 6), + "8": (-1, 7), + "9": (-1, 8), + "0": (-1, 9), + "-": (-1, 10), + "q": (0, 0), + "w": (0, 1), + "e": (0, 2), + "r": (0, 3), + "t": (0, 4), + "y": (0, 5), + "u": (0, 6), + "i": (0, 7), + "o": (0, 8), + "p": (0, 9), + "a": (1, 0), + "s": (1, 1), + "d": (1, 2), + "f": (1, 3), + "g": (1, 4), + "h": (1, 5), + "j": (1, 6), + "k": (1, 7), + "l": (1, 8), + "z": (2, 0), + "x": (2, 1), + "c": (2, 2), + "v": (2, 3), + "b": (2, 4), + "n": (2, 5), + "m": (2, 6), + } + + if global_config.popular_packages_path is not None: + self.popular_packages_path = global_config.popular_packages_path + + def are_neighbors(self, char1: str, char2: str) -> bool: + """Check if two characters are adjacent on a QWERTY keyboard. + + Parameters + ---------- + char1 : str + The first character. + char2 : str + The second character. + + Returns + ------- + bool + True if the characters are neighbors, False otherwise. + """ + c1 = self.keyboard_layout.get(char1) + c2 = self.keyboard_layout.get(char2) + if not c1 or not c2: + return False + return (abs(c1[0] - c2[0]) <= 1) and (abs(c1[1] - c2[1]) <= 1) + + def substitution_func(self, char1: str, char2: str) -> float: + """Calculate the substitution cost between two characters. + + Parameters + ---------- + char1 : str + The first character. + char2 : str + The second character. + + Returns + ------- + float + 0.0 if the characters are the same, `self.keyboard` if they are + neighbors on a QWERTY keyboard, and `self.cost` otherwise. + """ + if char1 == char2: + return 0.0 + if self.keyboard and self.are_neighbors(char1, char2): + return self.keyboard + return self.cost + + def jaro_distance(self, package_name: str, popular_package_name: str) -> float: + """Calculate the Jaro distance between two package names. + + Parameters + ---------- + package_name : str + The name of the package being analyzed. + popular_package_name : str + The name of a popular package to compare against. + + Returns + ------- + float + The Jaro distance between the two package names. + """ + if package_name == popular_package_name: + return 1.0 + + len1, len2 = len(package_name), len(popular_package_name) + if len1 == 0 or len2 == 0: + return 0.0 + + match_distance = max(len1, len2) // 2 - 1 + + package_name_matches = [False] * len1 + popular_package_name_matches = [False] * len2 + matches = 0 + transpositions = 0.0 # Now a float to handle partial costs + + # Count matches + for i in range(len1): + start = max(0, i - match_distance) + end = min(i + match_distance + 1, len2) + for j in range(start, end): + if popular_package_name_matches[j]: + continue + if package_name[i] == popular_package_name[j]: + package_name_matches[i] = True + popular_package_name_matches[j] = True + matches += 1 + break + + if matches == 0: + return 0.0 + + # Count transpositions with possible keyboard awareness + k = 0 + for i in range(len1): + if package_name_matches[i]: + while not popular_package_name_matches[k]: + k += 1 + if package_name[i] != popular_package_name[k]: + transpositions += self.substitution_func(package_name[i], popular_package_name[k]) + k += 1 + + transpositions /= 2.0 # Adjust for transpositions being counted twice + + return (matches / len1 + matches / len2 + (matches - transpositions) / matches) / 3.0 + + def ratio(self, package_name: str, popular_package_name: str) -> float: + """Calculate the Jaro-Winkler distance ratio. + + Parameters + ---------- + package_name : str + The name of the package being analyzed. + popular_package_name : str + The name of a popular package to compare against. + + Returns + ------- + float + The Jaro-Winkler distance ratio, incorporating a prefix bonus + for common initial characters. + """ + scaling = self.scaling + jaro_dist = self.jaro_distance(package_name, popular_package_name) + prefix_length = 0 + max_prefix = 4 + for i in range(min(max_prefix, len(package_name), len(popular_package_name))): + if package_name[i] == popular_package_name[i]: + prefix_length += 1 + else: + break + + return jaro_dist + prefix_length * scaling * (1 - jaro_dist) + + def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: + """Analyze the package. + + Parameters + ---------- + pypi_package_json: PyPIPackageJsonAsset + The PyPI package JSON asset object. + + Returns + ------- + tuple[HeuristicResult, dict[str, JsonType]]: + The result and related information collected during the analysis. + """ + # If there is a popular packages file, check if the package name is similar to any of them + package_name = pypi_package_json.component_name + if not self.popular_packages_path or not os.path.exists(self.popular_packages_path): + err_msg = f"Popular packages file not found or path not configured: {self.popular_packages_path}" + logger.warning("%s. Skipping typosquatting check.", err_msg) + return HeuristicResult.SKIP, {"error": err_msg} + + popular_packages = [] + try: + with open(self.popular_packages_path, encoding="utf-8") as file: + popular_packages = file.read().splitlines() + except OSError as e: + err_msg = f"Could not read popular packages file {self.popular_packages_path}: {e}" + logger.error(err_msg) + return HeuristicResult.SKIP, {"error": err_msg} + + for popular_package in popular_packages: + if package_name == popular_package: + return HeuristicResult.PASS, {"package_name": package_name} + + distance_ratio = self.ratio(package_name, popular_package) + if distance_ratio >= self.distance_ratio_threshold: + logger.info( + "Potential typosquatting detected: '%s' is similar to popular package '%s' (ratio: %.3f)", + package_name, + popular_package, + distance_ratio, + ) + return HeuristicResult.FAIL, { + "package_name": package_name, + "popular_package": popular_package, + "similarity_ratio": distance_ratio, + } + + return HeuristicResult.PASS, {"package_name": package_name} diff --git a/src/macaron/resources/popular_packages.txt b/src/macaron/resources/popular_packages.txt new file mode 100644 index 000000000..02da7afeb --- /dev/null +++ b/src/macaron/resources/popular_packages.txt @@ -0,0 +1,5000 @@ +boto3 +urllib3 +setuptools +botocore +requests +certifi +aiobotocore +charset-normalizer +idna +typing-extensions +packaging +grpcio-status +python-dateutil +s3transfer +s3fs +six +numpy +pyyaml +fsspec +google-api-core +cryptography +pip +pydantic +attrs +cffi +pandas +pycparser +protobuf +markupsafe +jinja2 +rsa +importlib-metadata +pyasn1 +jmespath +aiohttp +platformdirs +click +wheel +pytz +zipp +colorama +pydantic-core +googleapis-common-protos +awscli +cachetools +filelock +pluggy +virtualenv +google-auth +wrapt +tzdata +pyjwt +pyasn1-modules +pytest +jsonschema +tomli +annotated-types +sniffio +pygments +anyio +h11 +iniconfig +psutil +pyarrow +rich +httpx +sqlalchemy +requests-oauthlib +multidict +yarl +grpcio +pyparsing +httpcore +tomlkit +frozenlist +docutils +oauthlib +pathspec +aiosignal +requests-toolbelt +tqdm +pillow +beautifulsoup4 +distlib +werkzeug +google-cloud-storage +more-itertools +exceptiongroup +scipy +greenlet +soupsieve +pyopenssl +deprecated +propcache +rpds-py +et-xmlfile +openpyxl +isodate +trove-classifiers +referencing +decorator +lxml +async-timeout +jsonschema-specifications +python-dotenv +aiohappyeyeballs +proto-plus +msgpack +grpcio-tools +poetry-core +markdown-it-py +google-cloud-core +sortedcontainers +pynacl +mypy-extensions +flask +coverage +websocket-client +mdurl +gitpython +shellingham +azure-core +psycopg2-binary +tenacity +google-resumable-media +asn1crypto +itsdangerous +opentelemetry-api +smmap +regex +bcrypt +gitdb +msal +langsmith +scikit-learn +google-crc32c +dill +keyring +ptyprocess +wcwidth +chardet +pexpect +matplotlib +paramiko +snowflake-connector-python +pyproject-hooks +tabulate +alembic +jeepney +blinker +build +opentelemetry-semantic-conventions +jaraco-classes +opentelemetry-sdk +fastjsonschema +rapidfuzz +networkx +importlib-resources +huggingface-hub +cloudpickle +starlette +fastapi +secretstorage +threadpoolctl +typedload +google-cloud-bigquery +backoff +ruamel-yaml +joblib +kiwisolver +google-api-python-client +sqlparse +fonttools +asgiref +dnspython +pkginfo +prompt-toolkit +cycler +google-auth-oauthlib +py4j +httplib2 +defusedxml +pyzmq +xmltodict +uritemplate +poetry-plugin-export +types-requests +docker +uvicorn +cachecontrol +grpc-google-iam-v1 +google-auth-httplib2 +pytest-cov +azure-identity +gunicorn +marshmallow +awswrangler +installer +azure-storage-blob +msal-extensions +babel +cython +langchain +openai +poetry +redis +distro +tzlocal +contourpy +ruamel-yaml-clib +ipython +toml +setuptools-scm +dulwich +isort +crashtest +black +nest-asyncio +mccabe +jaraco-functools +pycodestyle +traitlets +cleo +jaraco-context +transformers +hatchling +jedi +opentelemetry-proto +py +zstandard +jsonpointer +pymysql +parso +websockets +typer +sentry-sdk +tornado +prometheus-client +markdown +matplotlib-inline +kubernetes +mako +webencodings +pendulum +opentelemetry-exporter-otlp-proto-common +termcolor +types-python-dateutil +python-json-logger +asttokens +executing +orjson +mypy +tokenizers +ruff +pyrsistent +opentelemetry-exporter-otlp-proto-http +aiofiles +stack-data +pure-eval +typing-inspect +arrow +ply +multiprocess +sympy +nodeenv +pycryptodome +future +argcomplete +torch +pymongo +rich-toolkit +opentelemetry-exporter-otlp-proto-grpc +shapely +pygithub +google-cloud-pubsub +smart-open +datadog +google-cloud-secret-manager +scramp +debugpy +snowflake-sqlalchemy +pytest-xdist +pycryptodomex +mpmath +opentelemetry-instrumentation +mysql-connector-python +aioitertools +python-slugify +identify +portalocker +jsonpatch +lz4 +pyflakes +pre-commit +backports-tarfile +cfgv +jupyter-core +jiter +jupyter-client +pyspark +requests-aws4auth +setproctitle +watchdog +slack-sdk +ipykernel +execnet +requests-file +opensearch-py +msrest +jupyterlab +croniter +langchain-core +thriftpy2 +rfc3339-validator +semver +comm +flake8 +jsonpath-ng +azure-common +opentelemetry-util-http +tinycss2 +bleach +redshift-connector +pytest-asyncio +mistune +cattrs +typeguard +tiktoken +nbformat +nbconvert +colorlog +opentelemetry-exporter-otlp +notebook +pytest-mock +elasticsearch +text-unidecode +google-cloud-aiplatform +nbclient +zope-interface +uv +pytzdata +xlsxwriter +jupyter-server +google-cloud-resource-manager +db-dtypes +editables +safetensors +google-cloud-appengine-logging +dataclasses-json +toolz +tb-nightly +pandas-gbq +overrides +pylint +nltk +pg8000 +gcsfs +astroid +argon2-cffi +pydantic-settings +email-validator +databricks-sql-connector +humanfriendly +google-pasta +argon2-cffi-bindings +python-multipart +pbs-installer +sphinx +findpython +lazy-object-proxy +pysocks +types-pyyaml +xlrd +docstring-parser +altair +ordered-set +deepdiff +pandocfilters +jupyterlab-server +jupyterlab-pygments +simplejson +absl-py +nvidia-cublas-cu12 +json5 +retry +wsproto +opentelemetry-instrumentation-requests +seaborn +google-cloud-logging +selenium +nvidia-cusparse-cu12 +apache-airflow-providers-common-sql +pkgutil-resolve-name +imageio +time-machine +structlog +nvidia-nvjitlink-cu12 +tblib +nvidia-cudnn-cu12 +watchfiles +nvidia-cufft-cu12 +nvidia-cuda-cupti-cu12 +uvloop +aenum +durationpy +nvidia-cuda-nvrtc-cu12 +nvidia-curand-cu12 +webcolors +nvidia-cusolver-cu12 +nvidia-cuda-runtime-cu12 +oscrypto +pydata-google-auth +xgboost +responses +send2trash +tensorboard +faker +mdit-py-plugins +schema +pbr +httptools +google-cloud-audit-log +graphql-core +google-cloud-dataproc +ipywidgets +fqdn +terminado +isoduration +nvidia-nccl-cu12 +uri-template +async-lru +numba +sagemaker +sentencepiece +widgetsnbextension +flatbuffers +rfc3986-validator +dbt-core +trio +jupyterlab-widgets +notebook-shim +jupyter-events +datasets +appdirs +llvmlite +narwhals +sshtunnel +pyodbc +databricks-sdk +libcst +plotly +progressbar2 +mock +triton +dacite +coloredlogs +inflection +google-cloud-vision +xxhash +jupyter-server-terminals +h5py +thrift +azure-storage-file-datalake +deprecation +aws-lambda-powertools +tensorflow +flask-caching +django +confluent-kafka +oauth2client +outcome +google-cloud-spanner +rfc3986 +python-utils +fastavro +semantic-version +jupyter-lsp +dbt-adapters +linkify-it-py +types-protobuf +azure-keyvault-secrets +pipenv +hpack +google-cloud-bigquery-storage +pathos +python-daemon +pymssql +adal +google-cloud-dlp +great-expectations +delta-spark +antlr4-python3-runtime +hyperframe +google-cloud-monitoring +looker-sdk +fastapi-cli +pox +apache-airflow-providers-snowflake +h2 +gym-notices +ppft +types-pytz +retrying +loguru +google-cloud-kms +tox +html5lib +trio-websocket +apache-airflow +nvidia-nvtx-cu12 +langchain-community +grpcio-health-checking +google-cloud-bigtable +flask-wtf +google-cloud-tasks +click-plugins +gast +omegaconf +apache-airflow-providers-mysql +psycopg2 +zeep +apache-airflow-providers-ssh +snowballstemmer +smdebug-rulesconfig +docker-pycreds +apache-airflow-providers-cncf-kubernetes +google-ads +prettytable +sqlalchemy-bigquery +aws-requests-auth +apache-airflow-providers-google +tldextract +kombu +google-cloud-datacatalog +opencv-python +boto3-stubs +google-cloud-bigquery-datatransfer +google-cloud-container +universal-pathlib +wandb +types-setuptools +brotli +botocore-stubs +freezegun +unidecode +google-cloud-translate +apache-airflow-providers-databricks +google-cloud-firestore +yamllint +lockfile +twine +torchvision +google-cloud-language +google-cloud-videointelligence +google-cloud-workflows +hvac +entrypoints +amqp +google-cloud-redis +google-cloud-build +google-cloud-automl +hypothesis +types-awscrt +google-cloud-dataplex +statsmodels +gcloud-aio-storage +gcloud-aio-auth +vine +gspread +google-cloud-os-login +google-cloud-speech +google-cloud-memcache +authlib +pybind11 +google-cloud-orchestration-airflow +google-cloud-compute +langchain-text-splitters +rich-argparse +bs4 +google-cloud-dataproc-metastore +types-s3transfer +google-cloud-dataform +flask-login +yapf +mashumaro +cached-property +kafka-python +asynctest +ujson +aiosqlite +agate +mergedeep +pytimeparse +google-cloud-texttospeech +types-urllib3 +celery +gcloud-aio-bigquery +gevent +azure-mgmt-core +grpcio-gcp +pytest-rerunfailures +monotonic +patsy +alabaster +pycountry +marshmallow-oneofschema +argparse +billiard +pytest-metadata +sphinxcontrib-serializinghtml +ecdsa +astronomer-cosmos +graphviz +ninja +gremlinpython +imagesize +nh3 +pip-tools +moto +mlflow +sphinxcontrib-htmlhelp +sphinxcontrib-applehelp +sphinxcontrib-qthelp +ijson +sphinxcontrib-devhelp +spacy +tensorboard-data-server +flask-cors +pydeequ +pywin32 +humanize +flit-core +psycopg +pickleshare +simple-salesforce +click-didyoumean +imbalanced-learn +opt-einsum +keras +flask-session +requests-mock +scikit-image +readme-renderer +ddtrace +backcall +mlflow-skinny +mysqlclient +sphinxcontrib-jsmath +makefun +zope-event +onnxruntime +docopt +parsedatetime +click-repl +databricks-cli +duckdb +dbt-common +blis +tensorflow-estimator +mypy-boto3-s3 +graphene +thinc +graphql-relay +opentelemetry-instrumentation-asgi +sqlalchemy-jsonfield +google-cloud-dataflow-client +bytecode +mmh3 +pytest-timeout +preshed +catalogue +avro-python3 +srsly +ipython-pygments-lexers +configupdater +envier +wasabi +opentelemetry-instrumentation-fastapi +langcodes +cron-descriptor +google-cloud-run +python-gitlab +jsonpickle +cymem +python-jose +murmurhash +leather +langchain-google-vertexai +commonmark +texttable +emoji +bitarray +cramjam +pypdf +langchain-openai +jira +dbt-extractor +spacy-legacy +ml-dtypes +google-cloud-storage-transfer +astunparse +spacy-loggers +pypdf2 +apache-beam +lark +pytest-runner +sqlalchemy-spanner +typing-inspection +confection +dbt-semantic-interfaces +google-cloud-batch +jpype1 +dask +python-http-client +pyproject-api +flask-sqlalchemy +uc-micro-py +asyncpg +py-cpuinfo +msrestazure +levenshtein +python-magic +polars +sendgrid +events +sqlalchemy-utils +anthropic +diskcache +accelerate +marisa-trie +elastic-transport +phonenumbers +djangorestframework +pytest-html +python-gnupg +azure-datalake-store +stevedore +cloudpathlib +types-redis +dateparser +apispec +pysftp +pyroaring +watchtower +opencv-python-headless +nvidia-cusparselt-cu12 +cssselect +astor +fasteners +libclang +azure-cosmos +rich-click +holidays +language-data +configparser +grpc-interceptor +azure-mgmt-resource +cmake +validators +aws-xray-sdk +wtforms +lightgbm +eval-type-backport +gym +httpx-sse +psycopg-binary +datetime +pyproj +parameterized +hyperlink +datadog-api-client +azure-storage-queue +avro +pyee +jaydebeapi +bracex +tomli-w +opencensus +azure-servicebus +einops +inflect +cfn-lint +invoke +opencensus-context +resolvelib +streamlit +pyathena +office365-rest-python-client +dataclasses +apache-airflow-providers-http +parse +types-paramiko +jupyter-console +flask-appbuilder +torchaudio +jupyter +tensorflow-io-gcs-filesystem +passlib +playwright +filetype +microsoft-kiota-http +lazy-loader +pydub +mypy-boto3-rds +markdownify +retryhttp +pydot +id +jax +tifffile +cachelib +pymdown-extensions +fuzzywuzzy +gradio +torchmetrics +sqlglot +google-analytics-admin +apscheduler +limits +posthog +tensorflow-serving-api +backports-zoneinfo +python-docx +azure-storage-file-share +microsoft-kiota-authentication-azure +snowflake-snowpark-python +openapi-spec-validator +sentence-transformers +ua-parser +types-tabulate +google-re2 +junitparser +pymupdf +ratelimit +pyright +connexion +oracledb +userpath +django-cors-headers +wcmatch +azure-kusto-data +junit-xml +gql +pytorch-lightning +configargparse +click-option-group +pyspnego +apache-airflow-providers-sqlite +types-deprecated +pydeck +pdfminer-six +fire +cfn-flip +pycrypto +fastparquet +azure-mgmt-storage +nose +pyotp +json-merge-patch +boto +xarray +lightning-utilities +iso8601 +contextlib2 +google +tableauserverclient +kfp +pywavelets +python-levenshtein +apache-airflow-providers-slack +starkbank-ecdsa +prefect +opentelemetry-instrumentation-wsgi +pipdeptree +kubernetes-asyncio +pandas-stubs +cssselect2 +timm +partd +locket +jsonref +aioresponses +aiohttp-retry +mkdocs-material +slicer +azure-keyvault-keys +lbprodrun +stripe +azure-nspkg +pandera +pytest-random-order +geographiclib +opencensus-ext-azure +apache-airflow-providers-ftp +ray +geopy +yandexcloud +shap +pyserial +pep517 +netaddr +factory-boy +textual +typing +types-aiofiles +geopandas +aniso8601 +checkov +jellyfish +weasel +msgspec +azure-storage-common +jsonlines +keras-applications +frozendict +tensorflow-text +openapi-schema-validator +natsort +iso3166 +orderly-set +flask-jwt-extended +ddsketch +aws-sam-translator +marshmallow-sqlalchemy +ansible-core +types-docutils +h3 +faiss-cpu +pycares +types-markdown +albumentations +aiodns +weaviate-client +boltons +reportlab +service-identity +jsondiff +ldap3 +enum34 +flask-limiter +onnx +slackclient +incremental +ansible +litellm +aliyun-python-sdk-core +querystring-parser +maxminddb +mkdocs +ciso8601 +meson +cloudevents +ipython-genutils +jaxlib +autopep8 +types-dataclasses +azure-keyvault +sql-metadata +django-filter +scp +azure-keyvault-certificates +protobuf3-to-dict +bandit +deltalake +keyrings-google-artifactregistry-auth +diracx-core +magicattr +immutabledict +types-croniter +opentelemetry-instrumentation-flask +binaryornot +sphinx-rtd-theme +twisted +ftfy +apache-airflow-providers-fab +python-nvd3 +ghp-import +opentelemetry-instrumentation-urllib3 +pytest-env +analytics-python +oldest-supported-numpy +hiredis +pyyaml-env-tag +strictyaml +pywin32-ctypes +bottle +langdetect +pika +pytest-localserver +opentelemetry-instrumentation-dbapi +logbook +twilio +azure-mgmt-containerregistry +asyncio +apache-airflow-providers-imap +statsd +sh +geoip2 +tree-sitter +llama-parse +typed-ast +pathy +flask-babel +ultralytics +rdflib +beartype +pydash +opentelemetry-instrumentation-urllib +cloudformation-cli +types-pymysql +gradio-client +cloudformation-cli-python-plugin +awscrt +mypy-boto3-sqs +methodtools +azure-mgmt-datafactory +diff-cover +cloudformation-cli-java-plugin +cloudformation-cli-go-plugin +cloudformation-cli-typescript-plugin +optree +firebase-admin +webdriver-manager +crcmod +mypy-boto3-glue +azure-eventhub +pyperclip +soundfile +requests-ntlm +aioboto3 +apache-airflow-providers-docker +minimal-snowplow-tracker +google-cloud +av +pytest-django +daff +fabric +mkdocs-material-extensions +clickhouse-connect +constructs +bidict +trino +pydantic-extra-types +mkdocstrings-python +unicodecsv +wirerope +marshmallow-enum +meson-python +automat +sagemaker-core +applicationinsights +pyproject-metadata +constantly +opentelemetry-instrumentation-psycopg2 +waitress +functions-framework +dash +chroma-hnswlib +cookiecutter +fs +ipdb +strenum +cligj +qrcode +opentelemetry-instrumentation-django +azure-mgmt-cosmosdb +pytest-forked +pyrfc3339 +namex +azure-mgmt-compute +towncrier +mypy-protobuf +azure-mgmt-containerinstance +mypy-boto3-dynamodb +pytest-split +ua-parser-builtins +jwcrypto +azure-data-tables +snowplow-tracker +adlfs +face +glom +parse-type +griffe +ndg-httpsclient +pdf2image +pathlib +dirac +mkdocs-get-deps +types-pyopenssl +azure-mgmt-keyvault +gensim +django-storages +apache-airflow-providers-smtp +numexpr +pathable +opentelemetry-distro +azure-mgmt-authorization +types-cachetools +sphinxcontrib-jquery +pyhcl +fakeredis +google-ai-generativelanguage +prison +django-extensions +gsutil +python-socketio +hatch-vcs +mypy-boto3-lambda +tf-keras-nightly +python-engineio +eth-account +minio +google-generativeai +unstructured-client +llama-index +apache-airflow-providers-common-compat +clickclick +configobj +swagger-ui-bundle +mypy-boto3-secretsmanager +appnope +stringcase +fasttext-wheel +azure-batch +dpath +setuptools-rust +paginate +pathlib2 +azure-monitor-opentelemetry-exporter +llama-index-core +azure-devops +ffmpy +kfp-pipeline-spec +peewee +elasticsearch-dsl +jsii +dbt-snowflake +pyarrow-hotfix +pyphen +webob +azure-mgmt-network +hydra-core +python-pptx +pyaml +azure-graphrbac +python-decouple +pkce +langchain-google-community +autograd +shortuuid +blessed +opentelemetry-instrumentation-logging +user-agents +nvidia-ml-py +atomicwrites +pipx +azure-kusto-ingest +xyzservices +addict +pytest-randomly +elementpath +dask-expr +futures +pyelftools +simple-websocket +types-toml +pdbr +atlassian-python-api +jsonschema-path +ipaddress +pooch +pymsteams +toposort +amazon-ion +teradatasql +evergreen-py +unittest-xml-reporting +pathlib-abc +pgvector +html2text +hatch +apache-airflow-providers-common-io +geomet +uritools +semgrep +types-cffi +azure-mgmt-containerservice +publication +hf-transfer +pathvalidate +codeowners +pkgconfig +langgraph +openlineage-python +fixedint +cog +fiona +py-spy +apache-airflow-providers-amazon +expiringdict +mypy-boto3-cloudformation +dictdiffer +recordlinkage +singer-sdk +schedule +spark-nlp +motor +distributed +boolean-py +funcsigs +xmlschema +pgpy +mypy-boto3-ec2 +llama-index-indices-managed-llama-cloud +mypy-boto3-appflow +uamqp +pypika +weasyprint +apache-airflow-providers-microsoft-mssql +async-generator +bokeh +cx-oracle +azure-mgmt-datalake-store +whitenoise +feedparser +sqlmodel +license-expression +python-snappy +pytest-json-report +django-redis +pypdfium2 +packageurl-python +unidiff +questionary +click-default-group +timezonefinder +xlwt +cmdstanpy +sqlalchemy-redshift +tokenize-rt +azure-mgmt-monitor +aws-cdk-asset-awscli-v1 +prometheus-fastapi-instrumentator +dbt-postgres +genson +optuna +launchdarkly-server-sdk +py-partiql-parser +locust +kaleido +hdfs +prophet +python-jenkins +python3-openid +aws-cdk-integ-tests-alpha +cyclonedx-python-lib +marshmallow-dataclass +msgraph-core +psycopg-pool +hexbytes +azure-mgmt-redis +altgraph +grpcio-reflection +azure-mgmt-rdbms +azure-mgmt-web +cerberus +clickhouse-driver +nvidia-cublas-cu11 +sqlfluff +colorful +colour +azure-mgmt-sql +datasketch +patchelf +cassandra-driver +socksio +flower +kfp-server-api +enum-compat +deepmerge +olefile +openlineage-integration-common +eth-rlp +geventhttpclient +atpublic +lxml-html-clean +pydocstyle +asyncssh +azure-appconfiguration +aiohttp-cors +django-debug-toolbar +azure-monitor-opentelemetry +azure-mgmt-dns +nvidia-cudnn-cu11 +acryl-datahub +myst-parser +mypy-boto3-sts +azure-core-tracing-opentelemetry +pyogrio +azure-mgmt-servicebus +blobfile +types-six +convertdate +pprintpp +uuid +google-cloud-datastore +pyhumps +azure-mgmt-eventhub +azure-mgmt-msi +ffmpeg-python +cohere +python-telegram-bot +azure-mgmt-cdn +opentelemetry-resource-detector-azure +notion-client +truststore +pyinstaller-hooks-contrib +google-cloud-bigquery-biglake +dynamodb-json +readchar +robotframework +svgwrite +azure-mgmt-loganalytics +azure-monitor-query +orbax-checkpoint +datamodel-code-generator +behave +langgraph-checkpoint +azure-mgmt-cognitiveservices +azure-mgmt-search +pyinstaller +pdfplumber +azure-mgmt-managementgroups +librosa +chevron +fpdf +xformers +roman-numerals-py +azure-mgmt-batch +zopfli +maturin +nvidia-cuda-runtime-cu11 +formulaic +cytoolz +peft +djangorestframework-simplejwt +nbclassic +pyiceberg +nvidia-cuda-nvrtc-cu11 +pytest-repeat +xmlsec +chromadb +qdrant-client +azure-mgmt-trafficmanager +drf-spectacular +llama-index-llms-openai +apache-flink +nox +yq +azure-mgmt-marketplaceordering +pydyf +jwt +environs +azure-mgmt-applicationinsights +unearth +py-serializable +azure-mgmt-nspkg +w3lib +korean-lunar-calendar +dependency-groups +web3 +pemja +azure-mgmt-iothub +azure-mgmt-recoveryservicesbackup +azure-mgmt-recoveryservices +interface-meta +diffusers +django-environ +python-crontab +apache-flink-libraries +prometheus-flask-exporter +imageio-ffmpeg +bitsandbytes +azure-mgmt-advisor +azure-mgmt-eventgrid +vcrpy +flit +db-contrib-tool +knack +memray +requirements-parser +mkdocstrings +flask-restful +sse-starlette +smbprotocol +catboost +uuid6 +langgraph-sdk +azure-cosmosdb-table +urwid +pynamodb +keras-preprocessing +eth-hash +tensorboard-plugin-wit +nested-lookup +allure-python-commons +types-jsonschema +pdpyras +autoflake +azure-mgmt-media +azure-mgmt-servicefabric +azure-mgmt-billing +azure-mgmt-policyinsights +azure-cosmosdb-nspkg +sklearn +dockerfile-parse +num2words +azure-mgmt-iothubprovisioningservices +azure-mgmt-batchai +dunamai +azure-synapse-artifacts +azure-mgmt-signalr +pygit2 +azure-mgmt-datalake-nspkg +azure-mgmt-datamigration +azure-mgmt-maps +mypy-boto3-redshift-data +azure-mgmt-iotcentral +stanio +pypandoc +mongomock +opentelemetry-instrumentation-sqlalchemy +s3path +eth-utils +pyzipper +testcontainers +rustworkx +influxdb +sphinx-autodoc-typehints +ultralytics-thop +pulumi +hatch-fancy-pypi-readme +opentelemetry-instrumentation-grpc +voluptuous +mixpanel +python-box +pikepdf +pytz-deprecation-shim +proglog +pre-commit-uv +geojson +netifaces +strip-hints +lifelines +mkdocs-autorefs +hishel +pytesseract +cssutils +terminaltables +inputimeout +memory-profiler +aws-cdk-lib +python3-saml +parsimonious +flask-migrate +zipfile38 +evaluate +pathlib-mate +pinotdb +google-cloud-pubsublite +apache-airflow-providers-sftp +yt-dlp +aws-psycopg2 +haversine +pytest-custom-exit-code +orderedmultidict +databricks-api +simpleeval +zstd +pyhocon +yfinance +google-analytics-data +flake8-bugbear +hjson +types-pillow +pyxlsb +qtpy +anytree +pytest-benchmark +eth-typing +cloudflare +url-normalize +objsize +paho-mqtt +django-timezone-field +moviepy +influxdb-client +dicttoxml +furl +pyhanko +dash-core-components +inject +lightning +azure-functions +databricks-connect +microsoft-kiota-serialization-text +audioread +fake-useragent +aiokafka +appium-python-client +rollbar +pystache +microsoft-kiota-abstractions +tensorflow-metadata +dash-html-components +temporalio +pyhive +dash-table +azure-search-documents +sqlglotrs +sphinx-copybutton +backrefs +supervisor +allure-pytest +pyreadline3 +dirtyjson +sqlparams +python-socks +azure-storage-file +github-heatmap +facebook-business +plotnine +multimethod +microsoft-kiota-serialization-json +pyinstrument +json-repair +tensorboardx +bc-detect-secrets +cbor2 +flaky +opentelemetry-instrumentation-redis +python-bidi +dm-tree +striprtf +dep-logic +scikit-build-core +requests-aws-sign +pdm +thrift-sasl +azureml-core +ansible-compat +azure-synapse-spark +aiorwlock +opentelemetry-instrumentation-aiohttp-client +types-certifi +oss2 +asana +pmdarima +azure-eventgrid +launchdarkly-eventsource +tablib +torchsde +unstructured +vllm +reactivex +types-psycopg2 +poetry-dynamic-versioning +mizani +pymeeus +pex +sphinx-design +opentelemetry-instrumentation-botocore +qtconsole +pyzstd +funcy +arpeggio +pytest-ordering +trampoline +spdx-tools +markdown2 +jdcal +types-html5lib +thefuzz +pypiwin32 +construct +pytest-order +piexif +django-celery-beat +pytest-sugar +aiomultiprocess +pypng +py7zr +newrelic +rasterio +jsonpath-python +syrupy +ollama +moreorless +pycocotools +python-crfsuite +apache-airflow-microsoft-fabric-plugin +opentelemetry-instrumentation-httpx +datefinder +langchain-aws +gprof2dot +cleanco +dependency-injector +openlineage-airflow +simple-gcp-object-downloader +zope-deprecation +policy-sentry +dynaconf +parver +cloudsplaining +multipledispatch +pytest-socket +opencv-contrib-python +pycep-parser +pytest-base-url +icdiff +eth-keys +hupper +kaitaistruct +python-consul +llama-index-readers-file +hyperopt +neo4j +aws-cdk-asset-node-proxy-agent-v6 +bc-python-hcl2 +eth-abi +pypyp +pastedeploy +azure-cli-core +llama-index-agent-openai +puremagic +webargs +rlp +repoze-lru +pyqt5-sip +boxsdk +instructor +llama-index-readers-llama-parse +azure-mgmt-devtestlabs +pyqt5 +dbt-spark +bc-jsonpath-ng +tf-keras +icalendar +pastel +sgmllib3k +eth-keyfile +dj-database-url +django-stubs-ext +promise +kornia +drf-yasg +std-uritemplate +scandir +editorconfig +requests-sigv4 +zict +mbstrdecoder +open-clip-torch +pytest-subtests +llama-index-embeddings-openai +aiocache +multi-key-dict +opentelemetry-propagator-aws-xray +concurrent-log-handler +pygeohash +jq +trailrunner +langfuse +graphframes +gguf +o365 +stdlibs +pycurl +hijri-converter +langchain-anthropic +soxr +osqp +types-simplejson +venusian +pytest-icdiff +typepy +usort +databricks-pypi1 +requests-cache +types-python-slugify +llama-index-program-openai +versioneer +lm-format-enforcer +fastcore +django-stubs +yaspin +lupa +boto-session-manager +sentinels +ec2-metadata +mistral-common +pyppmd +cheroot +jsbeautifier +python-editor +pybcj +affine +llama-index-multi-modal-llms-openai +ufmt +s3pathlib +pyhamcrest +iterproxy +tld +google-apitools +outlines +sounddevice +pydata-sphinx-theme +mypy-boto3-ssm +translationstring +comtypes +llama-index-cli +sacrebleu +eventlet +llama-cloud-services +pdm-backend +simsimd +curlify +types-psutil +multitasking +parsel +apprise +apache-airflow-providers-postgres +multivolumefile +findspark +pytest-messenger +pyfakefs +pefile +dbt-databricks +llama-index-question-gen-openai +papermill +base58 +pymemcache +redis-py-cluster +compressed-tensors +pygsheets +bazel-runfiles +publicsuffix2 +mypy-boto3-iam +func-args +sqlalchemy2-stubs +folium +ckzg +python-arango +pynndescent +chispa +pyaes +trimesh +channels +dagster +azure-mgmt-subscription +munch +branca +async-property +clickhouse-sqlalchemy +braceexpand +llama-cloud +types-mock +types-beautifulsoup4 +weread2notionpro +pyramid +umap-learn +types-retry +pyyaml-include +interegular +expandvars +json-log-formatter +wget +lru-dict +diff-match-patch +joserfc +plaster +pyfiglet +plaster-pastedeploy +google-cloud-recommendations-ai +prefect-aws +pymongo-auth-aws +hologram +xgrammar +plumbum +pymupdfb +scapy +aws-cdk-cloud-assembly-schema +asyncer +opentelemetry-exporter-prometheus +pulumi-aws +pygame +subprocess-tee +bitstring +snowflake +rtree +google-genai +snowflake-core +bashlex +python-hcl2 +dbt-bigquery +casefy +python-rapidjson +conan +requests-futures +etils +injector +txaio +tensorflow-hub +boto3-type-annotations +types-freezegun +pep8-naming +pillow-heif +dbutils +aio-pika +flask-httpauth +checkdigit +polling +pybloom-live +click-spinner +pyqt5-qt5 +auth0-python +autobahn +pytest-playwright +django-appconf +pylint-plugin-utils +cairosvg +inquirer +singledispatch +aiolimiter +opsgenie-sdk +tweepy +pyquery +javaproperties +soda-core-spark-df +googlemaps +bottleneck +pyusb +boa-str +pykwalify +pywinpty +cvxpy +onnxruntime-gpu +azure-cli +safehttpx +azure-cli-telemetry +inflate64 +cli-exit-tools +rq +cairocffi +kazoo +lib-detect-testenv +dagster-pipes +netcdf4 +langchain-experimental +soda-core-spark +azure-mgmt-datalake-analytics +pdfkit +sacremoses +fpdf2 +pamqp +social-auth-core +jproperties +realtime +wordcloud +azure-mgmt-apimanagement +speechrecognition +partial-json-parser +youtube-transcript-api +dbt-redshift +testfixtures +azure-mgmt-privatedns +django-js-asset +avro-gen3 +cdk-nag +django-model-utils +gotrue +openxlab +ifaddr +azure +lmdb +priority +ably +prance +flask-restx +patch-ng +imagehash +autograd-gamma +slack-bolt +aiofile +pint +discord-py +arviz +pympler +decord +pyclipper +pytest-instafail +ip3country +cftime +gcovr +codespell +types-aiobotocore +immutables +azure-mgmt-hdinsight +ansible-lint +azure-mgmt-reservations +pyairtable +pure-sasl +flatten-json +category-encoders +pinecone-plugin-interface +grpclib +sseclient-py +shtab +giturlparse +mypy-boto3-apigateway +groq +yamale +wrapt-timeout-decorator +asgi-lifespan +caio +respx +vulture +django-phonenumber-field +gdown +python-keycloak +pyramid-debugtoolbar +dagster-graphql +mitmproxy +virtualenv-clone +tensorstore +pyramid-mako +pulp +scs +azure-loganalytics +premailer +aioquic +flask-bcrypt +pycomposefile +pymilvus +azure-mgmt-security +aws-cdk-asset-kubectl-v20 +aws-secretsmanager-caching +circuitbreaker +pylsqpack +pytest-unordered +azure-mgmt-synapse +mypy-boto3-kinesis +pytest-dotenv +safety +microsoft-kiota-serialization-multipart +j2cli +django-celery-results +mysql-connector +restructuredtext-lint +azure-mgmt-consumption +supabase +pip-requirements-parser +dbl-tempo +gspread-dataframe +htmldate +python-ldap +pyramid-jinja2 +types-tqdm +microsoft-kiota-serialization-form +port-for +databricks-pypi2 +snowflake-legacy +python-xlib +albucore +lit +pulumi-command +social-auth-app-django +probableparsing +sarif-om +pyunormalize +usaddress +types-aiobotocore-s3 +commentjson +jschema-to-python +python-iso639 +tecton +mistralai +rfc3987 +timeout-decorator +pynvml +accessible-pygments +structlog-sentry +rembg +riot +confuse +kornia-rs +types-pygments +azure-mgmt-appconfiguration +pinecone-client +impyla +backports-functools-lru-cache +apache-airflow-providers-mongo +node-semver +oci +mypy-boto3-stepfunctions +tensorflow-datasets +pulumi-tls +azure-multiapi-storage +azure-mgmt-relay +azure-mgmt-netapp +azure-synapse-accesscontrol +azure-mgmt-sqlvirtualmachine +azure-mgmt-redhatopenshift +robotframework-pythonlibcore +types-cryptography +curl-cffi +supafunc +blake3 +supervision +mleap +zeroconf +postgrest +vertica-python +ortools +yacs +tox-uv +mirakuru +azure-mgmt-botservice +aiomysql +types-ujson +aiormq +flask-openid +sphinx-autobuild +azure-mgmt-notificationhubs +flashtext +webdataset +openlineage-sql +pywinrm +mitmproxy-rs +storage3 +azure-mgmt-databoxedge +daphne +soda-core +iopath +apache-sedona +azure-synapse-managedprivateendpoints +azure-keyvault-administration +requests-unixsocket +azure-mgmt-extendedlocation +pywinauto +fasttext +azure-mgmt-imagebuilder +crossplane +pymatting +salesforce-bulk +azure-mgmt-servicefabricmanagedclusters +azure-mgmt-appcontainers +cloud-sql-python-connector +natto-py +hypercorn +azure-mgmt-servicelinker +filterpy +dataproperty +tensorflowjs +strawberry-graphql +flake8-docstrings +mypy-boto3-ecr +tree-sitter-python +python-vagrant +azure-mgmt-logic +polyfactory +koalas +leb128 +multipart +ddapm-test-agent +docker-compose +mypy-boto3-athena +biopython +pytest-check +apache-airflow-providers-jdbc +editdistance +pip-api +segment-analytics-python +astropy +polib +arabic-reshaper +cssbeautifier +mammoth +types-markupsafe +tabledata +uwsgi +cobble +cmd2 +azure-servicefabric +pybuildkite +django-crispy-forms +tcolorpy +pytablewriter +presidio-analyzer +types-jinja2 +robotframework-requests +clarabel +lakefs-sdk +sphinxcontrib-mermaid +embedchain +pyandoc +robotframework-seleniumlibrary +opencv-contrib-python-headless +hubspot-api-client +stepfunctions +groovy +azure-mgmt +aliyun-python-sdk-kms +azure-schemaregistry +azure-mgmt-powerbiembedded +pyudev +asynch +azure-mgmt-commerce +azure-mgmt-scheduler +ansicolors +pyluach +hdbcli +mangum +azure-mgmt-hanaonazure +djlint +azure-mgmt-machinelearningcompute +azure-mgmt-managementpartner +mcp +xsdata +gymnasium +scrapy +protego +django-ipware +adapters +tzfpy +poetry-plugin-pypi-mirror +azure-ai-ml +azure-servicemanagement-legacy +ghapi +qdldl +ecs-logging +dataclass-wizard +pytest-httpx +pyhanko-certvalidator +flatdict +pytest-assume +zarr +pygtrie +pipelinewise-singer-python +flax +azure-mgmt-devspaces +opentelemetry-sdk-extension-aws +azure-ai-documentintelligence +queuelib +xhtml2pdf +mmcif +shrub-py +a2wsgi +poethepoet +ibm-db +signxml +ydata-profiling +statsig +cliff +types-boto3 +bump2version +azure-applicationinsights +pysmi +langchain-google-genai +biotite +pytest-httpserver +anyascii +coreapi +oras +django-import-export +python-ulid +smartsheet-python-sdk +attrdict +pyppeteer +sqlfluff-templater-dbt +pydispatcher +stringzilla +sqlalchemy-stubs +django-simple-history +pyspark-dist-explore +portpicker +json-delta +logzero +dlt +apache-airflow-providers-microsoft-azure +itypes +bitstruct +msoffcrypto-tool +tableauhyperapi +dohq-artifactory +docx2txt +pandasql +backports-weakref +svglib +dagster-postgres +igraph +dparse +python-pam +win32-setctime +channels-redis +clang-format +modin +dagster-webserver +geoalchemy2 +colorclass +marshmallow-jsonschema +mando +xmod +country-converter +shareplum +google-cloud-trace +ctranslate2 +numcodecs +zenpy +dataclasses-avroschema +jsonconversion +jsonpath-rw +python-can +blosc2 +editor +memoization +runs +seleniumbase +nvidia-cusolver-cu11 +pinecone +duckduckgo-search +mongoengine +ansi2html +flexparser +flask-compress +pytest-bdd +msgraph-sdk +nvidia-cuda-cupti-cu11 +flexcache +nvidia-curand-cu11 +furo +nvidia-cusparse-cu11 +tree-sitter-javascript +radon +python-stdnum +itemadapter +nvidia-cufft-cu11 +pusher +types-boto +firecrawl-py +opentelemetry-instrumentation-sqlite3 +mypy-boto3-logs +keystoneauth1 +microsoft-security-utilities-secret-masker +algoliasearch +sudachipy +aws-cdk-aws-lambda-python-alpha +dagster-aws +pytimeparse2 +hyperpyyaml +about-time +testpath +flake8-isort +click-log +ndjson +mypy-boto3-xray +grpc-stubs +itemloaders +nvidia-nvtx-cu11 +mypy-boto3-schemas +xmlrunner +backports-datetime-fromisoformat +jinja2-simple-tags +pycairo +flask-marshmallow +c7n +mypy-boto3-signer +flatten-dict +alive-progress +teradatasqlalchemy +tritonclient +speechbrain +autopage +intelhex +sshpubkeys +opentracing +cached-path +segment-anything +nanoid +flake8-comprehensions +dogpile-cache +farama-notifications +find-libpython +deptry +types-click +azure-mgmt-postgresqlflexibleservers +pylev +pytest-postgresql +devtools +mypy-boto3-kms +pylint-django +opentelemetry-instrumentation-jinja2 +azure-mgmt-mysqlflexibleservers +diracx-client +rope +pylance +pyahocorasick +lark-parser +textblob +nvidia-nccl-cu11 +mediapipe +tinyhtml5 +sigtools +mypy-boto3-sns +coolname +awkward +opentelemetry-instrumentation-threading +backports-tempfile +aiogram +cadwyn +opentelemetry-instrumentation-asyncio +aws-encryption-sdk +ibm-cloud-sdk-core +logging-azure-rest +vertexai +h5netcdf +pyrate-limiter +databricks +pytest-profiling +spandrel +python-baseconv +flask-shell-ipython +suds-community +trl +sphinx-basic-ng +faster-whisper +awkward-cpp +mutagen +airportsdata +github3-py +ccxt +inflector +jaconv +xattr +webtest +sanic +chex +aws-sam-cli +lancedb +retry2 +thop +fastrlock +odfpy +starlette-context +azureml-dataprep +jupytext +prefect-gcp +docxtpl +primp +opentelemetry-instrumentation-celery +yarg +dvc +hatch-requirements-txt +pytest-aiohttp +idna-ssl +django-allauth +langgraph-prebuilt +apache-airflow-providers-airbyte +boostedblob +azure-mgmt-hybridcompute +jieba +python-lsp-jsonrpc +mss +pyerfa +ray-elasticsearch +setuptools-git-versioning +modelscope +flask-admin +rouge-score +regress +tables +aws-lambda-builders +ulid-py +phonenumberslite +djangorestframework-stubs +shyaml +dbfread +pysaml2 +marko +py-ecc +cerberus-python-client +visions +exchangelib +apache-airflow-providers-odbc +protoc-gen-openapiv2 +jsonargparse +numpy-financial +pybase64 +uproot +intervaltree +tensorflow-addons +rx +cinemagoer +dpkt +s3cmd +aioredis +aiostream +dominate +urllib3-secure-extra +hashids +resampy +keyrings-alt +dotenv +path +click-help-colors +pymisp +typish +credstash +doit +oslo-utils +oyaml +dash-bootstrap-components +dockerpty +dropbox +ephem +apache-airflow-providers-apache-spark +python-oxmsg +imdbpy +types-decorator +betterproto +kneed +pipreqs +jsonmerge +rcssmin +pyupgrade +cloudscraper +pyvmomi +outlines-core +django-prometheus +mkdocs-monorepo-plugin +cuda-python +wand +rank-bm25 +sphinx-argparse +graypy +django-csp +dbt-fabric +pudb +panel +pyquaternion +pyvirtualdisplay +python-geohash +jaxtyping +nameparser +newspaper3k +pytest-dependency +mypy-boto3-cloudwatch +wasmer +lunardate +pytoolconfig +pymongocrypt +types-colorama +modal +opentelemetry-instrumentation-boto3sqs +apsw +justext +readerwriterlock +optax +rstr +datacompy +pyvis +mypy-boto3-lakeformation +cheetah3 +azureml-dataprep-rslex +xarray-einstats +apache-airflow-providers-dbt-cloud +wmi +pep8 +django-otp +gspread-formatting +fastapi-pagination +tlparse +array-record +pinecone-plugin-inference +cupy-cuda12x +html-text +config +mkdocs-macros-plugin +tableau-api-lib +jupyter-server-ydoc +flake8-pyproject +pyscreeze +python-certifi-win32 +pynput +swifter +tensorflow-cpu +k8 +textdistance +safety-schemas +wasmer-compiler-cranelift +glob2 +mypy-boto3-dataexchange +west +asgi-correlation-id +google-cloud-discoveryengine +django-countries +htmlmin +tf-nightly +flask-socketio +dnslib +versioneer-518 +c7n-org +tree-sitter-yaml +azure-mgmt-resourcegraph +func-timeout +jupyter-ydoc +ndindex +troposphere +crc32c +opentelemetry-resourcedetector-gcp +detect-secrets +peppercorn +janus +pytest-freezegun +opentelemetry-exporter-gcp-trace +sgqlc +django-picklefield +cron-validator +pysbd +python-amazon-sp-api +crewai +braintree +pretty-html-table +flake8-polyfill +coreschema +depyf +azure-storage +ebcdic +elastic-apm +easydict +types-werkzeug +synchronicity +pytube +django-health-check +requests-auth-aws-sigv4 +pillow-avif-plugin +ibm-platform-services +raven +cuda-bindings +update-checker +onnxconverter-common +awslambdaric +jsons +ldaptor +gluonts +sqlalchemy-trino +triad +fugue +dirty-equals +types-ipaddress +presto-python-client +result +slowapi +pyroute2 +plac +opencensus-ext-logging +artifacts-keyring +tfds-nightly +grimp +contextvars +apache-airflow-providers-oracle +torchdiffeq +legacy-cgi +y-py +curatorbin +proxy-protocol +workalendar +ocspbuilder +ocspresponder +evergreen-lint +oslo-config +typeid-python +recommonmark +py-deviceid +opentelemetry-instrumentation-asyncpg +gssapi +tyro +import-deps +coveralls +adagio +mkdocs-git-revision-date-localized-plugin +polling2 +django-silk +oslo-i18n +django-oauth-toolkit +python-on-whales +jupyter-server-fileid +ptpython +azure-ai-formrecognizer +zc-lockfile +jsonschema-spec +types-openpyxl +pdoc +pysnmp +eradicate +flask-talisman +progress +ormsgpack +pybytebuffer +check-jsonschema +ml-collections +sktime +spinners +pykmip +argparse-addons +log-symbols +aiosmtplib +policyuniverse +requests-oauth +fast-depends +flake8-eradicate +pytweening +geocoder +tensorflow-probability +transitions +pyexcel-io +logzio-python-handler +azure-schemaregistry-avroserializer +trafilatura +opentelemetry-instrumentation-aws-lambda +certbot-dns-cloudflare +gitdb2 +envyaml +lml +numpydoc +pyautogui +requests-kerberos +ypy-websocket +pyairports +tree-sitter-java +quart +dnachisel +langchain-ollama +myst-nb +sklearn-compat +future-fstrings +geckodriver-autoinstaller +jupyter-cache +python-ipware +isal +milvus-lite +pyrect +treelib +pygetwindow +tree-sitter-xml +testtools +uncertainties +opentelemetry-instrumentation-system-metrics +freetype-py +starlette-exporter +apache-airflow-providers-apache-kafka +types-flask +optimum +ratelim +linecache2 +libsass +ntlm-auth +azureml-mlflow +statsforecast +checksumdir +pip-audit +inquirerpy +traceback2 +scikit-base +flake8-import-order +mouseinfo +oslo-serialization +sanic-routing +pyenchant +courlan +apache-airflow-providers-salesforce +rjsmin +pysmb +dict2xml +clang +elementary-data +azure-ai-inference +phik +python-calamine +pfzy +pydruid +kconfiglib +jinja2-humanize-extension +mkdocs-literate-nav +types-chardet +flake8-builtins +types-stripe +tree-sitter-go +pymsgbox +flask-mail +undetected-chromedriver +textparser +super-collections +mypy-boto3-ecs +dotty-dict +snakeviz +tree-sitter-rust +latexcodec +opentelemetry-instrumentation-pika +decli +types-httplib2 +flake8-print +verboselogs +pyarrow-stubs +asteval +tree-sitter-cpp +mongo-tooling-metrics +opentelemetry-exporter-prometheus-remote-write +pluginbase +django-taggit +debtcollector +apache-airflow-providers-pagerduty +cachy +mongo-ninja-python +pybtex +mypy-boto3-emr +pyshp +shellescape +emr-notebooks-magics +volcengine-python-sdk +pyexasol +pytest-github-actions-annotate-failures +docxcompose +yappi +facexlib +line-profiler +simple-parsing +gnupg +tensorflow-intel +icecream +schwifty +commitizen +honcho +naked +josepy +os-service-types +python-codon-tables +lhcbdirac +onfido-python +diagrams +httpretty +mypy-boto3-elbv2 +aiodataloader +autofaker +chromedriver-autoinstaller +google-cloud-pipeline-components +hmsclient +types-regex +certifi-linux +sphinx-autoapi +python-cinderclient +django-anymail +pylatexenc +pytelegrambotapi +lbenv +shopifyapi +pyopengl +sampleproject +grapheme +lunarcalendar +rfc3339 +dagster-pandas +types-dateparser +sasl +lbplatformutils +lbcondawrappers +google-cloud-alloydb +stone +pdbp +mercantile +codecov +llama-index-legacy +lb-telemetry +xenv +textwrap3 +airbyte-cdk +airbyte-api +macholib +livy +bson +pytest-memray +pynose +django-formtools +gdbmongo +html-testrunner +patterns +comfyui-frontend-package +pylru +tabcompleter +mypy-boto3-ses +pytest-factoryboy +openapi-core +brotlipy +django-mptt +extract-msg +extras +itables +fastapi-utils +argh +google-cloud-scheduler +cibuildwheel +fastprogress +anybadge +openinference-semantic-conventions +opentelemetry-instrumentation-starlette +docusign-esign +pytest-lazy-fixture +httmock +treescope +typing-utils +dagster-k8s +types-lxml +pyactiveresource +pip-licenses +apache-airflow-providers-datadog +awscliv2 +llama-index-llms-azure-openai +azure-monitor-ingestion +pytest-retry +amazon-textract-response-parser +flask-smorest +torchao +crewai-tools +types-aiobotocore-sqs +red-discordbot +langchain-cohere +dagster-slack +mockito +tavily-python +datadog-lambda +torchtext +clr-loader +google-cloud-error-reporting +ansiwrap +kgb +wurlitzer +sphinx-book-theme +astropy-iers-data +tinydb +sphinx-tabs +ddt +python-frontmatter +aws-msk-iam-sasl-signer-python +subprocess32 +mypy-boto3-route53 +dagster-dbt +crccheck +docformatter +scons +opentelemetry-instrumentation-pymongo +easyocr +untokenize +mypy-boto3-sagemaker +nbsphinx +tree-sitter-bash +dbus-fast +openstacksdk +tree-sitter-html +types-pyserial +pytest-recording +celery-redbeat +pre-commit-hooks +property-manager +quantlib +django-compressor +utilsforecast +tree-sitter-css +js2py +tree-sitter-json +jenkinsapi +scenedetect +tree-sitter-sql +tree-sitter-regex +model-bakery +tree-sitter-toml +tree-sitter-markdown +param +restrictedpython +matrix-client +pyexcel +sphinxcontrib-websupport +localstack-core +scikit-build +nose2 +vtk +types-pkg-resources +uuid-utils +guppy3 +types-enum34 +django-ses +opentelemetry-resourcedetector-kubernetes +asyncstdlib +google-api-python-client-stubs +nibabel +bootstrap-flask +scikit-optimize +paste +backports-cached-property +tqdm-multiprocess +rpaframework +drf-nested-routers +opentelemetry-instrumentation-tortoiseorm +tinysegmenter +tensorflow-io +openapi-schema-pydantic +mkdocs-section-index +flyteidl +jinja2-cli +parsy +oci-cli +crypto +pytest-watch +presidio-anonymizer +iterative-telemetry +aws-cdk-aws-glue-alpha +jsonpath +urwid-readline +pytorch-metric-learning +shiboken6 +pip-system-certs +google-cloud-artifact-registry +ariadne +dvc-data +zigpy +clean-fid +platformio +pulsar-client +google-cloud-iam +fido2 +cli-helpers +git-remote-codecommit +singer-python +watchgod +opentelemetry-resourcedetector-docker +cmaes +pyside6-essentials +luqum +pytest-vcr +jiwer +ecos +pythonnet +firebolt-sdk +httpx-ws +flake8-quotes +gs-quant +jinja2-time +pyside6 +pytest-azurepipelines +formic2 +xopen +openai-whisper +strict-rfc3339 +mapbox-earcut +open3d +issubclass +tbats +annoy +appdirs-stubs +stomp-py +pytest-snapshot +dateutils +import-linter +easygui +recurring-ical-events +pyre-extensions +pyside6-addons +smmap2 +tink +watchdog-gevent +stdlib-list +databricks-feature-engineering +mxnet +elasticsearch-dbapi +setuptools-git +tree-sitter-languages +databricks-pypi-extras +flufl-lock +looseversion +nodejs-wheel-binaries +types-maxminddb +validate-email +ntplib +oic +django-polymorphic +pypsrp +intuit-oauth +aiohttp-jinja2 +python-openstackclient +azureml-dataprep-native +breathe +skl2onnx +x-wr-timezone +types-aiobotocore-dataexchange +airflow-provider-great-expectations +boto3-stubs-lite +types-futures +roundrobin +graphene-django +typeshed-client +eyes-common +frictionless +mem0ai +ruamel-yaml-jinja2 +nulltype +llama-index-embeddings-azure-openai +eyes-selenium +snuggs +sbvirtualdisplay +requestsexceptions +pyqt6 +awacs +tentaclio +types-pyasn1 +docstring-to-markdown +delta +mypy-boto3-autoscaling +pypinyin +python-keystoneclient +lmfit +patool +pytest-celery +fvcore +django-reversion +imagecodecs +enrich +singleton-decorator +scylla-driver +apache-airflow-providers-tableau +grandalf +sqlalchemy-mate +selenium-wire +molecule +sqlakeyset +telethon +flasgger +pyexcel-xlsx +google-cloud-documentai +polyline +opentelemetry-exporter-jaeger-thrift +django-ninja +flake8-black +pyxdg +django-structlog +mypy-boto3-events +mecab-python3 +mypy-boto3-acm +unittest2 +rpyc +coreforecast +pytest-parallel +apache-airflow-providers-celery +azure-mgmt-kusto +deepface +readability-lxml +types-xmltodict +json-logging +decopatch +tentaclio-s3 +ariadne-codegen +flytekit +fredapi +uvicorn-worker +google-cloud-recaptcha-enterprise +halo +mdx-truly-sane-lists +fixtures +delta-sharing +types-termcolor +backports-strenum +django-ratelimit +flask-debugtoolbar +pyautogen +publish-event-sns +us +flask-basicauth +markuppy +core-universal +hdbscan +javaobj-py3 +flake8-noqa +requests-pkcs12 +frida +sqlalchemy-continuum +dagster-cloud +customerio +xmljson +pyshark +logz +mkdocs-redirects +azure-iot-device +django-axes +urlobject +traittypes +dspy +mkdocs-awesome-pages-plugin +python-memcached +deep-translator +pdfrw +plaid-python +celery-types +jupyter-packaging +sphinxcontrib-bibtex +django-waffle +falcon +rdrobust +colored +alibabacloud-adb20211201 +cchardet +prefect-docker +fluent-logger +rdkit +mypy-boto3-opensearch +markitdown +zope-schema +pystan +imblearn +spacy-wordnet +mypy-boto3-transfer +pytest-reportlog +netsuitesdk +apache-airflow-providers-redis +mlxtend +fhir-resources +deepspeed +colorcet +dify-plugin +python-redis-lock +publicsuffixlist +sqlalchemy-hana +uszipcode +lxml-stubs +opentelemetry-exporter-jaeger-proto-grpc +dagster-spark +xlutils +krb5 +pyglet +sudachidict-core +flametree +htmldocx +argilla +jieba3k +feedfinder2 +teamcity-messages +opentelemetry-semantic-conventions-ai +initools +types-aiobotocore-dynamodb +langchain-chroma +braintrust-core +sqlalchemy-drill +django-admin-rangefilter +magic-filter +azure-storage-nspkg +flask-oidc +sqllineage +pyqt6-qt6 +chainlit +flask-dance +pyqt6-sip +flake8-plugin-utils +akshare +wadler-lindig +jsonfield +avro-gen +opencc +pygobject +mailchimp-marketing +azure-cognitiveservices-speech +clikit +alembic-postgresql-enum +mkdocs-gen-files +pyexcel-xls +minidump +types-emoji +mypy-boto3-ebs +cxxfilt +filesplit +braintrust +html5tagger +mypy-boto3-dlm +pyviz-comms +yarn-api-client +asyncache +tabula-py +streamsets +duckdb-engine +zipfile-deflate64 +tracerite +types-docker +django-widget-tweaks +flameprof +pymannkendall +langchain-groq +versioningit +asciitree +gpustat +wikipedia +python-subunit +scmrepo +mypy-boto3-cognito-idp +plum-dispatch +yattag +gliner +camel-converter +pyfzf +pydantic-yaml +cvxopt +pydantic-ai-slim +mypy-boto3-emr-serverless +swagger-spec-validator +sqlitedict +quinn +pathtools +pot +pydicom +tensorflow-gpu +backports-shutil-get-terminal-size +kerberos +setuptools-scm-git-archive +ragas +nanobind +flake8-bandit +mypy-boto3-batch +pytest-docker +apache-airflow-client +sparqlwrapper +pyserde +pyngrok +notifiers +webauthn +rpaframework-core +apache-airflow-providers-openlineage +databases +opentelemetry-instrumentation-elasticsearch +slacker +gputil +gender-guesser +nbqa +opencc-python-reimplemented +scrapbook +pythonping +transaction +collections-extended +pydantic-xml +gcs-oauth2-boto-plugin +embedding-reader +sphinxcontrib-spelling +airflow-dbt +jstyleson +python3-logstash +arnparse +autofaiss +zope-proxy +datarobotx +pgeocode +whatthepatch +pyannote-database +pylint-pydantic +blendmodes +uuid7 +mypy-boto3-bedrock-runtime +databricks-feature-store +adyen +pybtex-docutils +appengine-python-standard +oletools +acme +drf-spectacular-sidecar +objgraph +tangled-up-in-unicode +python-fsutil +pyannote-core +ansible-base +splunk-sdk +hellosign-python-sdk +pyjks +pyannote-audio +zthreading +python-lsp-server +domdf-python-tools +envs +sparse +xatlas +autocommand +pcodedmp +zipfile36 +textfsm +tach +opentelemetry-propagator-gcp +types-flask-cors +murmurhash2 +rstcheck +gtts +jinjasql +simplegeneric +livereload +robotframework-tidy +vhacdx +synapseml +salt-lint +sphinxcontrib-httpdomain +exchange-calendars +sshconf +shandy-sqlfmt +easyprocess +lsprotocol +msgpack-types +pytest-ansible +aws-assume-role-lib +spyne +pyjarowinkler +azureml-dataset-runtime +svg-path +check-manifest +deepeval +manhole +manifold3d +ydb +assertpy +verspec +e2b +pyannote-metrics +gcloud +fixedwidth +hashin +office365 +pyod +rpaframework-pdf +apipkg +scikeras +tox-gh-actions +google-search-results +unsloth +gnureadline +pyserial-asyncio +pydrive2 +resize-right +sunshine-conversations-client +types-tzlocal +prometheus-api-client +semantic-kernel +databind-json +dotmap +django-tables2 +robocorp-storage +submitit +coverage-badge +types-orjson +jaraco-text +mypy-boto3 +dvc-render +miscreant +json-stream-rs-tokenizer +groundingdino-py +jsmin +pydantic-ai +tbb +torch-geometric +mkdocs-click +defusedcsv +robotframework-seleniumtestability +robocorp-vault +alexapy +pypruningradixtrie +ruptures +fancycompleter +executor +beniget +arch +ratelimiter +pgspecial +asn1 +uptrace +cvss +word2number +connectorx +logfire-api +flash-attn +pytest-deadfixtures +opentelemetry-instrumentation-kafka-python +dvc-objects +keyboard +graspologic +brotlicffi +json2html +elevenlabs +rarfile +webvtt-py +table-logger +pdbpp +pymodbus +sqltap +darglint +osc-lib +plette +python-barcode +pydantic-graph +opentelemetry-instrumentation-openai +torchdata +treepoem +ipympl +opentok +msgpack-python +opentelemetry-instrumentation-pymysql +hunter +pytest-nunit +databind-core +opentelemetry-instrumentation-boto +jsonpath-rw-ext +autoevals +parsley +google-python-cloud-debugger +django-colorfield +backports-ssl-match-hostname +usearch +robotframework-pabot +evidently +libretranslatepy +solders +ibm-db-sa +docker-image-py +argparse-dataclass +blessings +djangorestframework-api-key +pybase62 +delta-kernel-rust-sharing-wrapper +capstone +patch +wmctrl +kubernetes-client +fairscale +pgcli +audioop-lts +pyston +suds-py3 +translate +pyston-autoload +buildkite-test-collector +language-tags +glfw +rtfde +crayons +mypy-boto3-firehose +cyclopts +json-ref-dict +basedpyright +django-treebeard +fasttext-langdetect +brickflows +robotframework-stacktrace +dbt-athena-community +eralchemy +imgaug +opentelemetry-instrumentation-aiohttp-server +azure-communication-email +cartopy +spython +autodoc-pydantic +opentelemetry-instrumentation-tornado +pytest-testinfra +python-logging-loki +java-access-bridge-wrapper +praw +pi-heif +ibm-cos-sdk +rich-rst +mdformat +postmarker +pynput-robocorp-fork +pycognito +kedro +python-semantic-release +taskgroup +crispy-bootstrap5 +importlib +ibm-cos-sdk-core +splunk-handler +plyvel +python-redis-rate-limit +aiopg +googleads +pyapns-client +sharepy +symengine +b2sdk +mypy-boto3-cognito-identity +ibm-cos-sdk-s3transfer +fastdiff +z3-solver +prawcore +nc-py-api +mistletoe +testing-common-database +lazyasd +djangorestframework-csv +pyaudio +requests-html +bingads +mailchimp-transactional +google-cloud-managedkafka +python-miio +diceware +tatsu +mkdocs-panzoom-plugin +missingpy +dbt-duckdb +dvc-task +kafka-python-ng +latex2mathml +aws-embedded-metrics +codetiming +lingua-language-detector +snapshottest +biotraj +psygnal +langchain-huggingface +seeuletter +simpleflow +azureml-telemetry +dvc-studio-client +opentelemetry-propagator-jaeger +xdoctest +optbinning +notion +holoviews +palettable +lief +ansible-runner +isoweek +retry-decorator +flask-threads +pymeta3 +webrtcvad-wheels +pygls +asteroid-filterbanks +pytest-docker-tools +tomesd +iso4217 +stamina +sqltrie +pem +localstack-client +django-webpack-loader +aiodocker +chargebee +mkdocs-meta-manager +types-aioboto3 +feu +mypy-boto3-application-autoscaling +cmakelang +iterfzf +sly +compressed-rtf +escapism +python-json-config +reportportal-client +lazify +lpips +flake8-junit-report-basic +pytest-archon +pyzbar +dvclive +torch-audiomentations +graphlib-backport +flask-oauthlib +flake8-broken-line +pytest-cases +xmlunittest +pebble +types-typing-extensions +pyqtgraph +dvc-http +nats-py +primepy +openvino-telemetry +lucopy +mkdocs-link-marker +jinja2-pluralize +types-aiobotocore-lambda +dapr +mariadb +pyjwkest +pysam +graspologic-native +testing-postgresql +google-reauth +array-api-compat +django-two-factor-auth +pytest-find-dependencies +uncalled +azure-containerregistry +roman +shellcheck-py +ruyaml +torch-pitch-shift +mkdocs-auto-tag-plugin +pandas-market-calendars +pyloudnorm +username +apache-airflow-providers-atlassian-jira +qudida +asyncio-atexit +mike +google-cloud-functions +snapshot-restore-py +django-auth-ldap +scooby +art +flake8-annotations +great-expectations-experimental +starrocks +workos +chdb +cron-converter +mf2py +bz2file +pyrdfa3 +dagster-cloud-cli +okta +mkdocs-glightbox +seqio-nightly +pytest-clarity +plotly-resampler +plux +tensorboard-plugin-profile +extruct +openvino +pyjsparser +googletrans +django-auditlog +pyfarmhash +json-stream +cvdupdate +types-appdirs +oslo-log +trafaret +copier +hurry-filesize +python-jsonschema-objects +pvlib +xpln2me +grpcio-testing +djangorestframework-dataclasses +nvidia-cuda-nvcc-cu12 +google-cloud-appengine-admin +pyannote-pipeline +python-logstash +django-ckeditor +nbstripout +mailjet-rest +opentelemetry-instrumentation-confluent-kafka +pyomo +django-object-actions +target-hotglue +pismosendlogs +opentelemetry-instrumentation-mysql +apache-airflow-providers-opsgenie +quadprog +pytest-freezer +django-modeltranslation +sqlalchemy-json +neptune-client +imapclient +expecttest +logging +flupy +pytest-wake +imutils +hyppo +langchain-postgres +gcloud-aio-pubsub +cement +elasticsearch8 +ascii-magic +lm-eval +currencyconverter +jinja2-ansible-filters +sphinxcontrib-apidoc +python-whois +apache-airflow-providers-apache-hive +pyminizip +types-bleach +fastcluster +docarray +databricks-labs-blueprint +opentelemetry-resourcedetector-process +azure-mgmt-managedservices +pytest-flask +authcaptureproxy +opentelemetry-container-distro +rangehttpserver +fastexcel +hnswlib +lkml +langid +sagemaker-mlflow +django-localflavor +rioxarray +pyvista +acryl-datahub-airflow-plugin +htmlmin2 +django-coverage-plugin +inline-snapshot +nutter +langchain-pinecone +sqlalchemy-databricks +ibis-framework +keplergl +browser-use +libusb1 +aws-cdk-asset-node-proxy-agent-v5 +jaraco-collections +python-string-utils +growthbook +azureml-pipeline-core +types-oauthlib +logfury +pytest-opentelemetry +mypy-boto3-efs +langgraph-checkpoint-postgres +browsergym-core +zipcodes +fortifyapi +udocker +gevent-websocket +apache-airflow-providers-samba +evdev +langchainhub +ydb-dbapi +google-cloud-ndb +grep-ast +pytest-datadir +mypy-boto3-sagemaker-runtime +spark-sklearn +rouge +duo-client +opentelemetry-instrumentation-psycopg +mujoco +appier +flask-testing +descartes +drf-extensions +pyvim +pyop +torchinfo +pytest-testmon +pyqrcode +openinference-instrumentation +opentelemetry-instrumentation-mysqlclient +opentelemetry-instrumentation-aio-pika +types-networkx +cbor +spacy-language-detection +mypy-boto3-textract +couchbase +sttable +yellowbrick +opentelemetry-instrumentation-falcon +jamo +mail-parser +sphinxext-rediraffe +faust-cchardet +apache-airflow-providers-apache-livy +zope-component +jupyter-highlight-selected-word +netmiko +p4python +flake8-debugger +hstspreload +jsonnet +whoosh +types-pycurl +libtmux +unstructured-inference +django-hijack +paddlepaddle +pip-check +ldapdomaindump +alembic-utils +google-apps-meet +jsonslicer +aiohttp-socks +python-benedict +textual-dev +wincertstore +spandrel-extra-arches +newrelic-telemetry-sdk +edgegrid-python +selectolax +tabcmd +tk +bumpversion +flaml +vobject +llama-index-readers-web +pyinstaller-versionfile +nutree +html-tag-names +flake8-commas +html-void-elements +wmill +qiskit +odxtools +unsloth-zoo +pyxirr +awsiotsdk +textual-serve +aws-lambda-typing +anyscale +zxcvbn +onnxruntime-genai +sqlite-utils +recordclass +gto +pymc +codefind +dagster-gcp +asyncclick +xmodem +pygerduty +uhashring +ajsonrpc +psycopg-c +anyconfig +flask-script +pyspellchecker +tsdownsample +infi-systray +pyvisa +types-fpdf2 +mapclassify +pymatgen +schemdraw +python-etcd +plantuml-markdown +webexteamssdk +pyiotools +pymiscutils +bugsnag +mypy-boto3-config +opentelemetry-instrumentation-pyramid +maybe-else +dagster-celery +pyheif +rdt +opentelemetry-instrumentation-aiopg +jupyter-nbextensions-configurator +darkdetect +csvw +coola +pysubtypes +rstcheck-core +junit2html +azureml-train-core +prettierfier +forex-python +pydotplus +pathmagic +django-guardian +mypy-boto3-eks +fastapi-slim +tree-sitter-typescript +pytensor +types-aiobotocore-ec2 +aws-error-utils +jurigged +mozilla-django-oidc +python-swiftclient +suds +xlsx2csv +ovld +codemagic-cli-tools +prefect-kubernetes +bayesian-optimization +paddleocr +segments +pytest-timeouts +types-python-jose +sodapy +opentelemetry-propagator-b3 +delighted +pyrepl +dbt-clickhouse +torch-fidelity +crhelper +gherkin-official +sphinx-togglebutton +libhoney +git-python +pylink-square +nmslib +opentelemetry-instrumentation-pymemcache +mypy-boto3-cloudfront +blackduck +pulp-glue +localstack-ext +sk-dist +polars-lts-cpu +mimesis +opentelemetry-instrumentation-click +fastai +pykerberos +alibabacloud-tea-openapi +pygraphviz +types-babel +google-cloud-org-policy +functools32 +ntc-templates +sphinx-notfound-page +ropwr +img2pdf +google-cloud-os-config +hidapi +apeye-core +dlinfo +petl +python-novaclient +tempora +streamlit-aggrid +iterators +py-moneyed +warcio +waiting +disposable-email-domains +python-interface +businesstimedelta +psycogreen +aerospike +datadog-logger +kafka +jsonalias +graphrag +mpi4py +throttlex +django-nested-admin +tsx +dapr-ext-fastapi +schematics +rlpycairo +google-cloud-asset +seqeval +streamerate +cantools +pytest-flakefinder +controlnet-aux +pydevd-pycharm +redlock-py +progressbar +slugify +sparkmeasure +pyuwsgi +pyawscron +types-aiobotocore-rds +backports-abc +pyvalid +prefixed +mypy-boto3-bedrock +mypy-boto3-kafka +python-matter-server +covdefaults +names +mypy-boto3-scheduler +python-monkey-business +sparkorm +anndata +google-cloud-access-context-manager +ping3 +pystac +httpie +sklearn2pmml +ezdxf +feast +textstat +cma +prov +pydevd +kedro-datasets +clipboard +scikit-plot +ast-grep-py +sanic-ext +mypy-boto3-codebuild +mypy-boto3-sso +jsonschema-rs +homeassistant +opentelemetry-test-utils +allure-behave +globus-sdk +oslo-context +backports-entry-points-selectable +gitlint-core +django-mysql +gitlint +selinux +home-assistant-chip-clusters +pysqlite3 +docstring-parser-fork +livekit-protocol +azureml-train-restclients-hyperdrive +mkdocs-techdocs-core +rpy2 +databricks-vectorsearch +intel-openmp +kagglehub +types-aiobotocore-cloudformation +tslearn +fnllm +line-bot-sdk +elasticsearch7 +numpy-quaternion +livekit-api +docling +jupyter-server-proxy +validator-collection +simpleitk +urlextract +traits +crowdstrike-falconpy +python3-xlib +java-manifest +embreex +pythran-openblas +openapi-python-client +importlab +robotframework-jsonlibrary +robotframework-browser +yandex-query-client +hypothesis-jsonschema +opentelemetry-instrumentation-remoulade +meteostat +catkin-pkg +opentelemetry-instrumentation-cassandra +fastapi-sso +googlesearch-python +dashscope +compress-pickle +apispec-oneofschema +fastapi-mail +pyapacheatlas +ordereddict +apeye +jsonseq +gcloud-rest-auth +torchlibrosa +pynetbox +pulp-cli +oauth2 +mypy-boto3-dms +cloudwatch +pysqlite3-binary +mypy-boto3-pricing +mypy-boto3-cloudtrail +flake8-variables-names +pyang +mkdocs-minify-plugin +unstructured-pytesseract +effdet +datarobot +alibabacloud-tea +types-defusedxml +robotframework-robocop +pytest-mypy +certbot +screeninfo +apache-airflow-providers-github +django-fsm +pythainlp +xlwings +enlighten +mypy-boto3-iot +django-constance +flask-dapr +roboflow +django-rq +yagmail +django-user-agents +pylint-gitlab +maison +pysimdjson +airflow-code-editor +sphinx-reredirects +ci-info +tfx-bsl +apache-airflow-providers-apache-druid +mypy-boto3-iot-data +correctionlib +coincurve +sphinx-click +customtkinter +sphinx-gallery +s2sphere +etelemetry +flake8-tidy-imports +julius +pyobjc-core +ncclient +bigframes +llama-index-readers-wikipedia +logfire +persistent +segno +clearml +csscompressor +pytd +zha-quirks +pyttsx3 +aiven-client +astpretty +pandarallel +lazy +swig +azureml-automl-core +bravado +docx +anywidget +colour-science +cherrypy +btrees +libusb-package +sklearn-crfsuite +rule-engine +testrail-api +mmhash3 +meshio +awscli-local +apify-client +sphinx-prompt +dockerfile +openshift +cdk8s +python-graphql-client +pytest-pudb +django-htmx +pyu2f +telebot +types-pysftp +mypy-boto3-codepipeline +mypy-boto3-organizations +mux-python +zope-i18n +types-factory-boy +tdqm +home-assistant-chip-core +amplitude-analytics +llama-index-tools-wikipedia +spotinst-agent +tika +mpire +svix +deepgram-sdk +mypy-boto3-wafv2 +solana +transliterate +robotframework-assertion-engine +pytype +schemathesis +morefs +mypy-boto3-resourcegroupstaggingapi +chalice +opentelemetry-exporter-jaeger +pykakasi +testresources +esprima +django-deprecate-fields +textual-imageview +aqtp +mypy-boto3-quicksight +bleak +mypy-boto3-elasticache +zigpy-znp +interpret-core +tobiko-cloud-helpers +objprint +opentelemetry-instrumentation-aiokafka +scantree +h3-pyspark +mypy-boto3-identitystore +databricks-labs-lsql +zigpy-deconz +mypy-boto3-apigatewaymanagementapi +getdaft +pyhmmer +zigpy-xbee +kaggle +alibabacloud-credentials +sqlite-fts4 +pycrdt +mypy-boto3-codedeploy +telnetlib3 +dramatiq +livekit +py-sr25519-bindings +bellows +azureml-train-automl-client +gptcache +mypy-boto3-apigatewayv2 +alibabacloud-tea-util +inscriptis +mypy-boto3-transcribe +boost-histogram +python-version +mypy-boto3-ce +pyiso8583 +pandas-profiling +pytest-alembic +lazy-model +libtpu +dirhash +labelbox +haystack-ai +cursor +mlserver +replicate +transforms3d +alibabacloud-openapi-util +plotly-express +dynamo-pandas +djoser +apache-airflow-providers-trino +mypy-boto3-servicediscovery +fusepy +google-api +az-cli +litestar +mpld3 +detect-delimiter +python-liquid +pytest-reportportal +azureml-pipeline-steps +cloudinary +pycollada +sphinx-bootstrap-theme +voyageai +scalecodec +django-json-widget +routes +pyinotify +multiaddr +assemblyai +langchain-mistralai +pipupgrade +utm +pyxero +pdqhash +mxnet-mkl +mypy-boto3-timestream-query +mypy-boto3-appconfig +aiosmtpd +mypy-boto3-redshift +alibabacloud-gateway-spi +yamlfix +sagemaker-data-insights +mypy-boto3-es +aim +django-tinymce +autodocsumm +cyclonedx-bom +aiortc +entrypoint2 +pymonetdb +quart-cors +flpc +grequests +multiprocessing-logging +mypy-boto3-dynamodbstreams +django-linear-migrations +django-multiselectfield +nipype +sql-formatter +email-reply-parser +bindep +keras-tuner +amundsen-common +mypy-boto3-neptunedata +mypy-boto3-translate +capsolver +statistics +mkdocs-mermaid2-plugin +robocorp-log +alibabacloud-endpoint-util +beanie +python-dynamodb-lock +alibabacloud-tea-xml +google-i18n-address +nvidia-ml-py3 +jax-cuda12-plugin +prospector +fds-sdk-utils +mergepythonclient +mypy-boto3-pinpoint +colorzero +gpiozero +pylibmc +returns +ip2location +pandas-datareader +mypy-boto3-sesv2 +apispec-webframeworks +pydomo +construct-typing +mygeotab +pywebpush +mypy-boto3-codeartifact +ada-url +dagster-snowflake +ase +hass-web-proxy-lib +stable-baselines3 +path-py +sccache +crochet +mypy-boto3-rds-data +fastapi-cache2 +simple-azure-blob-downloader +lizard +django-select2 +geojson-pydantic +pyspark-pandas +autogluon-core +jupyter-kernel-gateway +django-migration-linter +django-cleanup +simpervisor +azure-schemaregistry-avroencoder +ipaddr +django-rest-swagger +apache-airflow-providers-sendgrid +pyarmor +flask-swagger-ui +pyhdb +cron-schedule-triggers +dissect-target +fastembed +aiogoogle +lazy-imports +imap-tools +tox-ansible +lakefs-client +lcov-cobertura +salesforce-fuelsdk-sans +python-tds +twofish +tmtools +mnemonic +zope-i18nmessageid +fs-s3fs +mypy-boto3-ram +open-webui +llguidance +mypy-boto3-servicecatalog +mkl +pulp-cli-deb +mypy-boto3-location +splink +jax-cuda12-pjrt +pyxnat +py-grpc-prometheus +cdktf +pulp-glue-deb +llama-cpp-python +mypy-boto3-marketplace-entitlement +py-vapid +mypy-boto3-sso-oidc +coredis +coremltools +airflow-provider-lakefs +segtok +faiss-gpu +pockets +mypy-boto3-comprehend +mypy-boto3-securityhub +mypy-boto3-support +cmudict +starlark-pyo3 +stackprinter +sgp4 +mypy-boto3-mediaconvert +idf-component-manager +mypy-boto3-s3control +pyqtwebengine +qiskit-aer +e2b-code-interpreter +mypy-boto3-meteringmarketplace +mypy-boto3-service-quotas +mypy-boto3-synthetics +granian +clize +delocate +dbx +typos +pyprctl +pantab +rocketchat-api +prettyprinter +spaces +mypy-boto3-elb +sorl-thumbnail +dateformat +oauth2-client +hypothesis-graphql +mypy-boto3-mwaa +pydoe +mypy-boto3-route53resolver +parsl +mypy-boto3-guardduty +bpyutils +certvalidator +ffmpeg +trie +azureml-pipeline +meraki +randomname +pycobertura +pypd +mypy-boto3-connect +mypy-boto3-workspaces +valkey +mypy-boto3-amplify +gurobipy +jupyter-contrib-nbextensions +httpstan +dodgy +mypy-boto3-rekognition +stanza +mypy-boto3-sso-admin +fcache +neotime +empy +mypy-boto3-directconnect +portend +ua-parser-rs +mypy-boto3-acm-pca +dagster-pyspark +opentelemetry-instrumentation-langchain +yara-python +kaldiio +pyatlan +runez +apify-shared +mypy-boto3-cloudsearch +cut-cross-entropy +purecloudplatformclientv2 +yolo +od +einx +elasticsearch-curator +pyobjc-framework-cocoa +databind +mypy-boto3-cloudsearchdomain +pycaret +mypy-boto3-budgets +mypy-boto3-docdb +mypy-boto3-polly +auditwheel +py-bip39-bindings +django-money +clu +pyathenajdbc +mypy-boto3-license-manager +mypy-boto3-shield +mypy-boto3-storagegateway +mypy-boto3-appmesh +mypy-boto3-ec2-instance-connect +fastwarc +mypy-boto3-appsync +types-passlib +visitor +mypy-boto3-resource-groups +formencode +sliceline +mypy-boto3-mediaconnect +mypy-boto3-medialive +djangorestframework-jwt +mypy-boto3-kinesis-video-media +mypy-boto3-swf +mypy-boto3-waf +fds-sdk-paengine +mypy-boto3-machinelearning +mypy-boto3-lightsail +mypy-boto3-neptune +spark-expectations +case-conversion +ptvsd +mypy-boto3-lex-models +mypy-boto3-sdb +fds-sdk-sparengine +einops-exts +fds-protobuf-stach-extensions +mypy-boto3-serverlessrepo +mypy-boto3-kinesisvideo +mypy-boto3-kendra +sagemaker-datawrangler +varint +sphinxcontrib-napoleon +mypy-boto3-accessanalyzer +mypy-boto3-comprehendmedical +mypy-boto3-workmail +fds-protobuf-stach-v2 +batchgenerators +types-aiobotocore-sns +mypy-boto3-kinesis-video-archived-media +fds-protobuf-stach +mypy-boto3-fsx +mypy-boto3-iotsecuretunneling +mypy-boto3-workdocs +mypy-boto3-lex-runtime +mypy-boto3-outposts +bzt +mypy-boto3-snowball +mypy-boto3-backup +mypy-boto3-pi +mypy-boto3-waf-regional +autologging +mypy-boto3-marketplace-catalog +mypy-boto3-workmailmessageflow +ag2 +mypy-boto3-route53domains +mypy-boto3-timestream-write +mypy-boto3-qldb-session +mypy-boto3-sagemaker-a2i-runtime +mypy-boto3-kinesisanalytics +mypy-boto3-kinesisanalyticsv2 +mypy-boto3-codecommit +stestr +mypy-boto3-chime +mypy-boto3-pinpoint-email +mypy-boto3-mediatailor +mypy-boto3-personalize +pytest-explicit +simplefix +mypy-boto3-robomaker +basistheory +mypy-boto3-kinesis-video-signaling +mypy-boto3-mq +mypy-boto3-opsworks +pylint-celery +pytest-flake8 +spacy-curated-transformers +mypy-boto3-appstream +mypy-boto3-managedblockchain +torch-model-archiver +mypy-boto3-sms +mypy-boto3-pinpoint-sms-voice +bridgecrew +mypy-boto3-mediapackage-vod +mypy-boto3-gamelift +mypy-boto3-mturk +mypy-boto3-iotevents +viztracer +dagster-datadog +dj-rest-auth +mypy-boto3-mediapackage +mypy-boto3-imagebuilder +braintrust-api +mypy-boto3-health +mypy-boto3-mediastore-data +mypy-boto3-marketplacecommerceanalytics +mypy-boto3-savingsplans +mypy-boto3-mediastore +mypy-boto3-networkmanager +mypy-boto3-opsworkscm +mypy-boto3-qldb +arcticdb +mypy-boto3-iotthingsgraph +uptime-kuma-api +mypy-boto3-fms +mypy-boto3-compute-optimizer +mypy-boto3-sms-voice +mypy-boto3-iotanalytics +mypy-boto3-groundstation +mypy-boto3-mgh +spider-client +mypy-boto3-glacier +django-webtest +types-google-cloud-ndb +mypy-boto3-datasync +mypy-boto3-importexport +mypy-boto3-personalize-events +methoddispatch +globus-compute-endpoint +reprint +mypy-boto3-personalize-runtime +mypy-boto3-inspector +mypy-boto3-globalaccelerator +mypy-boto3-cognito-sync +mypy-boto3-cur +autogluon-features +mypy-boto3-ds +mypy-boto3-migrationhub-config +mypy-boto3-forecast +mypy-boto3-application-insights +mypy-boto3-codestar-notifications +mypy-boto3-elasticbeanstalk +mypy-boto3-iotevents-data +apache-airflow-providers-apache-beam +tm1py +mypy-boto3-iot-jobs-data +mypy-boto3-greengrass +mypy-boto3-iotsitewise +jupyter-contrib-core +mypy-boto3-emr-containers +mcap +mypy-boto3-clouddirectory +msgpack-numpy +awsebcli +mypy-boto3-autoscaling-plans +aiorun +pylibsrtp +evervault-attestation-bindings +starlette-testclient +yaml-config +mypy-boto3-ivs-realtime +mypy-boto3-cloud9 +mypy-boto3-cloudhsmv2 +django-configurations +tgcrypto +kedro-telemetry +mypy-boto3-dax +mypy-boto3-codeguru-reviewer +prefect-shell +mypy-boto3-forecastquery +secure +docling-core +mypy-boto3-devicefarm +ast-grep-cli +azure-ai-contentsafety +pysnmpcrypto +mypy-boto3-datapipeline +mypy-boto3-connectparticipant +markdown-exec +mypy-boto3-frauddetector +django-recaptcha +usaddress-scourgify +python-mimeparse +mypy-boto3-codestar-connections +gin-config +amundsen-databuilder +mypy-boto3-detective +mypy-boto3-discovery +mypy-boto3-cloudhsm +mypy-boto3-codeguruprofiler +pip-install-test +sox +panns-inference +dag-factory +tempita +evervault +mypy-boto3-elastictranscoder +sqlean-py +draftjs-exporter +mypy-boto3-macie2 +patchy +meltano +ibm-secrets-manager-sdk +pyjson5 +mdxpy +pylcs +munkres +mypy-boto3-ecr-public +python-debian +pyicu-binary +ws4py +aiotask-context +fugashi +cpplint +dagster-shell +mypy-boto3-bedrock-agent-runtime +python-schema-registry-client +meross-iot +pyocd +pyreadline +sspilib +hachoir +mypy-boto3-network-firewall +jc +rapidocr-onnxruntime +assisted-service-client +mypy-boto3-ivs +arize +mypy-boto3-appconfigdata +mypy-boto3-braket +autovizwidget +pybreaker +dbt-athena +flask-sock +pysolr +types-zstd +dm-control +hdijupyterutils +pyarabic +discord +python-binance +inscribe +g2p-en +pydeprecate +pypandoc-binary +extensionclass +antithesis +coiled +globus-identity-mapping +donfig +zope-location +doc8 +tensorflow-decision-forests +requirements-detector +literalai +mypy-boto3-s3outposts +heapdict +lifetimes +mypy-boto3-sagemaker-featurestore-runtime +semantic-link-sempy +mypy-boto3-sagemaker-edge +canopen +autogluon-tabular +mypy-boto3-servicecatalog-appregistry +files-com +streamlit-keyup +times +mypy-boto3-auditmanager +gfpgan +mypy-boto3-amp +livekit-agents +slack +mypy-boto3-iotwireless +wiki-fetch +together +wagtail +mypy-boto3-wellarchitected +pyroscope-io +flet +mypy-boto3-elastic-inference +mypy-boto3-bedrock-agent +reverse-geocoder +bravado-core +mypy-boto3-lookoutvision +sphinxcontrib-plantuml +autoray +mypy-boto3-amplifybackend +azureml-sdk +toml-sort +mypy-boto3-databrew +google-cloud-profiler +equinox +flask-apispec +asammdf +airtable +mypy-boto3-healthlake +impacket +mypy-boto3-appintegrations +pyro-ppl +mypy-boto3-greengrassv2 +mypy-boto3-lexv2-models +icalevents +mypy-boto3-customer-profiles +fastdtw +pennylane-lightning +css-inline +mypy-boto3-iotdeviceadvisor +mypy-boto3-lexv2-runtime +mypy-boto3-connect-contact-lens +zope-security +mypy-boto3-devops-guru +pdm-pep517 +pydrive +django-dirtyfields +paypalrestsdk +mltable +mypy-boto3-iotfleethub +djangorestframework-camel-case +phonemizer +distance +mkdocs-include-markdown-plugin +graphql-query +arxiv +measurement +jplephem +syncer +aioice +autogluon +python-openid +pyreadstat +amundsen-rds +azureml-inference-server-http +drf-jwt +mypy-boto3-lookoutmetrics +globus-compute-sdk +websocket +aws-kinesis-agg +pwlf +mypy-boto3-lookoutequipment +jaro-winkler +marshmallow3-annotations +mypy-boto3-mgn +mypy-boto3-fis +rust-demangler +pymupdf4llm +rlbot +pykcs11 +esp-idf-kconfig +flake8-string-format +flake8-pytest-style +mypy-boto3-s3tables +bce-python-sdk +mypy-boto3-account +human-json +mypy-boto3-payment-cryptography +mypy-boto3-ssm-incidents +mypy-boto3-apprunner +acres +mypy-boto3-payment-cryptography-data +adbc-driver-manager +typed-argument-parser +mypy-boto3-finspace +mypy-boto3-finspace-data +wiremock +mypy-boto3-ssm-contacts +kt-legacy +mypy-boto3-proton +pyvisa-py +mypy-boto3-applicationcostprofiler +youtube-dl +pybars4 +botbuilder-schema +better-exceptions +mypy-boto3-grafana +gcloud-aio-datastore +awslimitchecker +pylogbeat +pandas-flavor +keras-nightly +bump-my-version +smartystreets-python-sdk +tls-client +prefect-ray +mypy-boto3-cloudcontrol +simplekml +rpmfile +tensorflow-model-optimization +prisma +mypy-boto3-route53-recovery-control-config +xmldiff +pythran +pyproject-flake8 +connected-components-3d +django-admin-list-filter-dropdown +mypy-boto3-memorydb +mypy-boto3-chime-sdk-messaging +hist +mypy-boto3-redshift-serverless +mypy-boto3-wisdom +mypy-boto3-route53-recovery-readiness +mypy-boto3-route53-recovery-cluster +mypy-boto3-chime-sdk-identity +django-autocomplete-light +docling-parse +pymap3d +stopit +records +mediapy +cmsis-pack-manager +mypy-boto3-inspector2 +python-jsonpath +mypy-boto3-snow-device-management +azure-eventhub-checkpointstoreblob-aio +mypy-boto3-kafkaconnect +mypy-boto3-voice-id +mypy-boto3-keyspaces +pytest-codspeed +cmake-format +mplcursors +unleashclient +versionfinder +nameof +anyjson +botframework-connector +zmq +mypy-boto3-marketplace-agreement +mypy-boto3-workspaces-web +localstack +mypy-boto3-pinpoint-sms-voice-v2 +requests-gssapi +mypy-boto3-verifiedpermissions +exrex +fireworks-ai +djangorestframework-xml +mypy-boto3-opensearchserverless +mypy-boto3-cleanrooms +streamlit-folium +flake8-rst-docstrings +mypy-boto3-panorama +mypy-boto3-chime-sdk-meetings +dbt-trino +gjson +mypy-boto3-amplifyuibuilder +tencentcloud-sdk-python +mypy-boto3-billingconductor +mypy-boto3-datazone +mypy-boto3-backup-gateway +mypy-boto3-migrationhubstrategy +mypy-boto3-drs +mypy-boto3-evidently +mypy-boto3-resiliencehub +django-log-request-id +arize-phoenix +mypy-boto3-iottwinmaker +mypy-boto3-iotfleetwise +mypy-boto3-omics +mypy-boto3-rum +mypy-boto3-migration-hub-refactor-spaces +mypy-boto3-workspaces-thin-client +mypy-boto3-connectcases +crontab +chess +mypy-boto3-chime-sdk-media-pipelines +mypy-boto3-chime-sdk-voice +mypy-boto3-ivschat +mypy-boto3-rbin +extra-streamlit-components +mypy-boto3-controltower +mypy-boto3-qbusiness +stream-inflate +mypy-boto3-arc-zonal-shift +mypy-boto3-support-app +mypy-boto3-vpc-lattice +gcloud-rest-datastore +mypy-boto3-b2bi +mypy-boto3-connectcampaigns +mypy-boto3-timestream-influxdb +mypy-boto3-m2 +pygam +mypy-boto3-ssm-sap +mypy-boto3-application-signals +mypy-boto3-cost-optimization-hub +luigi +mypy-boto3-codecatalyst +mypy-boto3-license-manager-user-subscriptions +etuples +mypy-boto3-cloudtrail-data +mypy-boto3-cleanroomsml +mypy-boto3-tnb +mypy-boto3-appfabric +mypy-boto3-osis +mypy-boto3-mediapackagev2 +lap +mypy-boto3-kendra-ranking +mypy-boto3-bcm-data-exports +eccodes +mypy-boto3-resource-explorer-2 +mypy-boto3-rolesanywhere +mypy-boto3-sagemaker-metrics +mypy-boto3-internetmonitor +plyfile +rejson +mypy-boto3-taxsettings +mypy-boto3-entityresolution +mypy-boto3-artifact +mypy-boto3-neptune-graph +mypy-boto3-codeguru-security +mypy-boto3-pipes +text-generation +mypy-boto3-trustedadvisor +mypy-boto3-supplychain +mypy-boto3-migrationhuborchestrator +mypy-boto3-cloudfront-keyvaluestore +cwcwidth +django-rest-polymorphic +mypy-boto3-chatbot +stream-unzip +mypy-boto3-controlcatalog +mypy-boto3-securitylake +mypy-boto3-privatenetworks +python-jwt +mypy-boto3-oam +mypy-boto3-docdb-elastic +apache-airflow-providers-apprise +mypy-boto3-apptest +mypy-boto3-qconnect +mypy-boto3-simspaceweaver +mypy-boto3-kinesis-video-webrtc-storage +adtk +py3langid +mypy-boto3-mailmanager +paramiko-expect +mypy-boto3-pca-connector-ad +mypy-boto3-freetier +mypy-boto3-medical-imaging +mypy-boto3-managedblockchain-query +mypy-boto3-codeconnections +mypy-boto3-launch-wizard +mypy-boto3-deadline diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index c69de3bde..f580111ca 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -23,6 +23,7 @@ from macaron.malware_analyzer.pypi_heuristics.metadata.high_release_frequency import HighReleaseFrequencyAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.one_release import OneReleaseAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.source_code_repo import SourceCodeRepoAnalyzer +from macaron.malware_analyzer.pypi_heuristics.metadata.typosquatting_presence import TyposquattingPresenceAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.unchanged_release import UnchangedReleaseAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence import WheelAbsenceAnalyzer from macaron.malware_analyzer.pypi_heuristics.pypi_sourcecode_analyzer import PyPISourcecodeAnalyzer @@ -332,6 +333,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: SuspiciousSetupAnalyzer, WheelAbsenceAnalyzer, AnomalousVersionAnalyzer, + TyposquattingPresenceAnalyzer, ] # name used to query the result of all problog rules, so it can be accessed outside the model. @@ -395,12 +397,18 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: failed({Heuristics.ONE_RELEASE.value}), failed({Heuristics.ANOMALOUS_VERSION.value}). + % Package released with a name similar to a popular package. + {Confidence.MEDIUM.value}::trigger(malware_medium_confidence_3) :- + quickUndetailed, + failed({Heuristics.TYPOSQUATTING_PRESENCE.value}). + % ----- Evaluation ----- % Aggregate result {problog_result_access} :- trigger(malware_high_confidence_1). {problog_result_access} :- trigger(malware_high_confidence_2). {problog_result_access} :- trigger(malware_high_confidence_3). + {problog_result_access} :- trigger(malware_medium_confidence_3). {problog_result_access} :- trigger(malware_medium_confidence_2). {problog_result_access} :- trigger(malware_medium_confidence_1). query({problog_result_access}). From 600316659606440d0341422418c65f2bc27a2179 Mon Sep 17 00:00:00 2001 From: Amine Date: Wed, 23 Apr 2025 09:54:31 +0100 Subject: [PATCH 02/10] feat: implement typosquatting detection for package names Adds a new security analysis feature to detect potential typosquatting in package names. Compares the package name against a list of popular packages using the Jaro-Winkler similarity algorithm. Packages exceeding a configurable threshold are flagged. Includes a default popular package list and an option for a custom list via configuration. Signed-off-by: Amine --- .../pypi_heuristics/metadata/typosquatting_presence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py index 71ea90988..f68899a4a 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py @@ -104,7 +104,7 @@ def substitution_func(self, char1: str, char2: str) -> float: ------- float 0.0 if the characters are the same, `self.keyboard` if they are - neighbors on a QWERTY keyboard, and `self.cost` otherwise. + neighbors on a QWERTY keyboard, otherwise `self.cost` . """ if char1 == char2: return 0.0 From b49f92bcae844adff2dc29bacde3044fe306f2a0 Mon Sep 17 00:00:00 2001 From: Amine Date: Wed, 23 Apr 2025 12:37:22 +0100 Subject: [PATCH 03/10] feat: implement typosquatting detection for package names Adds a new security analysis feature to detect potential typosquatting in package names. Compares the package name against a list of popular packages using the Jaro-Winkler similarity algorithm. Packages exceeding a configurable threshold are flagged. Includes a default popular package list and an option for a custom list via configuration. Signed-off-by: Amine --- .../metadata/typosquatting_presence.py | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py index f68899a4a..099798ba3 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py @@ -27,43 +27,43 @@ def __init__(self) -> None: self.scaling = 0.15 self.keyboard = 0.8 self.keyboard_layout = { - "1": (-1, 0), - "2": (-1, 1), - "3": (-1, 2), - "4": (-1, 3), - "5": (-1, 4), - "6": (-1, 5), - "7": (-1, 6), - "8": (-1, 7), - "9": (-1, 8), - "0": (-1, 9), - "-": (-1, 10), - "q": (0, 0), - "w": (0, 1), - "e": (0, 2), - "r": (0, 3), - "t": (0, 4), - "y": (0, 5), - "u": (0, 6), - "i": (0, 7), - "o": (0, 8), - "p": (0, 9), - "a": (1, 0), - "s": (1, 1), - "d": (1, 2), - "f": (1, 3), - "g": (1, 4), - "h": (1, 5), - "j": (1, 6), - "k": (1, 7), - "l": (1, 8), - "z": (2, 0), - "x": (2, 1), - "c": (2, 2), - "v": (2, 3), - "b": (2, 4), - "n": (2, 5), - "m": (2, 6), + "1": (0, 0), + "2": (0, 1), + "3": (0, 2), + "4": (0, 3), + "5": (0, 4), + "6": (0, 5), + "7": (0, 6), + "8": (0, 7), + "9": (0, 8), + "0": (0, 9), + "-": (0, 10), + "q": (1, 0), + "w": (1, 1), + "e": (1, 2), + "r": (1, 3), + "t": (1, 4), + "y": (1, 5), + "u": (1, 6), + "i": (1, 7), + "o": (1, 8), + "p": (1, 9), + "a": (2, 0), + "s": (2, 1), + "d": (2, 2), + "f": (2, 3), + "g": (2, 4), + "h": (2, 5), + "j": (2, 6), + "k": (2, 7), + "l": (2, 8), + "z": (3, 0), + "x": (3, 1), + "c": (3, 2), + "v": (3, 3), + "b": (3, 4), + "n": (3, 5), + "m": (3, 6), } if global_config.popular_packages_path is not None: From b9f5b640cfe87d84b1aac9eee86c1545b14272a3 Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 29 Apr 2025 11:15:39 +0100 Subject: [PATCH 04/10] feat(typosquatting): add tests and move analyzer config to defaults.ini Added unit tests for typosquatting detection. Analyzer variables, including the file path, are now loaded from defaults.ini. Raised heuristic confidence level from medium to high. BREAKING CHANGE: Analyzer config must now be defined in defaults.ini. Signed-off-by: Amine --- src/macaron/config/defaults.ini | 11 ++ .../metadata/typosquatting_presence.py | 125 +++++++++++------- .../checks/detect_malicious_metadata_check.py | 7 +- .../pypi/test_typosquatting_presence.py | 78 +++++++++++ 4 files changed, 170 insertions(+), 51 deletions(-) create mode 100644 tests/malware_analyzer/pypi/test_typosquatting_presence.py diff --git a/src/macaron/config/defaults.ini b/src/macaron/config/defaults.ini index c46e09ce1..f831732ff 100644 --- a/src/macaron/config/defaults.ini +++ b/src/macaron/config/defaults.ini @@ -600,3 +600,14 @@ major_threshold = 20 epoch_threshold = 3 # The number of days +/- the day of publish the calendar versioning day may be. day_publish_error = 4 + +# The threshold ratio for two packages to be considered similar. +distance_ratio_threshold = 0.95 +# The Keyboard cost for two characters that are close to each other on the keyboard. +keyboard = 0.8 +# The scaling factor for the jaro winkler distance. +scaling = 0.15 +# The cost for two characters that are not close to each other on the keyboard. +cost = 1.0 +# The path to the file that contains the list of popular packages. +popular_packages_path = diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py index 099798ba3..c2af634b9 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py @@ -5,6 +5,7 @@ import logging import os +from macaron.config.defaults import defaults from macaron.config.global_config import global_config from macaron.json_tools import JsonType from macaron.malware_analyzer.pypi_heuristics.base_analyzer import BaseHeuristicAnalyzer @@ -17,58 +18,88 @@ class TyposquattingPresenceAnalyzer(BaseHeuristicAnalyzer): """Check whether the PyPI package has typosquatting presence.""" + KEYBOARD_LAYOUT = { + "1": (0, 0), + "2": (0, 1), + "3": (0, 2), + "4": (0, 3), + "5": (0, 4), + "6": (0, 5), + "7": (0, 6), + "8": (0, 7), + "9": (0, 8), + "0": (0, 9), + "-": (0, 10), + "q": (1, 0), + "w": (1, 1), + "e": (1, 2), + "r": (1, 3), + "t": (1, 4), + "y": (1, 5), + "u": (1, 6), + "i": (1, 7), + "o": (1, 8), + "p": (1, 9), + "a": (2, 0), + "s": (2, 1), + "d": (2, 2), + "f": (2, 3), + "g": (2, 4), + "h": (2, 5), + "j": (2, 6), + "k": (2, 7), + "l": (2, 8), + "z": (3, 0), + "x": (3, 1), + "c": (3, 2), + "v": (3, 3), + "b": (3, 4), + "n": (3, 5), + "m": (3, 6), + } + def __init__(self) -> None: super().__init__( name="typosquatting_presence_analyzer", heuristic=Heuristics.TYPOSQUATTING_PRESENCE, depends_on=None ) - self.popular_packages_path = os.path.join(global_config.resources_path, "popular_packages.txt") - self.distance_ratio_threshold = 0.95 - self.cost = 1 - self.scaling = 0.15 - self.keyboard = 0.8 - self.keyboard_layout = { - "1": (0, 0), - "2": (0, 1), - "3": (0, 2), - "4": (0, 3), - "5": (0, 4), - "6": (0, 5), - "7": (0, 6), - "8": (0, 7), - "9": (0, 8), - "0": (0, 9), - "-": (0, 10), - "q": (1, 0), - "w": (1, 1), - "e": (1, 2), - "r": (1, 3), - "t": (1, 4), - "y": (1, 5), - "u": (1, 6), - "i": (1, 7), - "o": (1, 8), - "p": (1, 9), - "a": (2, 0), - "s": (2, 1), - "d": (2, 2), - "f": (2, 3), - "g": (2, 4), - "h": (2, 5), - "j": (2, 6), - "k": (2, 7), - "l": (2, 8), - "z": (3, 0), - "x": (3, 1), - "c": (3, 2), - "v": (3, 3), - "b": (3, 4), - "n": (3, 5), - "m": (3, 6), - } + self.popular_packages_path, self.distance_ratio_threshold, self.keyboard, self.scaling, self.cost = ( + self._load_defaults() + ) if global_config.popular_packages_path is not None: self.popular_packages_path = global_config.popular_packages_path + def _load_defaults(self) -> tuple[str, float, float, float, float]: + """Load default settings from defaults.ini. + + Returns + ------- + tuple[str, float, float, float, float]: + The Major threshold, Epoch threshold, and Day published error. + """ + section_name = "heuristic.pypi" + default_path = os.path.join(global_config.resources_path, "popular_packages.txt") + if defaults.has_section(section_name): + section = defaults[section_name] + path = section.get("popular_packages_path", default_path) + # Fall back to default if the path in defaults.ini is empty + if not path.strip(): + path = default_path + return ( + path, + section.getfloat("distance_ratio_threshold", 0.95), + section.getfloat("keyboard", 0.8), + section.getfloat("scaling", 0.15), + section.getfloat("cost", 1.0), + ) + return ( + default_path, + 0.95, + 0.8, + 0.15, + 1.0, + ) + def are_neighbors(self, char1: str, char2: str) -> bool: """Check if two characters are adjacent on a QWERTY keyboard. @@ -84,8 +115,8 @@ def are_neighbors(self, char1: str, char2: str) -> bool: bool True if the characters are neighbors, False otherwise. """ - c1 = self.keyboard_layout.get(char1) - c2 = self.keyboard_layout.get(char2) + c1 = self.KEYBOARD_LAYOUT.get(char1) + c2 = self.KEYBOARD_LAYOUT.get(char2) if not c1 or not c2: return False return (abs(c1[0] - c2[0]) <= 1) and (abs(c1[1] - c2[1]) <= 1) @@ -213,7 +244,6 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes The result and related information collected during the analysis. """ # If there is a popular packages file, check if the package name is similar to any of them - package_name = pypi_package_json.component_name if not self.popular_packages_path or not os.path.exists(self.popular_packages_path): err_msg = f"Popular packages file not found or path not configured: {self.popular_packages_path}" logger.warning("%s. Skipping typosquatting check.", err_msg) @@ -228,6 +258,7 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes logger.error(err_msg) return HeuristicResult.SKIP, {"error": err_msg} + package_name = pypi_package_json.component_name for popular_package in popular_packages: if package_name == popular_package: return HeuristicResult.PASS, {"package_name": package_name} diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index f580111ca..6df436a47 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -398,9 +398,8 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: failed({Heuristics.ANOMALOUS_VERSION.value}). % Package released with a name similar to a popular package. - {Confidence.MEDIUM.value}::trigger(malware_medium_confidence_3) :- - quickUndetailed, - failed({Heuristics.TYPOSQUATTING_PRESENCE.value}). + {Confidence.HIGH.value}::trigger(malware_high_confidence_4) :- + quickUndetailed, forceSetup, failed({Heuristics.TYPOSQUATTING_PRESENCE.value}). % ----- Evaluation ----- @@ -408,7 +407,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: {problog_result_access} :- trigger(malware_high_confidence_1). {problog_result_access} :- trigger(malware_high_confidence_2). {problog_result_access} :- trigger(malware_high_confidence_3). - {problog_result_access} :- trigger(malware_medium_confidence_3). + {problog_result_access} :- trigger(malware_high_confidence_4). {problog_result_access} :- trigger(malware_medium_confidence_2). {problog_result_access} :- trigger(malware_medium_confidence_1). query({problog_result_access}). diff --git a/tests/malware_analyzer/pypi/test_typosquatting_presence.py b/tests/malware_analyzer/pypi/test_typosquatting_presence.py new file mode 100644 index 000000000..204b328a2 --- /dev/null +++ b/tests/malware_analyzer/pypi/test_typosquatting_presence.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""Tests for the TyposquattingPresenceAnalyzer heuristic.""" +# pylint: disable=redefined-outer-name + + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult +from macaron.malware_analyzer.pypi_heuristics.metadata.typosquatting_presence import TyposquattingPresenceAnalyzer + + +@pytest.fixture() +def analyzer(tmp_path: Path) -> TyposquattingPresenceAnalyzer: + """Pytest fixture to create a TyposquattingPresenceAnalyzer instance with a dummy popular packages file.""" + # create a dummy popular packages file + pkg_file = tmp_path / "popular.txt" + pkg_file.write_text("\n".join(["requests", "flask", "pytest"])) + analyzer_instance = TyposquattingPresenceAnalyzer() + analyzer_instance.popular_packages_path = str(pkg_file) + return analyzer_instance + + +def test_analyze_exact_match_pass(analyzer: TyposquattingPresenceAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer passes when the package name is an exact match to a popular package.""" + pypi_package_json.component_name = "requests" + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.PASS + assert info == {"package_name": "requests"} + + +def test_analyze_similar_name_fail(analyzer: TyposquattingPresenceAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer fails when the package name is suspiciously similar to a popular package.""" + pypi_package_json.component_name = "reqursts" + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.FAIL + assert info["package_name"] == "reqursts" + assert info["popular_package"] == "requests" + # ratio should match or exceed threshold 0.95 + assert isinstance(info["similarity_ratio"], (int, float)) + assert info["similarity_ratio"] >= analyzer.distance_ratio_threshold + + +def test_analyze_unrelated_name_pass(analyzer: TyposquattingPresenceAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer passes when the package name is not similar to any popular package.""" + pypi_package_json.component_name = "launchable" + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.PASS + assert info == {"package_name": "launchable"} + + +def test_analyze_nonexistent_file_skip(pypi_package_json: MagicMock) -> None: + """Test the analyzer skips if the popular packages file does not exist.""" + analyzer = TyposquattingPresenceAnalyzer() + analyzer.popular_packages_path = "/path/does/not/exist.txt" + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.SKIP + error_msg = info.get("error") + assert isinstance(error_msg, str) + assert "Popular packages file not found" in error_msg + + +@pytest.mark.parametrize( + ("s1", "s2", "expected"), + [ + ("requests", "requests", 1.0), + ("reqursts", "requests", 11 / 12), + ("abcd", "wxyz", 0.0), + ], +) +def test_jaro_distance(s1: str, s2: str, expected: float) -> None: + """Test the Jaro distance calculation.""" + analyzer = TyposquattingPresenceAnalyzer() + assert analyzer.jaro_distance(s1, s2) == expected From 220c4df8ffd59f51c0d89d9d84005b9d0f3c0a78 Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 13 May 2025 10:20:25 +0100 Subject: [PATCH 05/10] chore(typosquatting): improve variable names and comments for clarity Signed-off-by: Amine --- .DS_Store | Bin 0 -> 6148 bytes src/macaron/__main__.py | 11 --- src/macaron/config/global_config.py | 5 -- .../pypi_heuristics/metadata/.DS_Store | Bin 0 -> 6148 bytes .../metadata/typosquatting_presence.py | 72 ++++++++++-------- .../checks/detect_malicious_metadata_check.py | 1 + tests/.DS_Store | Bin 0 -> 6148 bytes .../pypi/test_typosquatting_presence.py | 13 ++++ tests/parsers/.DS_Store | Bin 0 -> 6148 bytes tests/slsa_analyzer/.DS_Store | Bin 0 -> 8196 bytes 10 files changed, 53 insertions(+), 49 deletions(-) create mode 100644 .DS_Store create mode 100644 src/macaron/malware_analyzer/pypi_heuristics/metadata/.DS_Store create mode 100644 tests/.DS_Store create mode 100644 tests/parsers/.DS_Store create mode 100644 tests/slsa_analyzer/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..99617323d5172c778e09efceb4d25376027d1ef9 GIT binary patch literal 6148 zcmeHKJ5Iwu5SoZgXh1|MRIt~_utLNy+L4}F#L2S8{9-shugXOy z(ha|r0eN-_6*QwOx?b`88eduE`D9*A5TD*2zCZ1~z8o#*mR0ese{tD)H#X^x_UR0} z7c`}!wL3rVV)ZW(%V&5vxZgT?i)ZLWeV$}}RO>wajLlq}0cXG&_%{roW{afTiat66 z&VVz}Fd*ND02Ry(YsL8Kzz|yiU;=X#%%zu*oM4z4){5{zSW|(T%GP4Aro$dAE;FnZ zHJ#X+54N4zIuwq(WBm}ri8DnXodIW{&A^6U4y67cukZicNq*%FI0Ju*0Z#I9KEf?o yZSCBg)Y None: help="The directory where Macaron looks for already cloned repositories.", ) - main_parser.add_argument( - "-pp", - "--popular-packages-path", - required=False, - type=str, - default=None, - help="The path to the popular packages file used for typosquatting detection.", - dest="popular_packages_path", - ) - # Add sub parsers for each action. sub_parser = main_parser.add_subparsers(dest="action", help="Run macaron --help for help") @@ -589,7 +579,6 @@ def main(argv: list[str] | None = None) -> None: build_log_path=os.path.join(args.output_dir, "build_log"), debug_level=log_level, local_repos_path=args.local_repos_path, - popular_packages_path=args.popular_packages_path, resources_path=os.path.join(macaron.MACARON_PATH, "resources"), ) diff --git a/src/macaron/config/global_config.py b/src/macaron/config/global_config.py index 805e9e7fa..8befb4045 100644 --- a/src/macaron/config/global_config.py +++ b/src/macaron/config/global_config.py @@ -49,9 +49,6 @@ class GlobalConfig: #: The path to the local .m2 Maven repository. This attribute is None if there is no available .m2 directory. local_maven_repo: str | None = None - #: The path to the popular packages file. - popular_packages_path: str | None = None - def load( self, macaron_path: str, @@ -60,7 +57,6 @@ def load( debug_level: int, local_repos_path: str, resources_path: str, - popular_packages_path: str, ) -> None: """Initiate the GlobalConfig object. @@ -85,7 +81,6 @@ def load( self.debug_level = debug_level self.local_repos_path = local_repos_path self.resources_path = resources_path - self.popular_packages_path = popular_packages_path def load_expectation_files(self, exp_path: str) -> None: """ diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/.DS_Store b/src/macaron/malware_analyzer/pypi_heuristics/metadata/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 None: self._load_defaults() ) - if global_config.popular_packages_path is not None: - self.popular_packages_path = global_config.popular_packages_path - def _load_defaults(self) -> tuple[str, float, float, float, float]: """Load default settings from defaults.ini. Returns ------- tuple[str, float, float, float, float]: - The Major threshold, Epoch threshold, and Day published error. + The path to the popular packages file, distance ratio threshold, + keyboard awareness factor, scaling factor, and cost factor. """ section_name = "heuristic.pypi" default_path = os.path.join(global_config.resources_path, "popular_packages.txt") if defaults.has_section(section_name): section = defaults[section_name] path = section.get("popular_packages_path", default_path) - # Fall back to default if the path in defaults.ini is empty + # Fall back to default if the path in defaults.ini is empty. if not path.strip(): path = default_path return ( @@ -100,14 +98,14 @@ def _load_defaults(self) -> tuple[str, float, float, float, float]: 1.0, ) - def are_neighbors(self, char1: str, char2: str) -> bool: + def are_neighbors(self, first_char: str, second_char: str) -> bool: """Check if two characters are adjacent on a QWERTY keyboard. Parameters ---------- - char1 : str + first_char : str The first character. - char2 : str + second_char : str The second character. Returns @@ -115,20 +113,20 @@ def are_neighbors(self, char1: str, char2: str) -> bool: bool True if the characters are neighbors, False otherwise. """ - c1 = self.KEYBOARD_LAYOUT.get(char1) - c2 = self.KEYBOARD_LAYOUT.get(char2) - if not c1 or not c2: + coordinates1 = self.KEYBOARD_LAYOUT.get(first_char) + coordinates2 = self.KEYBOARD_LAYOUT.get(second_char) + if not coordinates1 or not coordinates2: return False - return (abs(c1[0] - c2[0]) <= 1) and (abs(c1[1] - c2[1]) <= 1) + return (abs(coordinates1[0] - coordinates2[0]) <= 1) and (abs(coordinates1[1] - coordinates2[1]) <= 1) - def substitution_func(self, char1: str, char2: str) -> float: + def substitution_func(self, first_char: str, second_char: str) -> float: """Calculate the substitution cost between two characters. Parameters ---------- - char1 : str + first_char : str The first character. - char2 : str + second_char : str The second character. Returns @@ -137,9 +135,9 @@ def substitution_func(self, char1: str, char2: str) -> float: 0.0 if the characters are the same, `self.keyboard` if they are neighbors on a QWERTY keyboard, otherwise `self.cost` . """ - if char1 == char2: + if first_char == second_char: return 0.0 - if self.keyboard and self.are_neighbors(char1, char2): + if self.keyboard and self.are_neighbors(first_char, second_char): return self.keyboard return self.cost @@ -161,21 +159,22 @@ def jaro_distance(self, package_name: str, popular_package_name: str) -> float: if package_name == popular_package_name: return 1.0 - len1, len2 = len(package_name), len(popular_package_name) - if len1 == 0 or len2 == 0: + package_name_len = len(package_name) + popular_package_name_len = len(popular_package_name) + if package_name_len == 0 or popular_package_name_len == 0: return 0.0 - match_distance = max(len1, len2) // 2 - 1 + match_distance = max(package_name_len, popular_package_name_len) // 2 - 1 - package_name_matches = [False] * len1 - popular_package_name_matches = [False] * len2 + package_name_matches = [False] * package_name_len + popular_package_name_matches = [False] * popular_package_name_len matches = 0 - transpositions = 0.0 # Now a float to handle partial costs + transpositions = 0.0 # a float to handle partial costs. - # Count matches - for i in range(len1): + # Count matches. + for i in range(package_name_len): start = max(0, i - match_distance) - end = min(i + match_distance + 1, len2) + end = min(i + match_distance + 1, popular_package_name_len) for j in range(start, end): if popular_package_name_matches[j]: continue @@ -188,9 +187,9 @@ def jaro_distance(self, package_name: str, popular_package_name: str) -> float: if matches == 0: return 0.0 - # Count transpositions with possible keyboard awareness + # Count transpositions with possible keyboard awareness. k = 0 - for i in range(len1): + for i in range(package_name_len): if package_name_matches[i]: while not popular_package_name_matches[k]: k += 1 @@ -198,9 +197,11 @@ def jaro_distance(self, package_name: str, popular_package_name: str) -> float: transpositions += self.substitution_func(package_name[i], popular_package_name[k]) k += 1 - transpositions /= 2.0 # Adjust for transpositions being counted twice + transpositions /= 2.0 # Adjust for transpositions being counted twice. - return (matches / len1 + matches / len2 + (matches - transpositions) / matches) / 3.0 + return ( + matches / package_name_len + matches / popular_package_name_len + (matches - transpositions) / matches + ) / 3.0 def ratio(self, package_name: str, popular_package_name: str) -> float: """Calculate the Jaro-Winkler distance ratio. @@ -243,7 +244,6 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes tuple[HeuristicResult, dict[str, JsonType]]: The result and related information collected during the analysis. """ - # If there is a popular packages file, check if the package name is similar to any of them if not self.popular_packages_path or not os.path.exists(self.popular_packages_path): err_msg = f"Popular packages file not found or path not configured: {self.popular_packages_path}" logger.warning("%s. Skipping typosquatting check.", err_msg) @@ -253,13 +253,19 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes try: with open(self.popular_packages_path, encoding="utf-8") as file: popular_packages = file.read().splitlines() - except OSError as e: - err_msg = f"Could not read popular packages file {self.popular_packages_path}: {e}" + except OSError as exception: + err_msg = f"Could not read popular packages file {self.popular_packages_path}: {exception}" logger.error(err_msg) return HeuristicResult.SKIP, {"error": err_msg} + if not popular_packages: + err_msg = f"Popular packages file is empty: {self.popular_packages_path}" + logger.warning(err_msg) + return HeuristicResult.SKIP, {"error": err_msg} + package_name = pypi_package_json.component_name for popular_package in popular_packages: + # If there is a popular packages file, check if the package name is similar to any of them. if package_name == popular_package: return HeuristicResult.PASS, {"package_name": package_name} diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index 6df436a47..dba8c3763 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -408,6 +408,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: {problog_result_access} :- trigger(malware_high_confidence_2). {problog_result_access} :- trigger(malware_high_confidence_3). {problog_result_access} :- trigger(malware_high_confidence_4). + {problog_result_access} :- trigger(malware_high_confidence_5). {problog_result_access} :- trigger(malware_medium_confidence_2). {problog_result_access} :- trigger(malware_medium_confidence_1). query({problog_result_access}). diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ec72b3bf49c2b2264081e28eb4c117d776dd8851 GIT binary patch literal 6148 zcmeHK%}T>S5T30Ci+U({5l_LphrWQs67OoSN`GuoBsImhpq_H~349Wdo)q*k1RtPp z;5R#?CZ-id5GgY-^KEu#HZvb&Crd=GKJM0tszg+QGM1NMrUxTGuU zQIk$-Sn#$9W55{rZw&CayGm_3gT|EoeiO_!DWNfqXuvi;t~HL5{y<9D-1~lHKZ&AF zoOBRTxqduckDnj5e-hpOMfBh#n{^AX#bMS(wIgT;I;38{vW=~2KG_-$Zg1}QUwjRP zz52;VwqN<>^V;RS7Qu6y&N(Bt`K%^)MUQMw7fvT@QDfl literal 0 HcmV?d00001 diff --git a/tests/malware_analyzer/pypi/test_typosquatting_presence.py b/tests/malware_analyzer/pypi/test_typosquatting_presence.py index 204b328a2..8b757d7bc 100644 --- a/tests/malware_analyzer/pypi/test_typosquatting_presence.py +++ b/tests/malware_analyzer/pypi/test_typosquatting_presence.py @@ -76,3 +76,16 @@ def test_jaro_distance(s1: str, s2: str, expected: float) -> None: """Test the Jaro distance calculation.""" analyzer = TyposquattingPresenceAnalyzer() assert analyzer.jaro_distance(s1, s2) == expected + + +def test_empty_popular_packages_file(tmp_path: Path, pypi_package_json: MagicMock) -> None: + """Test the analyzer skips when the popular packages file is empty.""" + pkg_file = tmp_path / "empty_popular.txt" + pkg_file.write_text("") # Create an empty file + analyzer = TyposquattingPresenceAnalyzer() + analyzer.popular_packages_path = str(pkg_file) + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.SKIP + error_msg = info.get("error") + assert isinstance(error_msg, str) + assert "Popular packages file is empty" in error_msg diff --git a/tests/parsers/.DS_Store b/tests/parsers/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d13e152ba04f95bceb0faca9b50af387107cce44 GIT binary patch literal 6148 zcmeHKu};G<5IsYNss&Ug5R-qQ1AC~_m8D`sDoKkdQbMX02DYqx1{(_!I|~yF3p*Pl zU%@-uw6WS0SSp0>B>Oq%yZGg)_SHnB+E;@XQIm*jXpBJ}-2&rzZgaNgJe;h|JNgt; zpN4cwy}4*{hytR(UsHhhZUeiuM2yxmPHSK5j`4~SukDNFf1?WoN~GV@0i9k!I?Am86PsA%GfgJn~!)eeK(w!#Lrm& zB;M!!z`W~}fa?V3V)YtR)wmetn z^RhqXGtDh?STygi`^4*ttyTflY_?j%qV%GGC?E<<72xwhqcMgK3ybFHKxeK1zyNMz z7@Ham)a(F;4hxI$z?4k|+EnGP7|N!@uU%Z|u&`*;Ntu=L9=Eb`Hxy-7hhLj;QlUlZ zMFCO3S76;fHo5+Hs_*}Pl01n5qQJjWKm}1ZYU7h!ZJql#uC*HN1dWa33X3KMoh!#? ez@>N>%^2o14}hV=!Xi8{`6FOtkWLg>sREy_$erK- literal 0 HcmV?d00001 diff --git a/tests/slsa_analyzer/.DS_Store b/tests/slsa_analyzer/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5ebce8cc4fff0bcef387b652deb853fb8155a398 GIT binary patch literal 8196 zcmeI1!Ab)$5QZnUO0gitlZc1CDuVh3tJIT6AE4E?v|`&WtqR^2@$M`52tI`;LGUJm z2d^HztN$diCT?RDLXhsws=b9-w+p_?x z5KmO29n=;zsFLxP4pBf95Cud5Q9u;<3ku+y&7~0d?we6*MFCOZUn;=whX|EzY;9_+ zes!SHDFAF9-MZi!d4P_Itc|Trjn$!;(_|0IhANw4C=-tHh}~ghYg1!QI4KiO%HCPo z3`Oa?BQDbJq+(;G6$M0rtOA_77m=-3s7J?M{$6P}*S2d(r>BF|;3n+ZNy_EjcCrhP z!qZA=HoCi9|IEAo4ewsdrMO_2DSO6HqkS59l#};cAIDv`>REHX{h-;rIXcNZQOL*R zD5T6W4URo1H^6yEpN?={qlU*rr)MEQm+Sg!$Tf0Kmv5Y}$Em8!S)?Y|4n8GSGbdqB zm$kmwdebm4uS+}L+vCmY=Uc*F!$ z4PS_R-y7eF^Vf9_MC_Ly=o9Re26eHvZf3vaZl6PbF8|}rkbmTy?h_d2>v85w%sB Date: Mon, 19 May 2025 11:08:34 +0100 Subject: [PATCH 06/10] refactor(core): improve logic, error handling, and apply minor fixes Signed-off-by: Amine --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 1 + .../metadata/typosquatting_presence.py | 83 ++++++++++-------- .../checks/detect_malicious_metadata_check.py | 9 +- .../pypi/test_typosquatting_presence.py | 31 +++---- 5 files changed, 63 insertions(+), 61 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 99617323d5172c778e09efceb4d25376027d1ef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Iwu5SoZgXh1|MRIt~_utLNy+L4}F#L2S8{9-shugXOy z(ha|r0eN-_6*QwOx?b`88eduE`D9*A5TD*2zCZ1~z8o#*mR0ese{tD)H#X^x_UR0} z7c`}!wL3rVV)ZW(%V&5vxZgT?i)ZLWeV$}}RO>wajLlq}0cXG&_%{roW{afTiat66 z&VVz}Fd*ND02Ry(YsL8Kzz|yiU;=X#%%zu*oM4z4){5{zSW|(T%GP4Aro$dAE;FnZ zHJ#X+54N4zIuwq(WBm}ri8DnXodIW{&A^6U4y67cukZicNq*%FI0Ju*0Z#I9KEf?o yZSCBg)Y None: + def __init__(self, popular_packages_path: str | None = None) -> None: super().__init__( name="typosquatting_presence_analyzer", heuristic=Heuristics.TYPOSQUATTING_PRESENCE, depends_on=None ) - self.popular_packages_path, self.distance_ratio_threshold, self.keyboard, self.scaling, self.cost = ( + self.default_path = os.path.join(global_config.resources_path, "popular_packages.txt") + if popular_packages_path: + self.default_path = popular_packages_path + self.popular_packages, self.distance_ratio_threshold, self.keyboard, self.scaling, self.cost = ( self._load_defaults() ) - def _load_defaults(self) -> tuple[str, float, float, float, float]: + def _load_defaults(self) -> tuple[list[str], float, float, float, float]: """Load default settings from defaults.ini. Returns ------- - tuple[str, float, float, float, float]: - The path to the popular packages file, distance ratio threshold, + tuple[list[str], float, float, float, float]: + The popular packages list, distance ratio threshold, keyboard awareness factor, scaling factor, and cost factor. """ section_name = "heuristic.pypi" - default_path = os.path.join(global_config.resources_path, "popular_packages.txt") + path = self.default_path + distance_ratio_threshold = 0.95 + keyboard = 0.8 + scaling = 0.15 + cost = 1.0 + if defaults.has_section(section_name): section = defaults[section_name] - path = section.get("popular_packages_path", default_path) + path_from_config = section.get("popular_packages_path", self.default_path) # Fall back to default if the path in defaults.ini is empty. - if not path.strip(): - path = default_path - return ( - path, - section.getfloat("distance_ratio_threshold", 0.95), - section.getfloat("keyboard", 0.8), - section.getfloat("scaling", 0.15), - section.getfloat("cost", 1.0), - ) + if path_from_config.strip(): + path = path_from_config + distance_ratio_threshold = section.getfloat("distance_ratio_threshold", 0.95) + keyboard = section.getfloat("keyboard", 0.8) + scaling = section.getfloat("scaling", 0.15) + cost = section.getfloat("cost", 1.0) + + if not path or not os.path.exists(path): + err_msg = "Popular packages file not found or path not configured" + logger.debug(err_msg) + raise HeuristicAnalyzerValueError(err_msg) + + popular_packages_list = [] + try: + with open(path, encoding="utf-8") as file: + popular_packages_list = file.read().splitlines() + except OSError as error: + err_msg = "Could not read popular packages file" + logger.debug(err_msg) + raise HeuristicAnalyzerValueError(err_msg) from error + return ( - default_path, - 0.95, - 0.8, - 0.15, - 1.0, + popular_packages_list, + distance_ratio_threshold, + keyboard, + scaling, + cost, ) def are_neighbors(self, first_char: str, second_char: str) -> bool: @@ -244,27 +265,13 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes tuple[HeuristicResult, dict[str, JsonType]]: The result and related information collected during the analysis. """ - if not self.popular_packages_path or not os.path.exists(self.popular_packages_path): - err_msg = f"Popular packages file not found or path not configured: {self.popular_packages_path}" - logger.warning("%s. Skipping typosquatting check.", err_msg) - return HeuristicResult.SKIP, {"error": err_msg} - - popular_packages = [] - try: - with open(self.popular_packages_path, encoding="utf-8") as file: - popular_packages = file.read().splitlines() - except OSError as exception: - err_msg = f"Could not read popular packages file {self.popular_packages_path}: {exception}" - logger.error(err_msg) - return HeuristicResult.SKIP, {"error": err_msg} - - if not popular_packages: - err_msg = f"Popular packages file is empty: {self.popular_packages_path}" + if not self.popular_packages: + err_msg = "Popular packages file is empty" logger.warning(err_msg) return HeuristicResult.SKIP, {"error": err_msg} package_name = pypi_package_json.component_name - for popular_package in popular_packages: + for popular_package in self.popular_packages: # If there is a popular packages file, check if the package name is similar to any of them. if package_name == popular_package: return HeuristicResult.PASS, {"package_name": package_name} diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index dba8c3763..0e23d2fa8 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -383,6 +383,10 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: failed({Heuristics.CLOSER_RELEASE_JOIN_DATE.value}), forceSetup. + % Package released with a name similar to a popular package. + {Confidence.HIGH.value}::trigger(malware_high_confidence_4) :- + quickUndetailed, forceSetup, failed({Heuristics.TYPOSQUATTING_PRESENCE.value}). + % Package released recently with little detail, with multiple releases as a trust marker, but frequent and with % the same code. {Confidence.MEDIUM.value}::trigger(malware_medium_confidence_1) :- @@ -397,10 +401,6 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: failed({Heuristics.ONE_RELEASE.value}), failed({Heuristics.ANOMALOUS_VERSION.value}). - % Package released with a name similar to a popular package. - {Confidence.HIGH.value}::trigger(malware_high_confidence_4) :- - quickUndetailed, forceSetup, failed({Heuristics.TYPOSQUATTING_PRESENCE.value}). - % ----- Evaluation ----- % Aggregate result @@ -408,7 +408,6 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: {problog_result_access} :- trigger(malware_high_confidence_2). {problog_result_access} :- trigger(malware_high_confidence_3). {problog_result_access} :- trigger(malware_high_confidence_4). - {problog_result_access} :- trigger(malware_high_confidence_5). {problog_result_access} :- trigger(malware_medium_confidence_2). {problog_result_access} :- trigger(malware_medium_confidence_1). query({problog_result_access}). diff --git a/tests/malware_analyzer/pypi/test_typosquatting_presence.py b/tests/malware_analyzer/pypi/test_typosquatting_presence.py index 8b757d7bc..4bc7de2dc 100644 --- a/tests/malware_analyzer/pypi/test_typosquatting_presence.py +++ b/tests/malware_analyzer/pypi/test_typosquatting_presence.py @@ -10,6 +10,7 @@ import pytest +from macaron.errors import HeuristicAnalyzerValueError from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult from macaron.malware_analyzer.pypi_heuristics.metadata.typosquatting_presence import TyposquattingPresenceAnalyzer @@ -19,9 +20,9 @@ def analyzer(tmp_path: Path) -> TyposquattingPresenceAnalyzer: """Pytest fixture to create a TyposquattingPresenceAnalyzer instance with a dummy popular packages file.""" # create a dummy popular packages file pkg_file = tmp_path / "popular.txt" - pkg_file.write_text("\n".join(["requests", "flask", "pytest"])) - analyzer_instance = TyposquattingPresenceAnalyzer() - analyzer_instance.popular_packages_path = str(pkg_file) + popular_packages = ["requests", "flask", "pytest"] + pkg_file.write_text("\n".join(popular_packages)) + analyzer_instance = TyposquattingPresenceAnalyzer(str(pkg_file)) return analyzer_instance @@ -53,15 +54,11 @@ def test_analyze_unrelated_name_pass(analyzer: TyposquattingPresenceAnalyzer, py assert info == {"package_name": "launchable"} -def test_analyze_nonexistent_file_skip(pypi_package_json: MagicMock) -> None: - """Test the analyzer skips if the popular packages file does not exist.""" - analyzer = TyposquattingPresenceAnalyzer() - analyzer.popular_packages_path = "/path/does/not/exist.txt" - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.SKIP - error_msg = info.get("error") - assert isinstance(error_msg, str) - assert "Popular packages file not found" in error_msg +def test_analyze_nonexistent_file_skip() -> None: + """Test the analyzer raises an error if the popular packages file does not exist.""" + with pytest.raises(HeuristicAnalyzerValueError) as exc_info: + TyposquattingPresenceAnalyzer("nonexistent_file.txt") + assert "Popular packages file not found or path not configured" in str(exc_info.value) @pytest.mark.parametrize( @@ -72,19 +69,17 @@ def test_analyze_nonexistent_file_skip(pypi_package_json: MagicMock) -> None: ("abcd", "wxyz", 0.0), ], ) -def test_jaro_distance(s1: str, s2: str, expected: float) -> None: +def test_jaro_distance(analyzer: TyposquattingPresenceAnalyzer, s1: str, s2: str, expected: float) -> None: """Test the Jaro distance calculation.""" - analyzer = TyposquattingPresenceAnalyzer() assert analyzer.jaro_distance(s1, s2) == expected def test_empty_popular_packages_file(tmp_path: Path, pypi_package_json: MagicMock) -> None: """Test the analyzer skips when the popular packages file is empty.""" pkg_file = tmp_path / "empty_popular.txt" - pkg_file.write_text("") # Create an empty file - analyzer = TyposquattingPresenceAnalyzer() - analyzer.popular_packages_path = str(pkg_file) - result, info = analyzer.analyze(pypi_package_json) + pkg_file.write_text("") + analyzer_instance = TyposquattingPresenceAnalyzer(str(pkg_file)) + result, info = analyzer_instance.analyze(pypi_package_json) assert result == HeuristicResult.SKIP error_msg = info.get("error") assert isinstance(error_msg, str) From 2b85609d45d7d290edf4cf23aba2e9e9907945cb Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 20 May 2025 13:08:45 +0100 Subject: [PATCH 07/10] refactor(core): improve logic, error handling, and apply minor fixes Signed-off-by: Amine --- .../checks/test_detect_malicious_metadata_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py b/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py index 15caf3249..689fb12e5 100644 --- a/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py +++ b/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py @@ -29,7 +29,7 @@ # heuristic, a false negative has been introduced. Note that if the unit test were allowed to access the OSV # knowledge base, it would report the package as malware. However, we intentionally block unit tests # from reaching the network. - ("pkg:pypi/zlibxjson", CheckResultType.PASSED), + ("pkg:pypi/zlibxjson", CheckResultType.UNKNOWN), ("pkg:pypi/test", CheckResultType.UNKNOWN), ("pkg:maven:test/test", CheckResultType.UNKNOWN), ], From b85a03a42aec7a4479cb43a625b5745ae960a4fe Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 20 May 2025 14:04:04 +0100 Subject: [PATCH 08/10] chore(typosquatting): improve variable names and comments for clarity Signed-off-by: Amine --- .../metadata/typosquatting_presence.py | 29 ++++++++++--------- .../pypi/test_typosquatting_presence.py | 23 ++++++++------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py index 2c45954fb..45545c210 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/typosquatting_presence.py @@ -122,6 +122,9 @@ def _load_defaults(self) -> tuple[list[str], float, float, float, float]: def are_neighbors(self, first_char: str, second_char: str) -> bool: """Check if two characters are adjacent on a QWERTY keyboard. + Adjacent characters are those that are next to each other + either horizontally, vertically, or diagonally. + Parameters ---------- first_char : str @@ -190,18 +193,18 @@ def jaro_distance(self, package_name: str, popular_package_name: str) -> float: package_name_matches = [False] * package_name_len popular_package_name_matches = [False] * popular_package_name_len matches = 0 - transpositions = 0.0 # a float to handle partial costs. + transpositions = 0.0 # A float to handle partial costs. # Count matches. - for i in range(package_name_len): - start = max(0, i - match_distance) - end = min(i + match_distance + 1, popular_package_name_len) - for j in range(start, end): - if popular_package_name_matches[j]: + for first_index in range(package_name_len): + start = max(0, first_index - match_distance) + end = min(first_index + match_distance + 1, popular_package_name_len) + for second_index in range(start, end): + if popular_package_name_matches[second_index]: continue - if package_name[i] == popular_package_name[j]: - package_name_matches[i] = True - popular_package_name_matches[j] = True + if package_name[first_index] == popular_package_name[second_index]: + package_name_matches[first_index] = True + popular_package_name_matches[second_index] = True matches += 1 break @@ -210,12 +213,12 @@ def jaro_distance(self, package_name: str, popular_package_name: str) -> float: # Count transpositions with possible keyboard awareness. k = 0 - for i in range(package_name_len): - if package_name_matches[i]: + for index in range(package_name_len): + if package_name_matches[index]: while not popular_package_name_matches[k]: k += 1 - if package_name[i] != popular_package_name[k]: - transpositions += self.substitution_func(package_name[i], popular_package_name[k]) + if package_name[index] != popular_package_name[k]: + transpositions += self.substitution_func(package_name[index], popular_package_name[k]) k += 1 transpositions /= 2.0 # Adjust for transpositions being counted twice. diff --git a/tests/malware_analyzer/pypi/test_typosquatting_presence.py b/tests/malware_analyzer/pypi/test_typosquatting_presence.py index 4bc7de2dc..5530bf2ad 100644 --- a/tests/malware_analyzer/pypi/test_typosquatting_presence.py +++ b/tests/malware_analyzer/pypi/test_typosquatting_presence.py @@ -5,6 +5,7 @@ # pylint: disable=redefined-outer-name +import os from pathlib import Path from unittest.mock import MagicMock @@ -18,10 +19,10 @@ @pytest.fixture() def analyzer(tmp_path: Path) -> TyposquattingPresenceAnalyzer: """Pytest fixture to create a TyposquattingPresenceAnalyzer instance with a dummy popular packages file.""" - # create a dummy popular packages file - pkg_file = tmp_path / "popular.txt" + # Create a dummy popular packages file. + pkg_file = Path(os.path.join(tmp_path, "popular.txt")) popular_packages = ["requests", "flask", "pytest"] - pkg_file.write_text("\n".join(popular_packages)) + pkg_file.write_text("\n".join(popular_packages), encoding="utf-8") analyzer_instance = TyposquattingPresenceAnalyzer(str(pkg_file)) return analyzer_instance @@ -41,8 +42,8 @@ def test_analyze_similar_name_fail(analyzer: TyposquattingPresenceAnalyzer, pypi assert result == HeuristicResult.FAIL assert info["package_name"] == "reqursts" assert info["popular_package"] == "requests" - # ratio should match or exceed threshold 0.95 - assert isinstance(info["similarity_ratio"], (int, float)) + # The ratio should match or exceed threshold. + assert isinstance(info["similarity_ratio"], float) assert info["similarity_ratio"] >= analyzer.distance_ratio_threshold @@ -62,22 +63,24 @@ def test_analyze_nonexistent_file_skip() -> None: @pytest.mark.parametrize( - ("s1", "s2", "expected"), + ("package1", "package2", "expected_ratio"), [ ("requests", "requests", 1.0), ("reqursts", "requests", 11 / 12), ("abcd", "wxyz", 0.0), ], ) -def test_jaro_distance(analyzer: TyposquattingPresenceAnalyzer, s1: str, s2: str, expected: float) -> None: +def test_jaro_distance( + analyzer: TyposquattingPresenceAnalyzer, package1: str, package2: str, expected_ratio: float +) -> None: """Test the Jaro distance calculation.""" - assert analyzer.jaro_distance(s1, s2) == expected + assert analyzer.jaro_distance(package1, package2) == expected_ratio def test_empty_popular_packages_file(tmp_path: Path, pypi_package_json: MagicMock) -> None: """Test the analyzer skips when the popular packages file is empty.""" - pkg_file = tmp_path / "empty_popular.txt" - pkg_file.write_text("") + pkg_file = Path(os.path.join(tmp_path, "empty_popular.txt")) + pkg_file.write_text("", encoding="utf-8") analyzer_instance = TyposquattingPresenceAnalyzer(str(pkg_file)) result, info = analyzer_instance.analyze(pypi_package_json) assert result == HeuristicResult.SKIP From 39d728923f9395f082c95d7acdd5eabdd9a7ea70 Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 20 May 2025 22:33:14 +0100 Subject: [PATCH 09/10] docs: update docs and refine existing test Signed-off-by: Amine --- src/macaron/malware_analyzer/README.md | 7 ++++++- tests/malware_analyzer/pypi/test_typosquatting_presence.py | 5 ++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/macaron/malware_analyzer/README.md b/src/macaron/malware_analyzer/README.md index d5d30a670..af8451279 100644 --- a/src/macaron/malware_analyzer/README.md +++ b/src/macaron/malware_analyzer/README.md @@ -52,6 +52,11 @@ When a heuristic fails, with `HeuristicResult.FAIL`, then that is an indicator b - **Rule**: Return `HeuristicResult.FAIL` if the major or epoch is abnormally high; otherwise, return `HeuristicResult.PASS`. - **Dependency**: Will be run if the One Release heuristic fails. +10. **Typosquatting Presence** + - **Description**: Checks if the package name is suspiciously similar to any package name in a predefined list of popular packages. The similarity check incorporates the Jaro-Winkler distance and considers keyboard layout proximity to identify potential typosquatting. + - **Rule**: Return `HeuristicResult.FAIL` if the similarity ratio between the package name and any popular package name meets or exceeds a defined threshold; otherwise, return `HeuristicResult.PASS`. + - **Dependency**: None. + ### Contributing When contributing an analyzer, it must meet the following requirements: @@ -64,7 +69,7 @@ When contributing an analyzer, it must meet the following requirements: - Ensure it is assigned to the `problog_result_access` string variable, otherwise it will not be queried and evaluated. - Assign a rule ID to the rule. This will be used to backtrack to determine if it was triggered. - Make sure to wrap pass/fail statements in `passed()` and `failed()`. Not doing so may result in undesirable behaviour, see the comments in the model for more details. - - If there are commonly used combinations introduced by adding the heuristic, combine and justify them at the top of the static model (see `quickUndetailed` and `forceSetup` as current examples). + - If there are commonly used combinations introduced by adding the heuristic, combine and justify them at the top of the static model (see `quickUndetailed` and `forceSetup` as current examples). ### Confidence Score Motivation diff --git a/tests/malware_analyzer/pypi/test_typosquatting_presence.py b/tests/malware_analyzer/pypi/test_typosquatting_presence.py index 5530bf2ad..cd08c3f72 100644 --- a/tests/malware_analyzer/pypi/test_typosquatting_presence.py +++ b/tests/malware_analyzer/pypi/test_typosquatting_presence.py @@ -2,7 +2,6 @@ # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """Tests for the TyposquattingPresenceAnalyzer heuristic.""" -# pylint: disable=redefined-outer-name import os @@ -16,8 +15,8 @@ from macaron.malware_analyzer.pypi_heuristics.metadata.typosquatting_presence import TyposquattingPresenceAnalyzer -@pytest.fixture() -def analyzer(tmp_path: Path) -> TyposquattingPresenceAnalyzer: +@pytest.fixture(name="analyzer") +def analyzer_(tmp_path: Path) -> TyposquattingPresenceAnalyzer: """Pytest fixture to create a TyposquattingPresenceAnalyzer instance with a dummy popular packages file.""" # Create a dummy popular packages file. pkg_file = Path(os.path.join(tmp_path, "popular.txt")) From 2073903d1fe4bbce866eef941ed53692c02620ef Mon Sep 17 00:00:00 2001 From: Amine Date: Thu, 29 May 2025 14:44:33 +0100 Subject: [PATCH 10/10] refactor: adjust code to pass failing test and improve variable handling Signed-off-by: Amine --- .../pypi_heuristics/metadata/.DS_Store | Bin 6148 -> 0 bytes .../metadata/typosquatting_presence.py | 22 +++++++++--------- tests/.DS_Store | Bin 6148 -> 0 bytes .../pypi/test_typosquatting_presence.py | 2 +- tests/parsers/.DS_Store | Bin 6148 -> 0 bytes tests/slsa_analyzer/.DS_Store | Bin 8196 -> 0 bytes .../test_detect_malicious_metadata_check.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 src/macaron/malware_analyzer/pypi_heuristics/metadata/.DS_Store delete mode 100644 tests/.DS_Store delete mode 100644 tests/parsers/.DS_Store delete mode 100644 tests/slsa_analyzer/.DS_Store diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/.DS_Store b/src/macaron/malware_analyzer/pypi_heuristics/metadata/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 None: super().__init__( name="typosquatting_presence_analyzer", heuristic=Heuristics.TYPOSQUATTING_PRESENCE, depends_on=None ) - self.default_path = os.path.join(global_config.resources_path, "popular_packages.txt") + self.default_path = os.path.join(MACARON_PATH, "resources/popular_packages.txt") if popular_packages_path: self.default_path = popular_packages_path self.popular_packages, self.distance_ratio_threshold, self.keyboard, self.scaling, self.cost = ( @@ -98,18 +98,18 @@ def _load_defaults(self) -> tuple[list[str], float, float, float, float]: cost = section.getfloat("cost", 1.0) if not path or not os.path.exists(path): - err_msg = "Popular packages file not found or path not configured" - logger.debug(err_msg) - raise HeuristicAnalyzerValueError(err_msg) + error_message = "Popular packages file not found or path not configured" + logger.debug(error_message) + raise HeuristicAnalyzerValueError(error_message) popular_packages_list = [] try: with open(path, encoding="utf-8") as file: popular_packages_list = file.read().splitlines() except OSError as error: - err_msg = "Could not read popular packages file" - logger.debug(err_msg) - raise HeuristicAnalyzerValueError(err_msg) from error + error_message = "Could not read popular packages file" + logger.debug(error_message) + raise HeuristicAnalyzerValueError(error_message) from error return ( popular_packages_list, @@ -269,9 +269,9 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes The result and related information collected during the analysis. """ if not self.popular_packages: - err_msg = "Popular packages file is empty" - logger.warning(err_msg) - return HeuristicResult.SKIP, {"error": err_msg} + warning_message = "Popular packages file is empty" + logger.warning(warning_message) + return HeuristicResult.SKIP, {"warning": warning_message} package_name = pypi_package_json.component_name for popular_package in self.popular_packages: diff --git a/tests/.DS_Store b/tests/.DS_Store deleted file mode 100644 index ec72b3bf49c2b2264081e28eb4c117d776dd8851..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5T30Ci+U({5l_LphrWQs67OoSN`GuoBsImhpq_H~349Wdo)q*k1RtPp z;5R#?CZ-id5GgY-^KEu#HZvb&Crd=GKJM0tszg+QGM1NMrUxTGuU zQIk$-Sn#$9W55{rZw&CayGm_3gT|EoeiO_!DWNfqXuvi;t~HL5{y<9D-1~lHKZ&AF zoOBRTxqduckDnj5e-hpOMfBh#n{^AX#bMS(wIgT;I;38{vW=~2KG_-$Zg1}QUwjRP zz52;VwqN<>^V;RS7Qu6y&N(Bt`K%^)MUQMw7fvT@QDfl diff --git a/tests/malware_analyzer/pypi/test_typosquatting_presence.py b/tests/malware_analyzer/pypi/test_typosquatting_presence.py index cd08c3f72..2d23233a2 100644 --- a/tests/malware_analyzer/pypi/test_typosquatting_presence.py +++ b/tests/malware_analyzer/pypi/test_typosquatting_presence.py @@ -83,6 +83,6 @@ def test_empty_popular_packages_file(tmp_path: Path, pypi_package_json: MagicMoc analyzer_instance = TyposquattingPresenceAnalyzer(str(pkg_file)) result, info = analyzer_instance.analyze(pypi_package_json) assert result == HeuristicResult.SKIP - error_msg = info.get("error") + error_msg = info.get("warning") assert isinstance(error_msg, str) assert "Popular packages file is empty" in error_msg diff --git a/tests/parsers/.DS_Store b/tests/parsers/.DS_Store deleted file mode 100644 index d13e152ba04f95bceb0faca9b50af387107cce44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu};G<5IsYNss&Ug5R-qQ1AC~_m8D`sDoKkdQbMX02DYqx1{(_!I|~yF3p*Pl zU%@-uw6WS0SSp0>B>Oq%yZGg)_SHnB+E;@XQIm*jXpBJ}-2&rzZgaNgJe;h|JNgt; zpN4cwy}4*{hytR(UsHhhZUeiuM2yxmPHSK5j`4~SukDNFf1?WoN~GV@0i9k!I?Am86PsA%GfgJn~!)eeK(w!#Lrm& zB;M!!z`W~}fa?V3V)YtR)wmetn z^RhqXGtDh?STygi`^4*ttyTflY_?j%qV%GGC?E<<72xwhqcMgK3ybFHKxeK1zyNMz z7@Ham)a(F;4hxI$z?4k|+EnGP7|N!@uU%Z|u&`*;Ntu=L9=Eb`Hxy-7hhLj;QlUlZ zMFCO3S76;fHo5+Hs_*}Pl01n5qQJjWKm}1ZYU7h!ZJql#uC*HN1dWa33X3KMoh!#? ez@>N>%^2o14}hV=!Xi8{`6FOtkWLg>sREy_$erK- diff --git a/tests/slsa_analyzer/.DS_Store b/tests/slsa_analyzer/.DS_Store deleted file mode 100644 index 5ebce8cc4fff0bcef387b652deb853fb8155a398..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeI1!Ab)$5QZnUO0gitlZc1CDuVh3tJIT6AE4E?v|`&WtqR^2@$M`52tI`;LGUJm z2d^HztN$diCT?RDLXhsws=b9-w+p_?x z5KmO29n=;zsFLxP4pBf95Cud5Q9u;<3ku+y&7~0d?we6*MFCOZUn;=whX|EzY;9_+ zes!SHDFAF9-MZi!d4P_Itc|Trjn$!;(_|0IhANw4C=-tHh}~ghYg1!QI4KiO%HCPo z3`Oa?BQDbJq+(;G6$M0rtOA_77m=-3s7J?M{$6P}*S2d(r>BF|;3n+ZNy_EjcCrhP z!qZA=HoCi9|IEAo4ewsdrMO_2DSO6HqkS59l#};cAIDv`>REHX{h-;rIXcNZQOL*R zD5T6W4URo1H^6yEpN?={qlU*rr)MEQm+Sg!$Tf0Kmv5Y}$Em8!S)?Y|4n8GSGbdqB zm$kmwdebm4uS+}L+vCmY=Uc*F!$ z4PS_R-y7eF^Vf9_MC_Ly=o9Re26eHvZf3vaZl6PbF8|}rkbmTy?h_d2>v85w%sB