diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..8fcf2584b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Exclude local virtual environment from Docker build context +.venv/ +venv/ +env/ +ENV/ + +# Exclude other local development files +.vscode/ +.idea/ +*.pyc +__pycache__/ +.pytest_cache/ +.mypy_cache/ +*.egg-info/ +dist/ +build/ + +# Exclude temporary and cache directories +.tmp/ +.cache/ +*.log + +# SSL certificates must never be baked into the image +deploy/nginx/*.pem diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..12604de7c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +NVIDIA_API_KEY= +NV_BASE_URL=https://inference-api.nvidia.com/v1 +TAVILY_API_KEY= diff --git a/.github/workflows/test-deploy-cve-notebook.yml b/.github/workflows/test-deploy-cve-notebook.yml new file mode 100644 index 000000000..c8a704c05 --- /dev/null +++ b/.github/workflows/test-deploy-cve-notebook.yml @@ -0,0 +1,122 @@ +name: Test Deploy CVE Notebook + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run-notebook: + runs-on: arc-runners-org-nvidia-ai-bp-1-gpu + env: + NOTEBOOK_PATH: ./deploy/1_Deploy_CVE.ipynb + PYTHON_VERSION: 3.12 + steps: + - name: Checkout BP repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: | + requirements.txt + **/*.ipynb + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install python-dotenv + pip install --upgrade pip ipython ipykernel + ipython kernel install --name "python3" --user + + # Get System Info + echo "===================== System Info =====================" + more /etc/os-release + docker version + docker compose version + + - name: Run Jupyter Notebook + env: + GHSA_API_KEY: ${{ secrets.GHSA_API_KEY }} + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} + SERPAPI_API_KEY: ${{ secrets.SERPAPI_API_KEY }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + run: | + OUTPUT_NOTEBOOK="result.ipynb" + echo "Executing notebook: $NOTEBOOK_PATH" + papermill "$NOTEBOOK_PATH" "$OUTPUT_NOTEBOOK" --log-output --log-level DEBUG + + - name: Convert result to html format + if: always() + run: | + OUTPUT_NOTEBOOK="result.ipynb" + jupyter nbconvert --to html "$OUTPUT_NOTEBOOK" + + + - name: Run Test Code + if: always() + env: + TEST_DOCKER_PULL_KEY: ${{ secrets.TEST_DOCKER_PULL_KEY }} + run: | + # Check if the HTML files exist before running tests + if [ ! -f "./result.html" ]; then + echo "Warning: result.html not found" + fi + + # Run the test and capture the exit code + echo "$TEST_DOCKER_PULL_KEY" |docker login nvcr.io --username '$oauthtoken' --password-stdin + docker run --rm \ + -v ./result.html:/app/input/vulnerability_analysis/Launchable-VACS-2.1.0-result.html \ + -v "$(pwd):/workspace" \ + nvcr.io/rw983xdqtcdp/auto_test_team/blueprint-github-test-image:latest \ + pytest -m vacs --disable-warnings --html=/workspace/vacs.html --self-contained-html + + - name: Upload the result notebook as artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: result notebook and pytest + path: | + result.html + vacs.html + retention-days: 30 + + - name: Set result output + id: set_result + if: always() + run: | + echo "RESULT=$(if [ ${{ job.status }} == 'success' ]; then echo 'PASS'; else echo 'FAIL'; fi)" >> $GITHUB_OUTPUT + + - name: Send mail + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + if: always() + with: + server_address: smtp.gmail.com + server_port: 587 + username: ${{ secrets.SMTP_USERNAME }} + password: ${{ secrets.SMTP_PASSWORD }} + + # Email details + subject: "QA Test Workflow Result for ${{ github.repository }}" + to: Github-Action-Blueprint-QA@nvidia.com + from: github-workflow-notification@gmail.com + html_body: | +

Hello,

+ +

The workflow for repository: ${{ github.repository }} has completed.
+ Result: ${{ steps.set_result.outputs.RESULT }}

+ +

You can review the details on GitHub:
+ ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

+ +

Thanks!

diff --git a/.gitignore b/.gitignore index aa5db811f..ba56ca0cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ ###### Place new entries directly below this line! ###### +outputs/ # Ignore anything in the ./.tmp directory .tmp/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..f745ba698 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "aiq"] + path = aiq + url = https://github.com/NVIDIA-AI-Blueprints/aiq.git + branch = develop diff --git a/.tmp/vulnerability_markdown_reports/vulnerability_reports_morpheus_23_11-runtime/CVE-2023-36632.md b/.tmp/vulnerability_markdown_reports/vulnerability_reports_morpheus_23_11-runtime/CVE-2023-36632.md deleted file mode 100644 index 0e20a23ea..000000000 --- a/.tmp/vulnerability_markdown_reports/vulnerability_reports_morpheus_23_11-runtime/CVE-2023-36632.md +++ /dev/null @@ -1,394 +0,0 @@ - - -# Vulnerability Analysis Report for CVE-2023-36632 -> **Container Analyzed:** `nvcr.io/nvidia/morpheus/morpheus:23.11-runtime` - - -> **SBOM Info:** `type='file' file_path='data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom'` - - -> **Status:** Exploitable -## CVE-2023-36632 -> GHSA-ID: GHSA-gv66-v8c8-v69c - -`Disputed`[source](https://nvd.nist.gov/vuln/detail/CVE-2023-36632) - - - The legacy email.utils.parseaddr function in Python through 3.11.4 allows attackers to trigger "RecursionError: maximum recursion depth exceeded while calling a Python object" via a crafted argument. This argument is plausibly an untrusted value from an application's input data that was supposed to contain a name and an e-mail address. NOTE: email.utils.parseaddr is categorized as a Legacy API in the documentation of the Python email package. Applications should instead use the email.parser.BytesParser or email.parser.Parser class. [references](#ref) - - -### Severity and CVSS Score -| Source | Severity | CVSS Score | Vector | Published At | Last Updated | -|---|---|---|---|---|---| -| [**GHSA**](https://github.com/advisories/GHSA-gv66-v8c8-v69c) |UNKNOWN|N/A|N/A|2023-06-25|2023-11-06 -| [**NVD**](https://nvd.nist.gov/vuln/detail/CVE-2023-36632) |HIGH|7.5|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H|2023-06-25|2024-11-21| -| [**RHSA**](https://access.redhat.com/security/cve/CVE-2023-36632) |N/A|N/A|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N|2023-06-25|N/A| -| [**Ubuntu**](https://ubuntu.com/security/CVE-2023-36632) |HIGH|7.5|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H|2023-06-25|2024-07-24| - - -### EPSS Score -| EPSS | Percentile | -| --- | --- | -| 0.00121 | 0.47904 | - -## Vulnerability Analysis - -### Summary (Exploitable) -Based on the provided Checklist and Findings, the CVE is exploitable. Specifically, the application does not handle exceptions like `RecursionError` when processing input data (Checklist Item 1), and the Docker container's error handling mechanisms are not explicitly designed to handle `RecursionError` exceptions (Checklist Item 2). Additionally, there is a potential source of uncontrolled recursion in the `ProducerConsumerQueue` class due to the use of a recursive lock (Checklist Item 3). These findings suggest that the CVE is exploitable, as the application and Docker container do not have adequate measures in place to prevent or handle `RecursionError` exceptions. - -### Justification - - ->label: vulnerable - -The analysis concludes that the CVE is exploitable due to the application's and Docker container's inadequate handling of `RecursionError` exceptions and the presence of a potential source of uncontrolled recursion, indicating that the package is vulnerable to the CVE and needs to be patched. - ---- -### Vulnerable SBOM Dependencies - - -| SBOM Name | SBOM Version | Dependency Name | Dependency Version | Relation | System | -| --- | --- | --- | --- | --- | --- | -| python | 3.10.12 | python | 3.10.12 | SELF | binary | -| python | 3.10.13 | python | 3.10.13 | SELF | binary | -### Checklist -1. [Assess Input Data Handling](#checklist-step-1) - - 1. [Docker Container Code QA System](#checklist-step-1.1) - 2. [Docker Container Code QA System](#checklist-step-1.2) - 3. [Docker Container Developer Guide QA System](#checklist-step-1.3) -2. [Review Error Handling Mechanisms](#checklist-step-2) - - 1. [Docker Container Code QA System](#checklist-step-2.1) - 2. [Docker Container Developer Guide QA System](#checklist-step-2.2) - 3. [Internet Search](#checklist-step-2.3) -3. [Inspect Code for Uncontrolled Recursion](#checklist-step-3) - - 1. [Docker Container Code QA System](#checklist-step-3.1) - 2. [Docker Container Code QA System](#checklist-step-3.2) - -## Checklist Details - -## Step 1 : Assess Input Data Handling - - -> **Input**: *Assess Input Data Handling: Evaluate how the application handles exceptions like `RecursionError`, as the exploit triggers this specific error. Check if the application processes potentially untrusted input data that could include crafted arguments designed to exploit this vulnerability.* - -> **Response**: *The application does not handle exceptions like `RecursionError` when processing input data, but it does handle `RecursionError` in the logging part of the application. Additionally, the application processes input data, but it's unclear whether this input data is potentially untrusted or could include crafted arguments designed to exploit a vulnerability. The application attempts to validate and sanitize input data, but it's still unclear whether this is sufficient to prevent exploitation. The purpose and functionality of the Docker container, and how it handles input data, is not explicitly explained in the provided context.* - -### Step 1.1 : *Docker Container Code QA System* - - -#### Action Log -
Thought: To assess the input data handling of the application, I need to understand how the application processes input data and if it has any mechanisms in place to handle exceptions like `RecursionError`. I also need to check if the application processes potentially untrusted input data that could include crafted arguments designed to exploit this vulnerability.
-
-Action: Docker Container Code QA System
-Action Input: Does the application handle exceptions like `RecursionError` when processing input data?
- 
- - -#### Tool Input -
Does the application handle exceptions like `RecursionError` when processing input data?
- 
- - -#### Tool Output -
No, the application does not handle exceptions like `RecursionError` when processing input data. According to the provided context, `RecursionError` is only handled in the logging part of the application, specifically when writing log messages. It is not handled in the input data processing part.
- - Source Documents - - | ID | Type | source |... content_type |... language | Page Content | -| --- | --- | --- | --- | --- | --- | -| None | Document | morpheus/service/vdb/milvus_vector_db_service.py | functions_classes | python |
View ContentReturns ------- typing.Any The search result, which can vary depending on the query and options. Raises ------ RuntimeError If an error occurs during the search operation. If query argument is `None` and `data` keyword argument doesn't exist. If `data` keyword arguement is `None`. """ logger.debug("Searching in collection: %s, query=%s, kwargs=%s", self.\_name, query, kwargs) return self.\_client.query(collection\_name=self.\_name, filter=query, \*\*kwargs)
| -| None | Document | morpheus/stages/input/appshield_source_stage.py | functions_classes | python |
View Contentself.\_encoding = encoding self.\_input\_count = None self.\_watcher = DirectoryWatcher(input\_glob=input\_glob, watch\_directory=watch\_directory, max\_files=max\_files, sort\_glob=sort\_glob, recursive=recursive, queue\_max\_size=queue\_max\_size, batch\_timeout=batch\_timeout) @property def name(self) -> str: return "from-appshield" @property def input\_count(self) -> int: """Return None for no max intput count""" return self.\_input\_count def supports\_cpp\_node(self): return False
| -| None | Document | morpheus/stages/input/appshield_source_stage.py | functions_classes | python |
View ContentIf true the list of files matching `input\_glob` will be processed in sorted order. recursive : bool, default = True If true, events will be emitted for the files in subdirectories matching `input\_glob`. queue\_max\_size : int, default = 128 Maximum queue size to hold the file paths to be processed that match `input\_glob`. batch\_timeout : float, default = 5.0 Timeout to retrieve batch messages from the queue. encoding : str, default = latin1 Encoding to read a file. """
| -| None | Document | morpheus/utils/logger.py | functions_classes | python |
View Contentfile = self.\_stderr if is\_error else self.\_stdout color\_kwargs = self.\_determine\_color(record.levelno) with tqdm.external\_write\_mode(file=file, nolock=False): # Write the message click.echo(click.style(msg, \*\*color\_kwargs), file=file, err=is\_error) self.flush() # See issue 36272 https://bugs.python.org/issue36272 except (KeyboardInterrupt, SystemExit, RecursionError): # noqa raise except Exception: # pylint: disable=broad-except self.handleError(record)
| - - - - -[back to top](#checklist-toc) - -### Step 1.2 : *Docker Container Code QA System* - - -#### Action Log -
Thought: The application does not handle exceptions like `RecursionError` when processing input data. However, it does handle `RecursionError` in the logging part of the application. This suggests that the application may not be robust against crafted arguments designed to exploit this vulnerability.
-
-Action: Docker Container Code QA System
-Action Input: Does the application process potentially untrusted input data that could include crafted arguments designed to exploit this vulnerability?
- 
- - -#### Tool Input -
Does the application process potentially untrusted input data that could include crafted arguments designed to exploit this vulnerability?
- 
- - -#### Tool Output -
Based on the provided context, it appears that the application does process input data, specifically `user_inputs` and `input_names`, which are used to create mappings and validate input names. However, it's unclear whether this input data is potentially untrusted or could include crafted arguments designed to exploit a vulnerability.
-
-The tests `ProcessInputNamesMismatchMultipleMappings` and `ProcessInputNamesMismatchSingleMapping` suggest that the application is designed to handle invalid input and throw exceptions when input names do not match. This implies that the application is attempting to validate and sanitize input data, but it's still unclear whether this is sufficient to prevent exploitation.
-
-Therefore, the answer to the question is: I don't know.
- - Source Documents - - | ID | Type | source |... content_type |... language | Page Content | -| --- | --- | --- | --- | --- | --- | -| None | Document | morpheus/_lib/tests/llm/test_utils.cpp | functions_classes | cpp |
View ContentTEST\_F(TestLLMUtils, ProcessInputNamesMismatchMultipleMappings) { auto user\_inputs = llm::user\_input\_mappings\_t{{"/ext1", "input1"}, {"/ext2", "input3"}}; auto input\_names = std::vector<std::string>{"input1", "input2"}; EXPECT\_THROW(llm::process\_input\_names(user\_inputs, input\_names), std::invalid\_argument); }
| -| None | Document | morpheus/_lib/include/morpheus/messages/memory/inference_memory_nlp.hpp | functions_classes | cpp |
View Contentstruct InferenceMemoryNLPInterfaceProxy : public InferenceMemoryInterfaceProxy { /\*\* \* @brief Create and initialize an InferenceMemoryNLP object, and return a shared pointer to the result \* \* @param count : Message count in inference memory object \* @param input\_ids : The token-ids for each string padded with 0s to max\_length \* @param input\_mask : The mask for token-ids result where corresponding positions identify valid token-id values \* @param seq\_ids : Ids used to index from an inference input to a message. Necessary since there can be more inference inputs than messages (i.e., if some messages get broken into multiple inference requests) \* @return std::shared\_ptr<InferenceMemoryNLP> \*/
| -| None | Document | morpheus/_lib/tests/llm/test_utils.cpp | functions_classes | cpp |
View ContentTEST\_F(TestLLMUtils, ProcessInputNamesMismatchSingleMapping) { auto user\_inputs = llm::user\_input\_mappings\_t{{"/ext1", "input2"}}; auto input\_names = std::vector<std::string>{"input1"}; EXPECT\_THROW(llm::process\_input\_names(user\_inputs, input\_names), std::invalid\_argument); }
| -| None | Document | morpheus/_lib/messages/module.cpp | functions_classes | cpp |
View Content&InferenceMemoryNLPInterfaceProxy::set\_input\_mask) .def\_property( "seq\_ids", &InferenceMemoryNLPInterfaceProxy::get\_seq\_ids, &InferenceMemoryNLPInterfaceProxy::set\_seq\_ids);
| - - - - -[back to top](#checklist-toc) - -### Step 1.3 : *Docker Container Developer Guide QA System* - - -#### Action Log -
Thought: The application does not handle exceptions like `RecursionError` when processing input data, but it does handle `RecursionError` in the logging part of the application. Additionally, the application processes input data, but it's unclear whether this input data is potentially untrusted or could include crafted arguments designed to exploit a vulnerability. The application attempts to validate and sanitize input data, but it's still unclear whether this is sufficient to prevent exploitation.
-
-Action: Docker Container Developer Guide QA System
-Action Input: What is the purpose and functionality of the Docker container, and how does it handle input data?
- 
- - -#### Tool Input -
What is the purpose and functionality of the Docker container, and how does it handle input data?
- 
- - -#### Tool Output -
Unfortunately, the provided context does not explicitly explain the purpose and functionality of the Docker container or how it handles input data. The context mentions that Docker containers can be used to get started with Morpheus, and that pre-built Docker containers can be used or built from source, but it does not provide further details on their purpose or functionality.
-
-However, based on the general knowledge of Docker containers, it can be inferred that the Docker container is likely used to provide a self-contained environment for running the Morpheus pipeline, including the data processing stages and modules such as the `file_batcher`. But without more specific information, it's difficult to provide a more detailed answer.
-
-So, the answer is: I don't know.
- - Source Documents - - | ID | Type | source | Page Content | -| --- | --- | --- | --- | -| None | Document | examples/llm/rag/README.md |
View Content#### Data Input Points The pipeline has multiple data input avenues: 1. \*\*User Uploaded Documents\*\*: Raw documents provided by users for further processing. 2. \*\*Streaming Event Logs\*\*: Logs that are streamed in real-time. 3. \*\*Streaming Data Feeds\*\*: Continuous streams of data that could be dynamic in nature. 4. \*\*RSS Threat Intel Feeds\*\*: RSS-based feeds that might focus on threat intelligence. 5. \*\*LLM Input Query\*\*: Queries that are inputted for processing by the Large Language Model. #### Data Processing Stages The ingested data traverses several processing stages:
| -| None | Document | docs/source/index.rst |
View ContentGetting Started --------------- Using Morpheus ^^^^^^^^^^^^^^ \* :doc:`getting\_started` - Using pre-built Docker containers, building Docker containers from source, and fetching models and datasets \* :doc:`basics/overview` - Brief overview of the command line interface \* :doc:`basics/building\_a\_pipeline` - Introduction to building a pipeline using the command line interface \* :doc:`Morpheus Examples <examples>` - Example pipelines using both the Python API and command line interface \* :doc:`Pretrained Models <models\_and\_datasets>` - Pretrained models with corresponding training, validation scripts, and datasets \* :doc:`Developer Guides <developer\_guide/guides>` - Covers extending Morpheus with custom stages
| -| None | Document | docs/source/developer_guide/guides/10_modular_pipeline_digital_fingerprinting.md |
View ContentThe `file\_batcher` module is a component that is responsible for loading input files, filtering out files older than the specified time window, and grouping the remaining files by periods that fall within the time window. This module offers configurability for parameters such as batching options, cache directory, file type, filtering null values, data schema, and the timestamp column name. The `file\_batcher` module processes control messages, validates them, and generates a list of files with their timestamps. The module then groups files by the given period, creates control messages for each batch, and sends them downstream for further processing. A node function is used to handle the processing of control messages, and input and output ports are registered to integrate the module into
| -| None | Document | docs/source/developer_guide/guides/10_modular_pipeline_digital_fingerprinting.md |
View Contentprocessing. A node function is used to handle the processing of control messages, and input and output ports are registered to integrate the module into the data processing pipeline seamlessly.
| - - - - -## Step 2 : Review Error Handling Mechanisms - - -> **Input**: *Review Error Handling Mechanisms: Since the vendor's perspective is that this is neither a vulnerability nor a bug, but rather an expected behavior when limits are exceeded, review the application's error handling mechanisms to ensure they can properly handle `RecursionError` exceptions without causing unintended behavior or crashes.* - -> **Response**: *The Docker container's error handling mechanisms are not explicitly designed to handle `RecursionError` exceptions. However, the container's code does include some error handling mechanisms, such as raising `ValueError` exceptions in certain cases and catching `NotThisMethod` exceptions. To handle `RecursionError` exceptions, it is recommended to ensure that recursive functions have a terminating condition and to avoid exceeding the maximum recursion depth. Additionally, exceptions can be handled using try-except blocks.* - -### Step 2.1 : *Docker Container Code QA System* - - -#### Action Log -
Thought: To review the error handling mechanisms of the Docker container, I need to understand how the container handles `RecursionError` exceptions. I should first check if the container's code or any of its dependencies use a function or component that could potentially raise a `RecursionError`.
-
-Action: Docker Container Code QA System
-Action Input: Does the Docker container's code or any of its dependencies use a function or component that could raise a `RecursionError`?
- 
- - -#### Tool Input -
Does the Docker container's code or any of its dependencies use a function or component that could raise a `RecursionError`?
- 
- - -#### Tool Output -
I don't know.
- - Source Documents - - | ID | Type | source |... content_type |... language | Page Content | -| --- | --- | --- | --- | --- | --- | -| None | Document | morpheus/service/vdb/milvus_vector_db_service.py | functions_classes | python |
View Content- 'partition\_name' (str, optional): Required when dropping a specific partition within a collection. Specifies the partition name to be dropped. - 'field\_name' (str, optional): Required when dropping an index within a collection. Specifies the field name for which the index is created. - 'index\_name' (str, optional): Required when dropping an index within a collection. Specifies the name of the index to be dropped. Raises ------ ValueError If mandatory arguments are missing or if the provided 'collection' value is invalid. """ logger.debug("Dropping collection: %s, kwargs=%s", name, kwargs)
| -| None | Document | versioneer.py | functions_classes | python |
View Contentfrom\_vcs\_f = handlers.get("pieces\_from\_vcs") if from\_vcs\_f: try: pieces = from\_vcs\_f(cfg.tag\_prefix, root, verbose) ver = render(pieces, cfg.style) if verbose: print("got version from VCS %s" % ver) return ver except NotThisMethod: pass try: if cfg.parentdir\_prefix: ver = versions\_from\_parentdir(cfg.parentdir\_prefix, root, verbose) if verbose: print("got version from parentdir %s" % ver) return ver except NotThisMethod: pass if verbose: print("unable to compute version")
| -| None | Document | models/training-tuning-scripts/root-cause-models/root_cause_bert.py | simplified_code | python |
View Content# See the License for the specific language governing permissions and # limitations under the License. """ Example Usage: python root\_cause\_bert.py \ --trainingdata ../../datasets/training-data/root-cause-training-data.csv \ --unseenerrors ../../datasets/training-data/root-cause-unseen-errors.csv """
| -| None | Document | morpheus/utils/execution_chain.py | functions_classes | python |
View ContentReturns ------- Any The result of the last function in the chain. Raises ------ Exception If the execution fails at any function in the chain. """ local\_state = {} local\_state.update(kwargs) try: returned\_state = None for function in self.functions: signature = inspect.signature(function) func\_kwargs = {key: value for key, value in local\_state.items() if key in signature.parameters} returned\_state = function(\*\*func\_kwargs) local\_state.update(returned\_state)
| - - - - -[back to top](#checklist-toc) - -### Step 2.2 : *Docker Container Developer Guide QA System* - - -#### Action Log -
Thought: The Docker Container Code QA System was unable to find any specific information about the container's code or its dependencies using a function or component that could raise a `RecursionError`. However, it did return some information about error handling in the container's code. For example, in the `morpheus/service/vdb/milvus_vector_db_service.py` file, it appears that the `drop_collection` function raises a `ValueError` if mandatory arguments are missing or if the provided 'collection' value is invalid. Additionally, in the `versioneer.py` file, it seems that the `from_vcs_f` function catches a `NotThisMethod` exception and passes it. Furthermore, in the `models/training-tuning-scripts/root-cause-models/root_cause_bert.py` file, there is no obvious error handling mechanism. Lastly, in the `morpheus/utils/execution_chain.py` file, the `execute` function raises an `Exception` if the execution fails at any function in the chain.
-
-Action: Docker Container Developer Guide QA System
-Action Input: What is the purpose and functionality of the Docker container's error handling mechanisms?
- 
- - -#### Tool Input -
What is the purpose and functionality of the Docker container's error handling mechanisms?
- 
- - -#### Tool Output -
Unfortunately, I don't know the answer to this question. The provided context does not mention Docker containers or their error handling mechanisms. It appears to be discussing a model and pipeline for root cause analysis, but does not provide information about Docker containers.
- - Source Documents - - | ID | Type | source | Page Content | -| --- | --- | --- | --- | -| None | Document | docs/source/developer_guide/guides/2_real_world_phishing.md |
View Contentnecessary validation checks in the constructor, and raising an exception in the case of mis-configuration. This allows us to fail early rather than after the pipeline has started.
| -| None | Document | models/model-cards/root-cause-analysis-model-card.md |
View Content\* The model is primarily designed for testing purposes and serves as a small pre-trained model specifically used to evaluate and validate the Root Cause Analysis pipeline. This model is an example of customized transformer-based root cause analysis. It can be used for pipeline testing purposes. It needs to be re-trained for specific root cause analysis or predictive maintenance needs with the fine-tuning scripts in the repo. The hyperparameters can be optimised to adjust to get the best results with another dataset. The aim is to get the model to predict some false positives that could be previously unknown error types. Users can use this root cause analysis approach with other log types too. If they have known failures in their logs, they can use them to train along with ordinary logs
| -| None | Document | docs/source/developer_guide/guides/10_modular_pipeline_digital_fingerprinting.md |
View Contentprocessing. A node function is used to handle the processing of control messages, and input and output ports are registered to integrate the module into the data processing pipeline seamlessly.
| -| None | Document | docs/source/developer_guide/guides/10_modular_pipeline_digital_fingerprinting.md |
View ContentThe pipeline comprises a series of interconnected modules designed to create a versatile split processing pipeline. This design enables the reception and processing of control messages to perform tasks such as inference against observed events using either generic or user-specific models. Additionally, the pipeline can train new models based on aggregated or predefined training data, offering a more adaptable and user-friendly experience. ### Overview
| - - - - -[back to top](#checklist-toc) - -### Step 2.3 : *Internet Search* - - -#### Action Log -
Thought: The Docker Container Developer Guide QA System was unable to find any specific information about the Docker container's error handling mechanisms. However, it did return some information about error handling in general. For example, in the `docs/source/developer_guide/guides/2_real_world_phishing.md` file, it appears that the pipeline has necessary validation checks in the constructor and raises an exception in the case of mis-configuration. Additionally, in the `models/model-cards/root-cause-analysis-model-card.md` file, it seems that the model is designed for testing purposes and can be used for pipeline testing purposes. Furthermore, in the `docs/source/developer_guide/guides/10_modular_pipeline_digital_fingerprinting.md` file, the pipeline comprises a series of interconnected modules designed to create a versatile split processing pipeline.
-
-Action: Internet Search
-Action Input: How to handle RecursionError exceptions in Python
- 
- - -#### Tool Input -
How to handle RecursionError exceptions in Python
- 
- - -#### Tool Output -
- The Python RecursionError is an exception that occurs when the maximum recursion depth is exceeded. Learn how to fix it.
-- I have a reference to a function within an error. That is to say, if an error occurs, then the code re-references the function it is entered it runs again.
-- In this article, we will elucidate the Recursionerror In Python through examples, and we will also explore potential approaches to resolve this issue.
-- Make sure you recursive Python function has a terminating condition. If there is one, then consider the size of the data structure input to the ...
-- If an exception occurs during execution of the try clause, the exception may be handled by an except clause. If the exception is not handled by an except clause ...
-- Working on a bot that will write out text from a .CSV file at random, line by line as I run it, until all lines on that csv have been printed out.
-- I have an issue which I believe is caused by hitting the recursion limit inside of a getattr call. However, I have no idea how to debug this.
-- User code can raise built-in exceptions. This can be used to test an exception handler or to report an error condition “just like” the situation in which the ...
-- In this article, we shall look at why this error occurs and how to handle it in Python. To understand this, we need to first look at tail ...
-- The recursion limit is set too high. I think the problem is that fixRecursionLimit assumes that one python stack entry is no larger than 50 empty js function ...
- - - -## Step 3 : Inspect Code for Uncontrolled Recursion - - -> **Input**: *Inspect Code for Uncontrolled Recursion: Although the vulnerable package version is already verified to be installed, inspect the application code to identify any instances of uncontrolled recursion that could be exacerbated by the `email.utils.parseaddr` function's behavior, potentially leading to resource exhaustion or crashes.* - -> **Response**: *The Docker container does not appear to be vulnerable to uncontrolled recursion due to the `email.utils.parseaddr` function, as it is not used in the application code. However, there is a potential source of uncontrolled recursion in the `ProducerConsumerQueue` class due to the use of a recursive lock.* - -### Step 3.1 : *Docker Container Code QA System* - - -#### Action Log -
Thought: To answer this question, I need to inspect the application code within the Docker container to identify any instances of uncontrolled recursion that could be exacerbated by the `email.utils.parseaddr` function's behavior. I should use the Docker Container Code QA System to search for any usage of the `email.utils.parseaddr` function and analyze the surrounding code for potential recursion issues.
-
-Action: Docker Container Code QA System
-Action Input: Does the application code use the `email.utils.parseaddr` function, and if so, are there any instances of uncontrolled recursion in the surrounding code?
- 
- - -#### Tool Input -
Does the application code use the `email.utils.parseaddr` function, and if so, are there any instances of uncontrolled recursion in the surrounding code?
- 
- - -#### Tool Output -
No, the provided code does not use the `email.utils.parseaddr` function. Additionally, there is no indication of uncontrolled recursion in the surrounding code. The code appears to be related to file watching, data processing, and logging, but does not involve email parsing or recursive function calls that could lead to uncontrolled recursion.
- - Source Documents - - | ID | Type | source |... content_type |... language | Page Content | -| --- | --- | --- | --- | --- | --- | -| None | Document | morpheus/service/vdb/milvus_vector_db_service.py | functions_classes | python |
View ContentReturns ------- typing.Any The search result, which can vary depending on the query and options. Raises ------ RuntimeError If an error occurs during the search operation. If query argument is `None` and `data` keyword argument doesn't exist. If `data` keyword arguement is `None`. """ logger.debug("Searching in collection: %s, query=%s, kwargs=%s", self.\_name, query, kwargs) return self.\_client.query(collection\_name=self.\_name, filter=query, \*\*kwargs)
| -| None | Document | morpheus/stages/input/appshield_source_stage.py | functions_classes | python |
View Contentself.\_encoding = encoding self.\_input\_count = None self.\_watcher = DirectoryWatcher(input\_glob=input\_glob, watch\_directory=watch\_directory, max\_files=max\_files, sort\_glob=sort\_glob, recursive=recursive, queue\_max\_size=queue\_max\_size, batch\_timeout=batch\_timeout) @property def name(self) -> str: return "from-appshield" @property def input\_count(self) -> int: """Return None for no max intput count""" return self.\_input\_count def supports\_cpp\_node(self): return False
| -| None | Document | models/training-tuning-scripts/dfp-models/dfp-feature-selection-demo.ipynb | | |
View Content"... ... \n", "3234 External security challenge was not satisfied. \n", "3235 NaN \n", "3236 NaN \n", "3237 NaN \n", "3238 NaN \n", "\n", " properties.clientAppUsed \\\n", "0 Mobile Apps and Desktop clients \n", "1 Browser \n", "2 Mobile Apps and Desktop clients \n", "3 Mobile Apps and Desktop clients \n", "4 Mobile Apps and Desktop clients \n", "... ... \n",
| -| None | Document | morpheus/stages/input/azure_source_stage.py | functions_classes | python |
View ContentIf true the list of files matching `input\_glob` will be processed in sorted order. recursive: bool, default = True If true, events will be emitted for the files in subdirectories that match `input\_glob`. queue\_max\_size: int, default = 128 Maximum queue size to hold the file paths to be processed that match `input\_glob`. batch\_timeout: float, default = 5.0 Timeout to retrieve batch messages from the queue. """
| - - - - -[back to top](#checklist-toc) - -### Step 3.2 : *Docker Container Code QA System* - - -#### Action Log -
Thought: Based on the observation from the Docker Container Code QA System, it appears that the application code does not use the `email.utils.parseaddr` function, and there is no indication of uncontrolled recursion in the surrounding code. This suggests that the vulnerability related to uncontrolled recursion in the `email.utils.parseaddr` function may not be applicable to this Docker container.
-
-However, to further confirm this, I would like to investigate if there are any other functions or components in the application code that could potentially lead to uncontrolled recursion.
-
-Action: Docker Container Code QA System
-Action Input: Are there any other functions or components in the application code that could potentially lead to uncontrolled recursion?
- 
- - -#### Tool Input -
Are there any other functions or components in the application code that could potentially lead to uncontrolled recursion?
- 
- - -#### Tool Output -
Based on the provided code, there is one potential source of uncontrolled recursion: the use of a recursive lock (`threading.RLock`) in the `ProducerConsumerQueue` class. Recursive locks can lead to uncontrolled recursion if not used carefully, as they allow a thread to acquire the lock multiple times without releasing it.
-
-However, in this specific implementation, the recursive lock is used to prevent reentrant deadlocks, which suggests that the developer is aware of the potential risks and has taken steps to mitigate them.
-
-There are no other obvious sources of uncontrolled recursion in the provided code. The `DirectoryWatcher` class is not shown in the code snippet, but it's possible that it could contain recursive functions or methods that could lead to uncontrolled recursion. Without more information, it's impossible to say for sure.
-
-In general, it's always a good idea to carefully review code that uses recursive functions or locks to ensure that they are used safely and correctly.
- - Source Documents - - | ID | Type | source |... content_type |... language | Page Content | -| --- | --- | --- | --- | --- | --- | -| None | Document | morpheus/controllers/monitor_controller.py | functions_classes | python |
View Contentif isinstance(log\_level, LogLevels): # pylint: disable=isinstance-second-argument-not-valid-type log\_level = log\_level.value self.\_log\_level = log\_level self.\_enabled = None # defined on first call to \_is\_enabled @property def delayed\_start(self) -> bool: return self.\_delayed\_start @property def progress(self) -> tqdm: return self.\_progress def is\_enabled(self) -> bool: """ Returns a boolean indicating whether or not the logger is enabled. """ if self.\_enabled is None: self.\_enabled = logger.isEnabledFor(self.\_log\_level) return self.\_enabled
| -| None | Document | morpheus/utils/producer_consumer_queue.py | functions_classes | python |
View Contentclass ProducerConsumerQueue(queue.Queue, typing.Generic[\_T]): """ Custom queue.Queue implementation which supports closing and uses recursive locks Parameters ---------- maxsize : int Maximum size of queue. If maxsize is <= 0, the queue size is infinite. """ def \_\_init\_\_(self, maxsize: int = 0) -> None: super().\_\_init\_\_(maxsize=maxsize) # Use a recursive lock here to prevent reentrant deadlocks self.mutex = threading.RLock() self.not\_empty = threading.Condition(self.mutex) self.not\_full = threading.Condition(self.mutex) self.all\_tasks\_done = threading.Condition(self.mutex) self.\_is\_closed = False
| -| None | Document | morpheus/stages/input/appshield_source_stage.py | functions_classes | python |
View Contentself.\_encoding = encoding self.\_input\_count = None self.\_watcher = DirectoryWatcher(input\_glob=input\_glob, watch\_directory=watch\_directory, max\_files=max\_files, sort\_glob=sort\_glob, recursive=recursive, queue\_max\_size=queue\_max\_size, batch\_timeout=batch\_timeout) @property def name(self) -> str: return "from-appshield" @property def input\_count(self) -> int: """Return None for no max intput count""" return self.\_input\_count def supports\_cpp\_node(self): return False
| -| None | Document | morpheus/utils/directory_watcher.py | functions_classes | python |
View Contentmax\_files: int Max number of files to read. Useful for debugging to limit startup time. Default value of -1 is unlimited. sort\_glob : bool If true the list of files matching `input\_glob` will be processed in sorted order. recursive: bool If true, events will be emitted for the files in subdirectories matching `input\_glob`. queue\_max\_size: int Maximum queue size to hold the file paths to be processed that match `input\_glob`. batch\_timeout: float Timeout to retrieve batch messages from the queue. """
| - - - - -[back to top](#checklist-toc) - ---- -## References -Here are key references for further details on this vulnerability: -- https://www.cve.org/CVERecord?id=CVE-2023-36632 -- https://github.com/python/cpython/issues/103800 -- https://github.com/Daybreak2019/PoC_python3.9_Vul/blob/main/RecursionError-email.utils.parseaddr.py -- https://docs.python.org/3/library/email.html -- https://docs.python.org/3/library/email.utils.html -- https://github.com/advisories/GHSA-gv66-v8c8-v69c -- https://nvd.nist.gov/vuln/detail/CVE-2023-36632 - -[back to top](#cve-intro) diff --git a/README.md b/README.md index a1c839bec..f2ee51941 100644 --- a/README.md +++ b/README.md @@ -1,1486 +1,641 @@ - - -

NVIDIA AI Blueprint: Vulnerability Analysis for Container Security

- -## Table of Contents -- [Overview](#overview) -- [Software components](#software-components) -- [Target audience](#target-audience) -- [Prerequisites](#prerequisites) -- [Hardware requirements](#hardware-requirements) -- [Operating system requirements](#operating-system-requirements) -- [API definition](#api-definition) -- [Use case description](#use-case-description) - - [How it works](#how-it-works) - - [Key components](#key-components) - - [NIM microservices](#nim-microservices) -- [Getting started](#getting-started) - - [Install system requirements](#install-system-requirements) - - [Obtain API keys](#obtain-api-keys) - - [Set up the workflow repository](#set-up-the-workflow-repository) - - [Set up the environment file](#set-up-the-environment-file) - - [Authenticate Docker with NGC](#authenticate-docker-with-ngc) - - [Build the Docker containers](#build-the-docker-containers) - - [Start the Docker containers](#start-the-docker-containers) -- [Running the Workflow](#running-the-workflow) - - [From the quick start user guide notebook](#from-the-quick-start-user-guide-notebook) - - [From the command line](#from-the-command-line) - - [Command line interface (CLI) reference](#command-line-interface-cli-reference) - - [Configuration file reference](#configuration-file-reference) - - [NGINX caching server](#nginx-caching-server) -- [Customizing the Workflow](#customizing-the-workflow) - - [Customizing the SBOM input](#customizing-the-sbom-input) - - [Customizing the LLM models](#customizing-the-llm-models) - - [Customizing the embedding model](#customizing-the-embedding-model) - - [Customizing the Output](#customizing-the-output) - - [Adding Test Time Compute (TTC)](#adding-test-time-compute-ttc) -- [Evaluating the Workflow](#evaluating-the-workflow) - - [Running Evaluation (Overview)](#running-evaluation-overview) - - [Inputting Evaluation Datasets](#inputting-evaluation-datasets) - - [Running Multiple Evaluation Repetitions](#running-multiple-evaluation-repetitions) - - [Accuracy and Consistency Evaluators](#accuracy-and-consistency-evaluators) - - [Writing Custom Evaluators](#writing-custom-evaluators) -- [Troubleshooting](#troubleshooting) - - [Git LFS issues](#git-lfs-issues) - - [Container build issues](#container-build-issues) - - [NGINX caching server](#nginx-caching-server-1) - - [Service errors](#service-errors) - - [macOS Workarounds](#macos-workarounds) -- [Testing and validation](#testing-and-validation) -- [Roadmap](#roadmap) -- [Cite](#cite) -- [License](#license) -- [Terms of Use](#terms-of-use) - - -## Overview -This repository is what powers the [build experience](https://build.nvidia.com/nvidia/vulnerability-analysis-for-container-security), showcasing vulnerability analysis for container security using NVIDIA NIM microservices and [NVIDIA NeMo Agent Toolkit](https://github.com/NVIDIA/NeMo-Agent-Toolkit). - -The NVIDIA AI Blueprint demonstrates accelerated analysis on common vulnerabilities and exposures (CVE) at an enterprise scale, reducing mitigation from days and hours to just seconds. While traditional methods require substantial manual effort to pinpoint solutions for vulnerabilities, these technologies enable quick, automatic, and actionable CVE risk analysis using large language models (LLMs) and retrieval-augmented generation (RAG). With this blueprint, security analysts can expedite the process of determining whether a software package includes exploitable and vulnerable components using LLMs and event-driven RAG triggered by the creation of a new software package or the detection of a CVE. - -## Software components -The following are used by this blueprint: -- [NIM of meta/llama-3.1-70b-instruct](https://build.nvidia.com/meta/llama-3_1-70b-instruct) -- [NIM of nvidia/nv-embedqa-e5-v5](https://build.nvidia.com/nvidia/nv-embedqa-e5-v5) -- [NVIDIA NeMo Agent Toolkit](https://docs.nvidia.com/nemo/agent-toolkit/latest/) - -## Target audience -This blueprint is for: -- **Security analysts and IT engineers**: People analyzing vulnerabilities and ensuring the security of containerized environments. -- **AI practitioners in cybersecurity**: People applying AI to enhance cybersecurity, particularly those interested in using the NeMo Agent Toolkit and NIMs for faster vulnerability detection and analysis. +# Container Vulnerability Analysis Pipeline -## Prerequisites -- NVAIE developer licence -- API keys for vulnerability databases, search engines, and LLM model service(s). - - Details can be found in this later section: [Obtain API keys](#obtain-api-keys) - -## Hardware requirements -Below are the hardware requirements for each component of the vulnerability analysis workflow. +A multi-agent LLM system that performs end-to-end vulnerability analysis for container images. Given a CVE or GHSA identifier and an extracted container filesystem, the pipeline researches the vulnerability, analyzes the container's security posture, determines real-world exploitability, and produces a formal [VEX](https://www.cisa.gov/sites/default/files/2023-04/minimum-requirements-for-vex.pdf) (Vulnerability Exploitability eXchange) classification. -The overall hardware requirements depend on selected workflow configuration. At a minimum, the hardware requirements for workflow operation must be met. The LLM NIM and Embedding NIM hardware requirements only need to be met if self-hosting these components. See [Using self-hosted NIMs](#using-self-hosted-nims), [Customizing the LLM models](#customizing-the-llm-models) and [Customizing the embedding model](#customizing-the-embedding-model) sections for more information. +The workflow is implemented with **[NeMo Agent Toolkit](https://docs.nvidia.com/nemo/agent-toolkit/latest/)** (NAT): a single **`cve_pipeline`** graph is registered from this package and run with **`nat run`**, **`nat serve`**, and **`nat eval`**. -- **(Optional) LLM NIM**: [Meta Llama 3.1 70B Instruct Support Matrix](https://docs.nvidia.com/nim/large-language-models/latest/support-matrix.html#llama-3-1-70b-instruct) - - This workflow makes heavy use of parallel LLM calls to accelerate processing. For improved parallel performance (for example, in production workloads), we recommend 8x or more H100s for LLM inference. -- **(Optional) Embedding NIM**: [NV-EmbedQA-E5-v5 Support Matrix](https://docs.nvidia.com/nim/nemo-retriever/text-embedding/latest/support-matrix.html#nv-embedqa-e5-v5) +## Table of Contents +- [Container Vulnerability Analysis Pipeline](#container-vulnerability-analysis-pipeline) + - [Table of Contents](#table-of-contents) + - [Architecture Overview](#architecture-overview) + - [How It Works](#how-it-works) + - [Agents](#agents) + - [1. CVE Researcher](#1-cve-researcher) + - [2. Container Analyzer](#2-container-analyzer) + - [3. Exploitability Analyzer](#3-exploitability-analyzer) + - [4. VEX Categorizer](#4-vex-categorizer) + - [Shared package layout (`src/vuln_analysis/`)](#shared-package-layout-srcvuln_analysis) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Preparing Container Inputs](#preparing-container-inputs) + - [Option A — Automatic (recommended)](#option-a--automatic-recommended) + - [Option B — Pre-extract with the standalone helper](#option-b--pre-extract-with-the-standalone-helper) + - [Usage](#usage) + - [Full Pipeline](#full-pipeline) + - [Individual Agents](#individual-agents) + - [HTTP server (`nat serve`)](#http-server-nat-serve) + - [Multi-CVE Requests](#multi-cve-requests) + - [Programmatic API](#programmatic-api) + - [Output Structure](#output-structure) + - [Benchmarking](#benchmarking) + - [Included Dataset: `morpheus_benchmark_2506.csv`](#included-dataset-morpheus_benchmark_2506csv) + - [Running Benchmarks](#running-benchmarks) + - [Configuration](#configuration) + - [Environment Variables](#environment-variables) + - [Concurrency \& Rate Limiting](#concurrency--rate-limiting) + - [Key Defaults](#key-defaults) + - [Caching](#caching) + - [Troubleshooting](#troubleshooting) + - [Design Principles](#design-principles) + - [Project Structure](#project-structure) -## Operating system requirements +## Architecture Overview -**Officially Supported:** Ubuntu and other Linux distributions +The system is composed of **four specialized agents** coordinated by a **pipeline orchestrator**: -**Limited Support for macOS:** Testing and development of the workflow may be possible on macOS, however macOS is not officially supported or tested for this blueprint. Platform differences may require extra troubleshooting or impact performance. See [macOS Workarounds](#macos-workarounds) for known issues and workarounds. In addition, self-hosting NIMs is not supported on macOS (requires NVIDIA GPUs not available on Mac hardware). For production deployments, use Linux-based systems. +![Pipeline orchestrator architecture diagram](images/pipeline_orchestrator.png) +**Stage 1** runs CVE Researcher and Container Analyzer concurrently via `asyncio.gather`. **Stage 2** feeds both reports into the Exploitability Analyzer. **Stage 3** produces the final VEX classification. The orchestrator short-circuits on failure at any stage, defaulting to `unknown` status and `uncertain` justification as a fail-safe. -## API definition +In this repository, that orchestrator is the NAT **`cve_pipeline`** workflow (LangGraph); stage timeouts and retries are configured under **`workflow`** in [`src/vuln_analysis/configs/config.yml`](./src/vuln_analysis/configs/config.yml) (default timeout: **600** seconds). -[OpenAPI Specification](./src/vuln_analysis/configs/openapi/openapi.json) +## How It Works -## Use case description -Determining the impact of a documented CVE on a specific project or container is a labor-intensive and manual task, especially as the rate of new reports into the [CVE database](https://www.cve.org/) accelerates. This process involves the collection, comprehension, and synthesis of various pieces of information to ascertain whether immediate remediation is necessary upon the identification of a new CVE. +1. **Input**: Provide one or more CVE or GHSA identifiers, plus `image_name` and `image_tag`. Pass them as a [`PipelineInput`](./src/vuln_analysis/data_models/input.py) JSON document to `nat run` / `nat serve`. If `filesystem_path` is omitted, the pipeline combines `image_name:image_tag`, then pulls and extracts the rootfs via `crane`; if `filesystem_path` is present, the pre-extracted rootfs is used. When multiple CVEs are provided, CVE-specific stages fan out in parallel while the Container Analyzer runs once. +2. **Stage 1** runs two agents in parallel: + - The **CVE Researcher** queries public vulnerability databases (NVD, GitHub Advisories, OSV.dev) and vendor pages to build a structured vulnerability report — identifying affected packages, version ranges, vulnerable functions/APIs, and attack vectors. + - The **Container Analyzer** optionally researches the image online (Phase 1), then inspects the extracted filesystem (Phase 2) to produce a container report covering installed packages, entrypoints, security controls, and container classification. +3. **Stage 2**: The **Exploitability Analyzer** receives both reports. Its deterministic pre-triage phase programmatically verifies package presence on the filesystem and compares versions — exiting early with zero LLM calls when the vulnerability clearly does not apply. For ambiguous cases, a deep LLM analysis traces code reachability and data flows within the container. +4. **Stage 3**: The **VEX Categorizer** reads the exploitability report and container report, walks a precedence-ordered decision flowchart using a structured step-by-step decision trace, and produces a formal VEX status with justification. +5. **Output**: Per-agent Markdown reports and a structured VEX JSON classification. -**Current challenges in CVE analysis:** -- **Information collection**: The process involves significant manual labor to collect and synthesize relevant information. -- **Decision complexity**: Decisions on whether to update a library impacted by a CVE often hinge on various considerations, including: - - **Scan false positives**: Occasionally, vulnerability scans may incorrectly flag a library as vulnerable, leading to a false alarm. - - **Mitigating factors**: In some cases, existing safeguards within the environment may reduce or negate the risk posed by a CVE. - - **Lack of required environments or dependencies**: For an exploit to succeed, specific conditions must be met. The absence of these necessary elements can render a vulnerability irrelevant. -- **Manual documentation**: Once an analyst has determined the library is not affected, a Vulnerability Exploitability eXchange (VEX) document must be created to standardize and distribute the results. +Each stage has a configurable timeout (default: 600 seconds). If any stage fails, the pipeline short-circuits and defaults to `unknown` status and `uncertain` justification to reflect that the analysis could not be completed. -The efficiency of this process can be significantly enhanced through the deployment of an automated LLM agent workflow, leveraging generative AI to improve vulnerability defense while decreasing the load on security teams. +## Agents -### How it works -The workflow operates using a Plan-and-Execute-style LLM pipeline for CVE impact analysis. The process begins with an LLM planner that generates a context-sensitive task checklist. This checklist is then executed by an LLM agent equipped with Retrieval-Augmented Generation (RAG) capabilities. The gathered information and the agent's findings are subsequently summarized and categorized by additional LLM nodes to provide a final verdict. +### 1. CVE Researcher -> [!TIP] -> The workflow is adaptable, with support for NIM and OpenAI LLM APIs. NIM models can be hosted on [build.nvidia.com](https://build.nvidia.com/) or self-hosted. +Researches a CVE or GHSA from public sources (NVD, GitHub Advisories, vendor pages) and produces a structured vulnerability report identifying affected packages, versions, vulnerable functions/APIs, and attack vectors. -### Key components -The detailed architecture consists of the following components: +**Tools:** `web_search` (Tavily), `web_fetch`, `osv_lookup` -

- -

+### 2. Container Analyzer -- **Security scan result**: The workflow begins by inputting the identified CVEs from a container security scan as input. This can be generated from a container image scanner of your choosing such as [Anchore](https://anchore.com/container-vulnerability-scanning/). +Analyzes an extracted container filesystem to produce a security-focused container report. Classifies the container type (SDK/Development, Runtime/Microservice, Infrastructure/Platform), maps dependencies, inventories entrypoints, and evaluates security controls. -- **PreProcessing**: All the below actions are encapsulated by multiple NeMo Agent toolkit functions to prepare the data for use with the LLM engine. - - **Code repository and documentation**: The blueprint pulls code repositories and documentation provided by the user. These repositories are processed through an embedding model, and the resulting embeddings are stored in vector databases (VDBs) for the agent's reference. - - **Vector database**: Various vector databases can be used for the embedding. We currently utilize FAISS for the VDB because it does not require an external service and is simple to use. Any vector store can be used, such as NVIDIA cuVS, which would provide accelerated indexing and search. - - **Lexical search**: As an alternative, a lexical search is available for use cases where creating an embedding is impractical due to a large number of source files in the target container. - - **Software Bill of Materials (SBOM)**: A Software Bill of Materials (SBOM) is a machine-readable manifest of all the dependencies of a software package or container. The blueprint cross-references every entry in the SBOM for known vulnerabilities and looks at the code implementation to see whether the implementation puts users at risk—just as a security analyst would do. For this reason, starting with an accurate SBOM is an important first step. SBOMs can be generated for any container using the open-source tool [Syft](https://github.com/anchore/syft). For more information on generating SBOMs for your containers, see the [SBOM documentation](./src/vuln_analysis/data/sboms/README.md). - - **Web vulnerability intel**: The system collects detailed information about each CVE through web scraping and data retrieval from various public security databases, including GHSA, Redhat, Ubuntu, and NIST CVE records, as well as tailored threat intelligence feeds. +**Architecture:** Two-phase with security isolation: -- **Core LLM engine**: The below actions comprise the core LLM engine and are each implemented as NeMo Agent toolkit functions within the workflow. - - **Checklist generation**: Leveraging the gathered information about each vulnerability, the checklist generation node creates a tailored, context-sensitive task checklist designed to guide the impact analysis. (See [`src/vuln_analysis/functions/cve_checklist.py`](./src/vuln_analysis/functions/cve_checklist.py).) +- **Phase 1 (Web Research)** — Searches public registries and vendor docs. Network access only, no filesystem access. Conditionally skipped for private images by setting `skip_web_research` to `true` in `PipelineInput`. +- **Phase 2 (Filesystem Analysis)** — Inspects the extracted rootfs. Filesystem access only, no network access. - - **Task agent**: At the core of the process is an LLM agent iterating through each item in the checklist. For each item, the agent answers the question using a set of tools which provide information about the target container. The tools tap into various data sources (web intel, vector DB, search etc.), retrieving relevant information to address each checklist item. The loop continues until the agent resolves each checklist item satisfactorily. (See [`src/vuln_analysis/functions/cve_agent.py`](./src/vuln_analysis/functions/cve_agent.py).) +**Tools:** Phase 1: `web_search`, `web_fetch` | Phase 2: `read_file`, `list_directory`, `grep_search`, `glob_find` - - **Summarization**: Once the agent has compiled findings for each checklist item, these results are condensed by the summarization node into a concise, human-readable paragraph. (See [`src/vuln_analysis/functions/cve_summarize.py`](./src/vuln_analysis/functions/cve_summarize.py).) +### 3. Exploitability Analyzer - - **Justification Assignment**: Given the summary, the justification status categorization node then assigns a resulting VEX (Vulnerability Exploitability eXchange) status to the CVE. We provided a set of predefined categories for the model to choose from. (See [`src/vuln_analysis/functions/cve_justify.py`](./src/vuln_analysis/functions/cve_justify.py).) If the CVE is deemed exploitable, the reasoning category is "vulnerable." If there is no vulnerable packages detected from the SBOM or insufficient intel gathered the agent is bypassed and an appropriate label is provided. If it is not exploitable, there are 10 different reasoning categories to explain why the vulnerability is not exploitable in the given environment: - - `false_positive` - - `code_not_present` - - `code_not_reachable` - - `requires_configuration` - - `requires_dependency` - - `requires_environment` - - `protected_by_compiler` - - `protected_at_runtime` - - `protected_by_perimeter` - - `protected_by_mitigating_control` +Determines whether a CVE is actually exploitable within the specific container context. Produces a verdict with a confidence level. -- **Output**: At the end of the workflow run, an output file including all the gathered and generated information is prepared for security analysts for a final review. (See [`src/vuln_analysis/functions/cve_file_output.py`](./src/vuln_analysis/functions/cve_file_output.py).) +**Architecture:** Deterministic pre-triage followed by LLM deep analysis: -> [!WARNING] -> All output should be vetted by a security analyst before being used in a cybersecurity application. +- **Pre-Triage (deterministic, zero LLM calls)** — Parses reports, verifies package presence on the filesystem, checks version ranges, inspects distro backports. Can exit early with `NOT APPLICABLE` or `ENVIRONMENT ISOLATED` and generate a full report from templates. +- **Deep Analysis (LLM)** — Code reachability analysis, data flow tracing, security control evaluation. Only invoked when pre-triage cannot reach a definitive verdict. -### NIM microservices -The NeMo Agent toolkit can utilize various embedding model and LLM endpoints, and is optimized to use [NVIDIA NIM microservices](https://developer.nvidia.com/nim) (NIMs). NIMs are pre-built containers for the latest AI models that provide industry-standard APIs and optimized inference for the given model and hardware. Using NIMs enables easy deployment and scaling for self-hosted model inference. +The pre-triage uses the container filesystem as the authoritative source for package verification — all analysis is filesystem-based, including direct Go binary scanning for Go modules. -The current default embedding NIM model is `nv-embedqa-e5-v5`, which was selected to balance speed and overall workflow accuracy. The current default LLM model is the `llama-3.1-70b-instruct` NIM, with specifically tailored prompt engineering and edge case handling. Other models are able to be substituted for either the embedding or LLM model, such as smaller, fine-tuned NIM LLM models or other external LLM inference services. Subsequent updates will provide more details about fine-tuning and data flywheel techniques. +**Verdicts:** `NOT APPLICABLE` · `ENVIRONMENT ISOLATED` · `NOT REACHABLE` · `MITIGATED` · `CONDITIONALLY EXPLOITABLE` · `EXPLOITABLE` -> [!NOTE] -> Within the NeMo Agent toolkit workflow, the LangChain framework is employed to deploy all LLMs and agents, and the LangGraph framework is used for orchestration, streamlining efficiency and reducing the need for duplicative efforts. +**Tools:** `read_file`, `list_directory`, `grep_search`, `glob_find`, `check_binary_dependencies`, `search_binary_content`, `check_distro_patches` -> [!TIP] -> Routinely checked validation datasets are critical to ensuring proper and consistent outputs. Learn more about our test-driven development approach in the section on [testing and validation](#testing-and-validation). +### 4. VEX Categorizer -## Getting started +Assigns a formal VEX status and justification by following a precedence-ordered decision flowchart. Reads the exploitability and container reports, records each step with a structured decision-trace tool, and produces both a Markdown summary and structured JSON output. Use [`configs/config-relaxed-vex.yml`](./src/vuln_analysis/configs/config-relaxed-vex.yml) to select the relaxed VEX categorization mode, which leans more toward `code_not_reachable` or `requires_configuration` versus `vulnerable`. -> [!TIP] -> Please refer to the [Troubleshooting](#troubleshooting) section for common errors encountered during setup. +**VEX Statuses:** -### Install system requirements +| Status | Description | +|--------|-------------| +| `false_positive` | The vulnerability does not exist in the reported component | +| `code_not_present` | The vulnerable code is not included in the container | +| `code_not_reachable` | The vulnerable code exists but cannot be reached at runtime | +| `requires_configuration` | Exploitation requires a non-default configuration | +| `requires_dependency` | Exploitation requires a dependency not present in the container | +| `requires_environment` | Exploitation requires specific environmental conditions | +| `compiler_protected` | Compiler-level protections prevent exploitation | +| `runtime_protected` | Runtime protections prevent exploitation | +| `perimeter_protected` | Network perimeter controls prevent exploitation | +| `mitigating_control_protected` | Other mitigating controls prevent exploitation | +| `vulnerable` | The vulnerability is exploitable in this container context | -* [git](https://git-scm.com/) -* [git-lfs](https://git-lfs.com/) -* Since the workflow uses [NVIDIA NeMo Agent Toolkit](https://docs.nvidia.com/nemo/agent-toolkit/latest/), the [NeMo Agent toolkit requirements](https://docs.nvidia.com/nemo/agent-toolkit/latest/quick-start/installing.html#prerequisites) also need to be installed. +**Tools:** `reason` (structured decision trace) -### Obtain API keys -To run the workflow you need to obtain API keys for the following APIs. These will be needed in a later step to [Set up the environment file](#set-up-the-environment-file). +### Shared package layout (`src/vuln_analysis/`) -- **Required API Keys**: These APIs are required by the workflow to retrieve vulnerability information from databases, perform online searches, and execute LLM queries. +Common building blocks for the NAT-registered workflow: - - GitHub Security Advisory (GHSA) Database - - Follow [these](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) instructions to create a personal access token. No repository access or permissions are required for this API. - - This will be used in the `GHSA_API_KEY` environment variable. - - National Vulnerability Database (NVD) - - Follow [these](https://nvd.nist.gov/developers/request-an-api-key) instructions to create an API key. - - This will be used in the `NVD_API_KEY` environment variable. - - SerpApi - - Go to https://serpapi.com/ and create a SerpApi account. Once signed in, navigate to Your Account > Api Key. - - This will be used in the `SERPAPI_API_KEY` environment variable. - - NVIDIA Inference Microservices (NIM) - - There are two possible methods to generate an API key for NIM: - - Sign in to the [NVIDIA Build](https://build.nvidia.com/explore/discover?signin=true) portal with your email. - - Click on any model that displays the "Run Anywhere" tag, such as [this one](https://build.nvidia.com/meta/llama-3_1-70b-instruct). - - Click "Deploy" in the top menu, then click "Get API Key", and finally, click "Generate Key". - - Sign in to the [NVIDIA NGC](https://ngc.nvidia.com/) portal with your email. - - After logging in, click on your user at the top right to open a dropdown menu. Click on the first menu item to select your organization. You must select an organization which has NVIDIA AI Enterprise (NVAIE) enabled. - - In the same dropdown menu as the previous step, select "Setup." - - Click the "Generate API Key" option and then the "+ Generate Personal Key" button to create your API key. - - This will be used in the `NVIDIA_API_KEY` environment variable. +| Path | Purpose | +|------|---------| +| `register.py` | Registers `cve_pipeline` and agent functions with NAT | +| `configs/*.yml` | LLM definitions, tools, timeouts, and workflow wiring | +| `functions/` | CVE researcher, container analyzer, exploitability, VEX implementations | +| `tools/` | Tavily/OSV/web fetch, filesystem toolkit, binary and package helpers | +| `utils/` | Pre-triage, report generation, Markdown parsing, HTTP helpers | +| `data_models/` | `PipelineInput`, `PipelineResult`, stage payloads | -The workflow can be configured to use other LLM services as well, see the [Customizing the LLM models](#customizing-the-llm-models) section for more info. +## Prerequisites -### Set up the workflow repository +- **Python 3.13** (see [`pyproject.toml`](./pyproject.toml), which requires `>=3.13,<3.14`) +- **[crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane)** — used by the pipeline to pull and extract container images. Already baked into the project Dockerfile; local automatic extraction requires `crane` on `PATH`. +- **NVIDIA API key** — NAT `nim` LLM configs use NVIDIA's OpenAI-compatible inference API (`NVIDIA_API_KEY`) +- **Tavily API key** — for web search capabilities -Clone the repository and set an environment variable for the path to the repository root. +Install `crane` locally before using automatic image extraction: ```bash -export REPO_ROOT=$(git rev-parse --show-toplevel) -``` - -All commands are run from the repository root unless otherwise specified. +# macOS +brew install crane -### Set up the environment file - -First we need to create an `.env` file in the `REPO_ROOT`, and add the API keys you created in the earlier [Obtain API keys](#obtain-api-keys) step. - -```bash -cd $REPO_ROOT -cat < .env -GHSA_API_KEY="your GitHub personal access token" -NVD_API_KEY="your National Vulnerability Database API key" -NVIDIA_API_KEY="your NVIDIA Inference Microservices API key" -SERPAPI_API_KEY="your SerpApi API key" -EOF +# Linux or any platform with Go installed +go install github.com/google/go-containerregistry/cmd/crane@latest +export PATH="$HOME/go/bin:$PATH" ``` -These variables need to be exported to the environment: -```bash -export $(cat .env | xargs) -``` +For other platforms, see the upstream [crane installation instructions](https://github.com/google/go-containerregistry/tree/main/cmd/crane#installation). -### Authenticate Docker with NGC +## Installation -In order to pull images required by the workflow from NGC, you must first authenticate Docker with NGC. You can use same the NVIDIA API Key obtained in the [Obtain API keys](#obtain-api-keys) section (saved as `NVIDIA_API_KEY` in the `.env` file). +This project uses **[uv](https://docs.astral.sh/uv/)** as its package manager. ```bash -echo "${NVIDIA_API_KEY}" | docker login nvcr.io -u '$oauthtoken' --password-stdin -``` +# Install uv (if not already installed) +curl -LsSf https://astral.sh/uv/install.sh | sh -### Build the Docker containers -Next, build the `vuln-analysis` container from source using the following command. This ensures that the container includes all the latest changes from the repository. -```bash -cd $REPO_ROOT +# Initialize repository submodules +git submodule update --init --recursive -# Build the vuln-analysis container -docker compose build vuln-analysis +# Create a virtual environment and install the project +uv venv --python 3.13 .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +uv sync ``` -### Start the Docker containers - -There are two supported configurations for starting the Docker containers. Both configurations utilize `docker compose` to start the service: -1. [**NVIDIA-hosted NIMs**](#using-nvidia-hosted-nims): The workflow is run with all computation being performed by NIMs hosted in NVIDIA GPU Cloud. This is the default configuration and is **recommended** for most users getting started with the workflow. - 1. When using NVIDIA-hosted NIMs, only the `docker-compose.yml` configuration file is required. -2. [**Self-hosted NIMs**](#using-self-hosted-nims): The workflow is run using self-hosted LLM NIM services. This configuration is more advanced and requires additional setup to run the NIM services locally. - 1. When using self-hosted NIMs, both the `docker-compose.yml` and `docker-compose.nim.yml` configuration files are required. - -These two configurations are illustrated by the following diagram: +The explicit `--python 3.13` keeps setup predictable on machines where system `python3` is older. If Python 3.13 is not already installed, `uv` can auto-discover or download a compatible interpreter. -

-Workflow configurations -

- -Before beginning, ensure that the environment variables are set correctly. Both configurations require the same environment variables to be set. More information on setting these variables can be found in the [Obtain API keys](#obtain-api-keys) section. - ->[!TIP] -The container binds to port 8080 by default. If you encounter a port collision error (for example, `Bind for 0.0.0.0:8080 failed: port is already allocated`), you can set the environment variable `NGINX_HOST_HTTP_PORT` to specify a custom port before launching `docker compose`. For example: -> -> ```bash -> export NGINX_HOST_HTTP_PORT=8081 -> -> #... docker compose commands... -> ``` - -#### Using NVIDIA-hosted NIMs -When running the workflow in this configuration, only the `vuln-analysis` service needs to be started since we will utilize NIMs hosted by NVIDIA. The `vuln-analysis` container can be started using the following command: +For development (linting, testing, formatting tools): ```bash -cd ${REPO_ROOT} -docker compose up -d +uv sync --group dev ``` -The command above starts the container in the background using the detached mode, `-d`. We can confirm the container is running via the following command: -```bash -docker compose ps -``` - -Next, we need to attach to the `vuln-analysis` container to access the environment where the workflow [command line tool](#from-the-command-line) and dependencies are installed. -```bash -docker compose exec -it vuln-analysis bash -``` - -Continue to the [Running the workflow](#running-the-workflow) section to run the workflow. +The repo root includes symlinks **`configs`** → `src/vuln_analysis/configs` and **`data`** → `src/vuln_analysis/data` so paths in the examples below resolve. -#### Using self-hosted NIMs -To run the workflow using self-hosted NIMs, we use a second `docker compose` configuration file, `docker-compose.nim.yml`, which adds the self-hosted NIM services to the workflow. Utilizing a second configuration file allows for easy switching between the two configurations while keeping the base configuration file the same. - ->[!NOTE] -> The self-hosted NIM services require additional GPU resources to run. With this configuration, the LLM NIM, embedding model NIM, and the `vuln-analysis` service will all be launched on the same machine. Ensure that you have the necessary hardware requirements for all three services before proceeding (multiple services can share the same GPU). - -To use multiple configuration files, we need to specify both configuration files when running the `docker compose` command. You will need to specify both configuration files for every `docker compose` command. For example: +Create a `.env` file in the project root from the template, then fill in your keys: ```bash -docker compose -f docker-compose.yml -f docker-compose.nim.yml [NORMAL DOCKER COMPOSE COMMAND] +cp .env.example .env ``` -For example, to start the `vuln-analysis` service with the self-hosted NIMs, you would run: -```bash -cd ${REPO_ROOT} -docker compose -f docker-compose.yml -f docker-compose.nim.yml up -d +```env +NVIDIA_API_KEY=your-nvidia-api-key +NV_BASE_URL=https://inference-api.nvidia.com/v1 +TAVILY_API_KEY=your-tavily-api-key ``` -Next, we need to attach to the `vuln-analysis` container to access the environment where the workflow [command line tool](#from-the-command-line) and dependencies are installed. -```bash -docker compose -f docker-compose.yml -f docker-compose.nim.yml exec -it vuln-analysis bash -``` - -Continue to the [Running the workflow](#running-the-workflow) section to run the workflow. - -## Running the Workflow - -> [!TIP] -> Please refer to the [Troubleshooting](#troubleshooting) section for common errors encountered while running the workflow. - -Once the services have been started, the workflow can be run using either the [Quick start user guide notebook](#from-the-quick-start-user-guide-notebook) for an interactive step-by-step process, or directly from the [command line](#from-the-command-line). - -### From the quick start user guide notebook -To run the workflow in an interactive notebook, connect to the Jupyter notebook at http://localhost:8000/lab. Once connected, navigate to the notebook located at [`quick_start/quick_start_guide.ipynb`](./quick_start/quick_start_guide.ipynb) and follow the instructions. - -> [!TIP] -> If you are running the workflow on a remote machine, you can forward the port to your local machine using SSH. For example, to forward port 8000 from the remote machine to your local machine, you can run the following command from your local machine: -> -> ```bash -> ssh -L 8000:127.0.0.1:8000 -> ``` - -### From the command line +For local `nat` commands, make sure the variables are available in your shell. Missing API keys may surface as repeated provider retries or timeouts instead of an immediate failure. -The vulnerability analysis workflow is designed to be run using the `nat` command line tool installed within the `vuln-analysis` container. This section describes how get started using the command line tool. For more detailed information about the command line interface, see the [NeMo Agent toolkit Command Line Interface (CLI)](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/cli.html) documentation. - -#### Workflow configuration file - -The workflow settings are controlled using configuration files. These are YAML files that define the functions, tools, and models to use in the workflow. Example configuration files are located in the [`configs/`](./src/vuln_analysis/configs/) folder. - -> [!NOTE] -> The `configs/` and `data/` directories are symlinks pointing to the actual file locations in the `src/vuln_analysis/configs/` and `src/vuln_analysis/data/` directories respectively. The symlinks are available for convenience. - -A brief description of each configuration file is as follows: -- `config.yml`: This configuration file defines the functions, tools, and models used by the vulnerability analysis workflow, as described above in [Key Components](#key-components) section. -- `config-tracing.yml`: This configuration file is identical to `config.yml` but adds configuration for observing traces of this workflow in Phoenix. - -There are three main modalities that the workflow can be run in using the following commands: - - `nat run`: The workflow will process the input data, then it will shut down after it is completed. This modality is suitable for rapid iteration during testing and development. -- `nat serve`: The workflow is turned into a microservice which will run indefinitely, which is suitable for using in production. -- `nat eval`: Similar to the `nat run` command. However, in addition to running the workflow, it is also used for profiling and evaluating accuracy of the workflow. - -For a breakdown of the configuration file and available options, see the [Configuration file reference](#configuration-file-reference) section. To customize the configuration files for your use case, see [Customizing the workflow](#customizing-the-workflow). - -#### Example command: `nat run` - -The workflow can be started using the following command: ```bash -nat run --config_file=${CONFIG_FILE} --input_file=data/input_messages/morpheus:23.11-runtime.json -``` - -In the command, `${CONFIG_FILE}` is the path to the configuration file you want to use. For example, to run the workflow with the `config.yml` configuration file, you would run: -```bash -nat run --config_file=configs/config.yml --input_file=data/input_messages/morpheus:23.11-runtime.json -``` - -When the workflow runs to completion, you should see logs similar to the following: -``` -Vulnerability 'GHSA-3f63-hfp8-52jq' affected status: FALSE. Label: code_not_reachable -Vulnerability 'CVE-2023-50782' affected status: FALSE. Label: requires_configuration -Vulnerability 'CVE-2023-36632' affected status: FALSE. Label: code_not_present -Vulnerability 'CVE-2023-43804' affected status: TRUE. Label: vulnerable -Vulnerability 'GHSA-cxfr-5q3r-2rc2' affected status: TRUE. Label: vulnerable -Vulnerability 'GHSA-554w-xh4j-8w64' affected status: TRUE. Label: vulnerable -Vulnerability 'GHSA-3ww4-gg4f-jr7f' affected status: FALSE. Label: requires_configuration -Vulnerability 'CVE-2023-31147' affected status: FALSE. Label: code_not_present --------------------------------------------------- -Workflow Result: -{"input":{"scan":{"id":"8351fd75-4798-42c9-81d8-a43d7df838fd","type":null,"started_at":"2025-06-25T20:21:19.698253","completed_at":"2025-06-25T20:30:09.667598","vulns":[{"vuln_id":"GHSA-3f63-hfp8-52jq","description":null,"score":null,"severity":null,"published_date":null,"last_modified_date":null,"url":null,"feed_group":null,"package":null,"package_version":null,"package_name":null,"package_type":null},{"vuln_id":"CVE-2023-50782","description":null,"score":null,"severity":null,"published_date":null,"last_modified_date":null,"url":null,"feed_group":null,"package":null,"package_version":null,"package_name":null,"package_type":null},{"vuln_id":"CVE-2023-36632","description":null,"score":null,"severity":null,"published_date":null,"last_modified_date":null,"url":null,"feed_group":null,"package":null,"package_version":null,"package_name":null, -... -``` - -> [!WARNING] -> The output you receive from the workflow may not be identical as the output in the example above. The output may vary due to the non-deterministic nature of the LLM models. - -##### Reviewing the workflow output +set -a +source .env +set +a -The workflow generates two types of output: - -**1. JSON output** (`.tmp/output.json`): A single machine-readable JSON file containing comprehensive workflow execution details for all analyzed CVEs. This file is designed to capture all workflow input, intermediate information, and output for auditing and downstream processing in production systems. The output JSON includes the following top level fields: - -* `input`: contains the inputs that were provided to the workflow, such as the container and repo source information, the list of vulnerabilities to scan, etc. -* `info`: contains additional information collected by the workflow for decision making. This includes paths to the generated VDB files, intelligence from various vulnerability databases, the list of SBOM packages, and any vulnerable dependencies that were identified. -* `output`: contains the output from the core LLM Engine, including the generated checklist, analysis summary, and justification assignment. - -**2. Markdown reports** (`.tmp/vulnerability_markdown_reports/`): Individual human-readable reports formatted for easy review by security analysts. - -**Directory structure:** -``` -.tmp/vulnerability_markdown_reports/ -└── vulnerability_reports_/ - ├── CVE-YYYY-XXXXX.md - ├── CVE-YYYY-YYYYY.md - └── ... +test -n "$NVIDIA_API_KEY" +test -n "$TAVILY_API_KEY" ``` -A separate folder is created for each container in the input message. Each folder contains a separate markdown file for each CVE affecting the container. For detailed investigation of specific CVEs, navigate to the corresponding markdown file in `.tmp/vulnerability_markdown_reports/vulnerability_reports_/` - -**Report sections:** - -Each markdown report contains: - -- **Header**: Container name, SBOM info, and overall status (Exploitable/Not Exploitable) -- **CVE Description**: Full vulnerability description with GHSA ID and dispute status if applicable -- **Severity and CVSS Score**: Table showing severity ratings and CVSS scores from multiple sources (GHSA, NVD, RHSA, Ubuntu) -- **EPSS Score**: Exploit Prediction Scoring System probability and percentile -- **Summary**: High-level analysis conclusion with key findings -- **Justification**: VEX status label (e.g., `vulnerable`, `code_not_present`, `code_not_reachable`) with reasoning -- **Vulnerable SBOM Dependencies**: Table of packages from the SBOM that match the CVE -- **Checklist**: Context-sensitive questions generated by the LLM to guide the investigation -- **Checklist Details**: Step-by-step breakdown showing each tool call (Code QA, Documentation QA, Internet Search), inputs, outputs, and source documents retrieved -- **References**: Links to authoritative sources for the CVE - -> [!TIP] -> To return detailed steps taken by the LLM agent in the output, set `return_intermediate_steps` to `true` for the `cve_agent_executor` function in the configuration file. This can be helpful for explaining the output, and for troubleshooting unexpected results. -###### Reviewing an example vulnerability report +A **`docker compose`** setup lives in [`deploy/`](deploy/) for containerized runs; the `vuln-analysis` service uses the same variables at runtime. Run it from the repo root, pointing at the compose file and the project-root `.env`: -An example Markdown-formatted report can be found [here](./.tmp/vulnerability_markdown_reports/vulnerability_reports_morpheus_23_11-runtime/CVE-2023-36632.md). The report opens with key information about the analysis context and gathered CVE intelligence for a quick summary view. - -

-Vulnerability Analysis Report -

- -The rest of the report provides the final summary and justification, followed by step-by-step details of the agent's investigation process, including tool calls and retrieved source documents. This "under-the-hood" view allows analysts to explore the logic and rationale behind the reported status: - -

-Vulnerability Analysis Report -

- -#### Example command: `nat serve` - -Similarly, to run the workflow with the `nat serve` command, you would run: ```bash -nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 -``` - -This command starts an HTTP server that listens on port `26466` and runs the workflow indefinitely, waiting for incoming data to process. This is useful if you want to trigger the workflow on demand via HTTP requests. +# Build and start JupyterLab plus the nginx API cache. +docker compose -f deploy/docker-compose.yml --env-file ./.env up --build -d vuln-analysis nginx-cache -Once the server is running, you can send a `POST` request to the `/generate` endpoint with the input parameters in the request body. The workflow will process the input data and return the output in the terminal and the given output path in the config file. - -Here's an example using `curl` to send a `POST` request. From a new terminal outside of the container, go to the root of the cloned git repository, and run: -```bash -curl -X POST --url http://localhost:26466/generate --header 'Content-Type: application/json' --data @data/input_messages/morpheus:23.11-runtime.json +# Optional: start the scan HTTP service inside the running container. +docker compose -f deploy/docker-compose.yml --env-file ./.env exec -d vuln-analysis \ + sh -lc 'exec nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 > /var/tmp/nat-serve.log 2>&1' ``` -In this command: -- `http://localhost:26466/generate` is the URL of the server and endpoint. -- The `-d` option specifies the data file being sent in the request body. In this case, it's pointing to the input file `morpheus:23.11-runtime.json` under the `data/input_messages/` directory. You can refer to this file as an example of the expected data format. - - Since it uses a relative path, it's important to run the `curl` command from the root of the git repository. Alternatively, you can modify the relative path in the command to directly reference the example json file. -After processing the request, the server will return the the results from the curl request and save the results to the output path specified in the configuration file. The server will also display log and summary results from the workflow as it's running. Additional submissions to the server will append the results to the specified output file. +JupyterLab is available at . If you start `nat serve`, the scan API listens at , with OpenAPI docs at and health checks at . See [`deploy/README.md`](deploy/README.md) for the full walkthrough and self-hosted-NIM overlay. -The workflow also provides a `/generate/async` endpoint for sending requests to workflow asynchronously. The following `curl` command is an example of using this endpoint. The request is processed the same as the `/generate` endpoint but without blocking until the request has completed processing. -```bash -curl -X POST --url http://localhost:26466/generate/async --header 'Content-Type: application/json' --data @data/input_messages/morpheus:23.11-runtime.json -``` -The above command immediately returns a response similar to the following: -``` -{"job_id":"f79951ee-aeea-4a2d-b103-a0a8c15e314b","status":"submitted"} -``` +## Preparing Container Inputs -The status of the request can then be polled using `/generate/async/job/{job_id}`. For example, -```bash -curl --request GET --url http://localhost:26466/generate/async/job/f79951ee-aeea-4a2d-b103-a0a8c15e314b -``` +The pipeline needs a merged rootfs to inspect. Always provide: -> [!NOTE] -> When using the `/generate/async` endpoint, you can ensure that standard output and errors are visible in the server logs by using the `--dask_workers=threads` flag with the `nat serve` command: -> ```bash -> nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 --dask_workers=threads -> ``` -> This flag configures the Dask scheduler to use threaded workers instead of process-based workers, allowing print statements and logging from your workflow to appear directly in the server logs. -> -> When processing multiple async requests concurrently, logs from different requests can be interleaved in the server logs, making it difficult to trace individual requests. If you need to ensure clean, sequential logs for debugging purposes, you can limit the server to process one async request at a time: -> ```bash -> nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 --dask_workers=threads --workers=1 --max_running_async_jobs=1 -> ``` -> Note that this configuration will reduce throughput as the service will only process one async request at a time. -> -> For more information about available flags for the `nat serve` command, refer to the [NeMo Agent Toolkit API Server documentation](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/api-server-endpoints.html#asynchronous-specific-cli-flags). - - -#### Example command: `nat eval` - -The workflow configuration file, [config.yml](src/vuln_analysis/configs/config.yml), includes settings in the `eval` section for use with the [NeMo Agent Toolkit Profiler](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/profiler.html) feature, which can be used to analyze and optimize workflow execution. The profiler can be started using the following command: -```bash -nat eval --config_file=configs/config.yml -``` - -##### Reviewing the profiling results - -The profiler collects usage statistics and stores them in the `.tmp/eval/cve_agent` directory as configured in [config.yml](src/vuln_analysis/configs/config.yml). In this directory, you will find the following files: - - all_requests_profiler_traces.json - - gantt_chart.png - - inference_optimization.json - - standardized_data_all.csv - - workflow_output.json - - workflow_profiling_metrics.json - - workflow_profiling_report.txt - -> [!TIP] -> To analyze these statistics, we highly recommend referring to the NeMo Agent Toolkit documentation, which provides a [**comprehensive guide for analyzing profiling results**](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/profiler.html#analyzing-the-profiling-results) with detailed visualizations and step-by-step examples, including: -> - **Prompt vs Completion Token Analysis**: Scatter plots to compare token usage across LLMs -> - **Workflow Runtime Analysis**: Box plots showing runtime distributions on log scale -> - **Token Efficiency Metrics**: Bar charts comparing total token usage per model -> - **Bottleneck Analysis**: Gantt charts showing where models spend time during execution -> - **Step-by-step walkthrough**: Complete example of profiling a workflow and interpreting results +- **`image_name`** — image repository/name without the tag for automatic extraction (e.g. `nvcr.io/nvidia/morpheus/morpheus`) +- **`image_tag`** — tag to combine with `image_name` (e.g. `25.06-runtime`) -#### Observing traces in Phoenix +Then either omit `filesystem_path` for automatic extraction, or provide `filesystem_path` to use a pre-extracted rootfs. -A separate workflow configuration file, [config-tracing.yml](src/vuln_analysis/configs/config-tracing.yml) is provided that enables tracing in the workflow using [Phoenix](https://phoenix.arize.com/). +### Option A — Automatic (recommended) -First, run the following command in separate terminal within the container to start a Phoenix server on port 6006: -```bash -phoenix serve -``` +Supply `image_name` and `image_tag` in your `PipelineInput`: -You can now use `config-tracing.yml` to run the workflow with tracing enabled: -```bash -nat run --config_file=configs/config-tracing.yml --input_file=data/input_messages/morpheus:23.11-runtime.json +```json +{ + "vulns": [{"vuln_id": "CVE-2025-47273"}], + "image_name": "nvcr.io/nvidia/morpheus/morpheus", + "image_tag": "25.06-runtime", + "output_dir": "outputs" +} ``` -Tracing will also be enabled with `config-tracing.yml` when running workflow with `nat serve` and `nat eval`. -Open your browser and navigate to http://localhost:6006 to view the traces. +For this CVE/image example, the generated VEX result is status `not_affected` with justification `false_positive`. -### Command line interface (CLI) reference +On the first run, the orchestrator pulls the image with `crane`, extracts a flattened rootfs into `${VULN_ANALYSIS_IMAGE_CACHE:-.cache/vuln_analysis/images}//rootfs`, and reuses the cache on subsequent runs. Authentication uses the standard `~/.docker/config.json` (run `crane auth login ` if needed). Local automatic extraction fails if `crane` is not installed and available on `PATH`. -The main entrypoint is the `nat` CLI tool with built-in documentation using the `--help` command. For example, to see what commands are available, you can run: -``` -(.venv) root@04a3a12a687d:/workspace# nat --help -Usage: nat [OPTIONS] COMMAND [ARGS]... - - Main entrypoint for the NeMo Agent Toolkit CLI - -Options: - --version Show the version and exit. - --log-level [debug|info|warning|error|critical] - Set the logging level [default: INFO] - --help Show this message and exit. - -Commands: - configure Configure NeMo Agent Toolkit developer preferences. - eval Evaluate a workflow with the specified dataset. - info Provide information about the local NeMo Agent Toolkit environment. - mcp Run a NeMo Agent Toolkit workflow using the mcp front end. - registry Utility to configure NeMo Agent Toolkit remote registry channels. - run Run a NeMo Agent Toolkit workflow using the console front end. - serve Run a NeMo Agent Toolkit workflow using the fastapi front end. - start Run a NeMo Agent Toolkit workflow using a front end configuration. - uninstall Uninstall a NeMo Agent Toolkit plugin packages from the local... - validate Validate a configuration file - workflow Interact with templated workflows. -``` +### Option B — Pre-extract with the standalone helper -### Configuration file reference -The configuration defines how the workflow operates, including functions, LLMs, and embedders along with general configuration settings. More details about the NeMo Agent toolkit workflow configuration file can be found [here](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/workflow-configuration.html). - -1. **General configuration** (`general`): The `general` section contains general configuration settings for NeMo Agent toolkit which is not specific to any workflow. - - `use_uvloop`: Specifies whether to use `uvloop` event loop which can provide significant speedup. For debugging purposes it is recommended to set this to `false`. - - `telemetry.logging`: Sets log level for logging. - - `telemetry.tracing`: This is used in `config-tracing.yml` where `endpoint` is set to a Phoenix server. Traces of workflow can then be viewed in Phoenix UI. -2. **Functions** (`functions`): The `functions` section contains the tools used in the workflow. - - Preprocessing functions: - - `cve_generate_vdbs`: Generates vector database from code repositories and documentation. - - `agent_name`: Name of the agent executor (`cve_agent_executor`). Used to determine which tools are enabled in the agent to conditionally generate vector databases or indexes. - - `embedder_name`: Name of embedder (`nim-embedder`) configured in `embedders` section. - - `base_vdb_dir`: The directory used for storing vector database files. Default is `.cache/am_cache/vdb`. - - `base_git_dir`: The directory for storing pulled git repositories used for code analysis. Default is `.cache/am_cache/git`. - - `base_code_index_dir`: The directory used for storing code index files. Default is `./cache/am_cache/code_index`. - - `cve_fetch_intel`: Fetches details about CVEs from NIST and CVE Details websites. - - `max_retries`: Maximum number of retries on client and server errors. Default is 5. - - `retry_on_client_errors`: Whether to retry on client errors. Default is true. - - `request_timeout`: Timeout for individual HTTP requests in seconds. Default is 30. - - `intel_source_timeout`: Timeout for each intel source (across all retries) in seconds. `null` means no timeout. Default is `null`. - - `cve_process_sbom`: Prepares and validates input SBOM. - - `cve_check_vuln_deps`: Cross-references every entry in the SBOM for known vulnerabilities. If none are found, the agent is skipped for that CVE. - - `skip`: If true, skips this check and runs all CVEs through the agent. Useful when the input CVEs to the workflow have been validated by a dedicated vulnerability scanner. Default is false. - - Core LLM engine functions: - - `cve_checklist`: Generates tailored, context-sensitive task checklist for impact analysis. - - `llm_name`: Name of LLM (`checklist_llm`) configured in `llms` section. - - `llm_max_rate`: Controls the maximum LLM rate limit (requests per second). If not set or set to `null`, inherits from workflow-level `llm_max_rate`. If set to a number, uses that specific limit. Note: Setting to `null` only results in no rate limiting if the workflow-level setting is also `null`. - - `Container Image Code QA System`: Retriever tool used by `cve_agent_executor` to query source code vector database. - - `Container Image Developer Guide QA System`: Retriever tool used by `cve_agent_executor` to query documentation vector database. - - `Lexical Search Container Image Code QA System`: Lexical search tool used by `cve_agent_executor` to search source code. This tool is an alternative to `Container Image Code QA System` and can be useful for very large code bases that take a long time to embed as a vector database. Disabled by default, enable by uncommenting the tool in `cve_agent_executor`. - - `Internet Search`: SerpApi Google search tool used by `cve_agent_executor`. - - `cve_agent_executor`: Iterates through checklist items using provided tools and gathered intel. - - `llm_name`: Name of LLM (`cve_agent_executor_llm`) configured in `llms` section. - - `tool_names`: Container Image Code QA System, Container Image Developer Guide QA System, (Optional) Lexical Search Container Image Code QA System, Internet Search - - `llm_max_rate`: Controls the maximum LLM rate limit (requests per second) for all LLM calls made by the agent or its tools. If not set or set to `null`, inherits from workflow-level `llm_max_rate`. If set to a number, uses that specific limit. Note: Setting to `null` only results in no rate limiting if the workflow-level setting is also `null`. - - `max_iterations`: The maximum number of iterations for the agent. Default is 10. - - `prompt_examples`: Whether to include examples in agent prompt. Default is `false`. - - `replace_exceptions`: Whether to replace exception message with custom message. Default is `true`. - - `replace_exceptions_value`: If `replace_exceptions` is `true`, use this message. Default is `I do not have a definitive answer for this checklist item."` - - `return_intermediate_steps`: Controls whether to return intermediate steps taken by the agent, and include them in the output file. Helpful for troubleshooting agent responses. Default is `false`. - - `verbose`: Set to true for verbose output. Default is `false`. - - `cve_summarize`: Generates concise, human-readable summarization paragraph from agent results. - - `llm_name`: Name of LLM (`summarize_llm`) configured in `llms` section. - - `llm_max_rate`: Controls the maximum LLM rate limit (requests per second). If not set or set to `null`, inherits from workflow-level `llm_max_rate`. If set to a number, uses that specific limit. Note: Setting to `null` only results in no rate limiting if the workflow-level setting is also `null`. - - `cve_justify`: Assigns justification label and reason to each CVE based on summary. - - `llm_name`: Name of LLM (`justify_llm`) configured in `llms` section. - - `llm_max_rate`: Controls the maximum LLM rate limit (requests per second). If not set or set to `null`, inherits from workflow-level `llm_max_rate`. If set to a number, uses that specific limit. Note: Setting to `null` only results in no rate limiting if the workflow-level setting is also `null`. - - Postprocessing/Output functions - - `cve_file_output`: Outputs workflow results to a file. - - `file_path`: Defines the path to the file where the output will be saved. - - `markdown_dir`: Defines the path to the directory where the output will be saved in individual navigable markdown files per CVE-ID. - - `overwrite`: Indicates whether the output file should be overwritten when the workflow starts if it already exists. Will throw an error if set to `False` and the file already exists. Note that the overwrite behavior only occurs on workflow initialization. For pipelines started in HTTP mode, each new request will append the existing file until the workflow is restarted. -1. **LLMs** (`llms`): The `llms` section contains the LLMs used by the workflow. Functions can reference LLMs in this section to use. The supported LLM API types in NeMo Agent toolkit are `nim` and `openai` The models in this workflow use `nim`. - - Configured models in this workflow: `checklist_llm`, `code_vdb_retriever_llm`, `doc_vdb_retriever_llm`, `cve_agent_executor_llm`, `summarize_llm`, `justify_llm` - - Each `nim` model is configured with the following attributes defined in the NeMo Agent toolkit's [NimModelConfig](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/llm/nim_llm/index.html#nat.llm.nim_llm.NIMModelConfig). Use [OpenAIModelConfig](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/llm/openai_llm/index.html#nat.llm.openai_llm.OpenAIModelConfig) for `openai` models. - - `base_url`: Optional attribute to override `https://integrate.api.nvidia.com/v1` - - `model_name`: The name of the LLM model used by the node. - - `temperature`: Controls randomness in the output. A lower temperature produces more deterministic results. - - `max_tokens`: Defines the maximum number of tokens that can be generated in one output step. - - `top_p`: Limits the diversity of token sampling based on cumulative probability. - - In addition, `nim` models in the Nemo Agent Toolkit also support the following built-in automatic retry configuration options defined by the [RetryMixin](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/data_models/retry_mixin/index.html) class. - - `do_auto_retry`: Whether to enable automatic retries. - - `num_retries`: Number of times to retry a method call that fails with a retryable error. - - `retry_on_status_codes`: List of HTTP status codes that should trigger a retry. - - `retry_on_errors`: List of error messages that should trigger a retry. We recommend including both the error message and error code to accommodate libraries that don’t expose a HTTP status code attribute in raised exceptions. -2. **Embedding models** (`embedders`): The `embedders` section contains the embedding models used by the workflow. Functions can reference embedding models in this section to use. The supported embedding model API types in NeMo Agent toolkit are `nim` and `openai`. - - The models uses `nim` model, `nvidia/nv-embedqa-e5-v5`. - - Each `nim` embedding model is configured with the following attributes defined in the NeMo Agent Toolkit\'s [NimEmbedderModelConfig](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/embedder/nim_embedder/index.html). Use [OpenAIEmbedderModelConfig](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/embedder/openai_embedder/index.html#nat.embedder.openai_embedder.OpenAIEmbedderModelConfig) for `openai` embedding models. - - `base_url`: Optional attribute to override `https://integrate.api.nvidia.com/v1` - - `model_name`: The name of the LLM model used by the node. - - `truncate`: Specifies how inputs longer than the maximum token length of the model are handled. Passing `START` discards the start of the input. `END` discards the end of the input. In both cases, input is discarded until the remaining input is exactly the maximum input token length for the model. If `NONE` is selected, when the input exceeds the maximum input token length an error will be returned. - - `max_batch_size`: Specifies the batch size to use when generating embeddings. We recommend setting this to 128 (default) or lower when using the cloud-hosted embedding NIM. When using a local NIM, this value can be tuned based on throughput/memory performance on your hardware. - - In addition, `nim` models in the Nemo Agent Toolkit also support the following built-in automatic retry configuration options defined by the [RetryMixin](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/data_models/retry_mixin/index.html) class. - - `do_auto_retry`: Whether to enable automatic retries. - - `num_retries`: Number of times to retry a method call that fails with a retryable error. - - `retry_on_status_codes`: List of HTTP status codes that should trigger a retry. - - `retry_on_errors`: List of error messages that should trigger a retry. We recommend including both the error message and error code to accommodate libraries that don’t expose a HTTP status code attribute in raised exceptions. -3. **Workflow** (`workflow`): The `workflow` section ties the previous sections together by defining the tools and LLM models to use in the workflow. - - `_type`: This is set to `cve_agent` indicating to NeMo Agent toolkit to use the function defined in [register.py](src/vuln_analysis/register.py) for the workflow. - - `missing_source_action`: Action when source analysis is unavailable in the agent due to: missing source_info, VDB generation failures, or inaccessible repositories. Options are: - - `error`: Fail pipeline with validation error - - `skip_agent`: Collect intel and check dependencies, but skip agent - - `continue_with_warning`: Run full pipeline with warning log (degraded analysis quality). Default is "continue_with_warning" for backwards compatibility. - - `llm_max_rate`: Controls the maximum LLM rate limit (requests per second) globally for all workflow functions. Individual function `llm_max_rate` settings override this value. `null` means no rate limiting. Default is 5. This setting is particularly useful for: - - Setting a baseline rate limit across all functions in the workflow - - Preventing rate limit errors when using cloud-hosted models - - **Note:** Setting `llm_max_rate: null` or omitting it at the function-level will inherit the workflow-level setting instead of disabling rate limiting. To disable rate limiting across all functions, set `llm_max_rate: null` or omit it at the workflow-level. As a proxy for disabling rate limiting for specific functions, set the function-level `llm_max_rate` to a high value. (Fully disabling rate limiting for individual functions is currently not supported.) - - **Example configuration:** - ```yaml - functions: - cve_checklist: - # llm_max_rate not set - inherits global limit of 5 requests/second - cve_agent_executor: - llm_max_rate: null # Also inherits global limit of 5 requests/second - cve_justify: - llm_max_rate: 10 # Overrides global limit with 10 requests/second - - workflow: - _type: cve_agent - llm_max_rate: 5 # Global limit of 5 requests/second for all functions - ``` - - The remaining configuration items correspond to attributes in [CVEWorkflowConfig](src/vuln_analysis/register.py) to specify the registered tools to use in the workflow. -4. **Evaluations and Profiling** (`eval`): The `eval` section contains the evaluation settings for the workflow. Refer to [Evaluating NVIDIA Agent Intelligence Toolkit Workflows](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/evaluate.html) for more information about NeMo Agent toolkit built-in evaluators as well as the plugin system to add custom evaluators. An example evaluation pipeline has been provided in the [Evaluation](#evaluation) section of this README. Additionally, the CVE workflow uses the `eval` section to configure a profiler that uses the NeMo Agent Toolkit evaluation system to collect usage statistics and store them to the local file system. You can find more information about NeMo Agent toolkit profiling and performance monitoring [here](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/profiler.html). - - `general.output_dir`: Defines the path to the directory where profiling results will be saved. - - `general.dataset`: Defines file path and format of dataset used to run profiling. - - `evaluators`: Custom evaluators defined in `eval/evaluators`. See [Evaluation](#evaluation) for more details on each custom evaluator and their configurations. - - `profiler`: The profiler for this workflow is configured with the following options. - - `token_uniqueness_forecast`: Compute inter query token uniqueness - - `workflow_runtime_forecast`: Compute expected workflow runtime - - `compute_llm_metrics`: Compute inference optimization metrics - - `csv_exclude_io_text`: Avoid dumping large text into the output CSV (helpful to not break structure) - - `prompt_caching_prefixes`: Identify common prompt prefixes - - `bottleneck_analysis`: Enable bottleneck analysis - - `concurrency_spike_analysis`: Enable concurrency spike analysis. Set the `spike_threshold` to 7, meaning that any concurrency spike above 7 will be raised to the user specifically. - -### NGINX caching server - -The docker compose file includes an `nginx-cache` proxy server container that enables caching for API requests made by the workflow. It is highly recommend to route API requests through the proxy server to reduce API calls for duplicate requests and improve workflow speed. This is especially useful when running the workflow multiple times with the same configuration (for example, for debugging) and can help keep costs down when using paid APIs. - -The NGINX proxy server is started by default when running the `vuln-analysis` service. However, it can be started separately using the following command: -```bash -cd ${REPO_ROOT} -docker compose up --detach nginx-cache -``` +To warm the cache, debug extraction, or hand a pre-extracted rootfs to the pipeline, run: -To use the proxy server for API calls in the workflow, you can set environment variables for each base URL used by the workflow to point to `http://localhost:${NGINX_HOST_HTTP_PORT}/`. These are set automatically when running the `vuln-analysis` service, but can be set manually in the `.env` file as follows: ```bash -CWE_DETAILS_BASE_URL="http://localhost:8080/cwe-details" -DEPSDEV_BASE_URL="http://localhost:8080/depsdev" -FIRST_BASE_URL="http://localhost:8080/first" -GHSA_BASE_URL="http://localhost:8080/ghsa" -NGC_API_BASE="http://localhost:8080/nemo/v1" -NIM_EMBED_BASE_URL="http://localhost:8080/nim_embed/v1" -NVD_BASE_URL="http://localhost:8080/nvd" -NVIDIA_API_BASE="http://localhost:8080/nim_llm/v1" -OPENAI_API_BASE="http://localhost:8080/openai/v1" -OPENAI_BASE_URL="http://localhost:8080/openai/v1" -RHSA_BASE_URL="http://localhost:8080/rhsa" -SERPAPI_BASE_URL="http://localhost:8080/serpapi" -UBUNTU_BASE_URL="http://localhost:8080/ubuntu" -``` - -## Customizing the Workflow - -The primary method for customizing the workflow is to generate a new configuration file with new options. The configuration file defines the workflow settings, such as the functions, the LLM models used, and the output format. The configuration file is a YAML file that can be modified to suit your needs. - -### Customizing the SBOM input - -To use an SBOM from a URL ([example](https://raw.githubusercontent.com/NVIDIA-AI-Blueprints/vulnerability-analysis/refs/heads/main/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus%3Av23.11.01-runtime.sbom)), update the SBOM info configuration to: -``` - "sbom_info": { - "_type": "http", - "url": "https://raw.githubusercontent.com/NVIDIA-AI-Blueprints/vulnerability-analysis/refs/heads/main/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus%3Av23.11.01-runtime.sbom" - } -``` - -### Customizing the LLM models - -The workflow configuration file also allows customizing the LLM model and parameters for each component of the workflow, as well as which LLM API is used when invoking the model. - -In any configuration file, locate the `llms` section to see the current settings. For example, the following snippet defines the LLM used for the default LLM model: -``` - default_llm: - _type: nim - base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - model_name: ${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct} - temperature: 0.0 - max_tokens: 2000 - top_p: 0.01 +uv run python scripts/extract_image.py nvcr.io/nvidia/morpheus/morpheus:25.06-runtime /tmp/img-cache ``` -* `_type`: specifies the LLM API type. Refer to the [Supported LLM APIs](#supported-llm-apis) table for available options. -* `base_url`: Base URL for LLM. Sets to value of `NVIDIA_API_BASE` environment variable if set. Otherwise, sets to default NIM base URL. `base_url` can also be omitted in which case, the default base URL is used for type `nim`. We use an environment variable here so that we can easily set it to proxy server URL when running with `docker compose`. -* `model_name`: specifies the model name within the LLM API. Sets to value of `DEFAULT_MODEL_NAME` environment variable if set. Otherwise, sets to default model, `meta/llama-3.1-70b-instruct`. This is also applicable to the other LLM models in the workflow, each having their own environment variable for setting the model name. Refer to the LLM API documentation to determine the available models. -* `temperature`, `max_tokens`, `top_p`, ...: specifies the model parameters. The available parameters can be found in the NeMo Agent Toolkit [NIMModelConfig](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/llm/nim_llm/index.html). Any non-supported parameters provided in the configuration will be ignored. - -Instead of using a single default LLM model for the entire workflow, you can optionally use different LLM models and configurations for each function (e.g., checklist, agent, summarize, etc.). - -To configure a custom LLM for a specific function: -1. In the config file's `llms` section, uncomment the relevant LLM configuration (e.g., `checklist_llm`, `cve_agent_executor_llm`, `summarize_llm`, etc.) -2. Update the model name and other settings as needed -3. In the `functions` section, update the function's `llm_name` parameter to reference your custom LLM configuration - -For example, to use a different model for the checklist function, uncomment the `checklist_llm` configuration. - -```yaml -llms: - checklist_llm: - _type: nim - model_name: meta/llama-3.1-8b-instruct # Different model - temperature: 0.0 - max_tokens: 2000 - # ... other parameters -``` - -In the functions section, update the `llm_name` to reference the configuration we just updated. - -```yaml -functions: - cve_checklist: - _type: cve_checklist - llm_name: checklist_llm # Reference the LLM we just configured -``` - -#### Supported LLM APIs -| Name | `_type` | Auth Env Var(s) | Base URL Env Var(s) | Proxy Server Route | -| ------------------------------------------------------------------------------------------------------------ | -------------- | ----------------------------- | ------------------------------------------------------------------------------- | ------------------ | -| [NVIDIA Inference Microservices (NIMs)](http://build.nvidia.com/) (Default) | `nim` | `NVIDIA_API_KEY` | `NVIDIA_API_BASE` | `/nim_llm/v1` | -| [OpenAI](https://platform.openai.com/docs/api-reference/authentication) | `openai` | `OPENAI_API_KEY` | `OPENAI_API_BASE` (used by `langchain`)
`OPENAI_BASE_URL` (used by `openai`) | `/openai/v1` | - - -#### Steps to configure an LLM model -1. Obtain an API key and any other required auth info for the selected service. -2. Update the `.env` file with the auth and base URL environment variables for the service as indicated in the [Supported LLM APIs](#supported-llm-apis) table. If you choose not to use the default LLM models in your workflow (`meta/llama-3.1-70b-instruct`), you can also add environment variables to override the model names to your `.env` file. In addition to `DEFAULT_MODEL_NAME`, you can also set `model_name` for the other LLM models using `CHECKLIST_MODEL_NAME`, `CODE_VDB_RETRIEVER_MODEL_NAME`, `DOC_VDB_RETRIEVER_MODEL_NAME`, `CVE_AGENT_EXECUTOR_MODEL_NAME`, `SUMMARIZE_MODEL_NAME`, and `JUSTIFY_MODEL_NAME`. -3. Update the config file as described above. For example, if you want to use OpenAI's `gpt-4o` model for checklist generation, update `checklist_llm` in the `llms` section to: -``` - checklist_llm: - _type: openai - model_name: ${CHECKLIST_MODEL_NAME:-gpt-4o} - temperature: 0.0 - seed: 0 - top_p: 0.01 - max_retries: 5 -``` -Please note that the prompts have been tuned to work best with the Llama 3.1 70B NIM and that when using other LLM models it may be necessary to adjust the prompting. - -### Customizing the embedding model -Vector databases are used by the agent to fetch relevant information for impact analysis investigations. The embedding model used to vectorize your documents can significantly affect the agent's performance. The default embedding model used by the workflow is the NIM [nvidia/nv-embedqa-e5-v5](https://build.nvidia.com/nvidia/nv-embedqa-e5-v5) model, but you can experiment with different embedding models of your choice. - -To test a custom embedding model, modify the workflow configuration file (for example, `config.yml`) in the `embedders` section. For example, the following snippet defines the settings for the default embedding model. The full set of available parameters for a NIM embedder can be found [here](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/embedder/nim_embedder/index.html). -``` - nim_embedder: - _type: nim - base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - model_name: ${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} - truncate: END - max_batch_size: 128 -``` -* `_type`: specifies the LLM API type. Refer to the [Supported LLM APIs](#supported-llm-apis) table for available options. -* `base_url`: Base URL for embedding model. Sets to value of `NVIDIA_API_BASE` environment variable if set. Otherwise, sets to default NIM base URL. `base_url` can also be omitted in which case, the default base -* `model_name`: specifies the model name for the embedding provider. Sets to value of `EMBEDDER_MODEL_NAME` environment variable if set. Otherwise, sets to default embedding model, `nvidia/nv-embedqa-e5-v5`. Refer to the embedding provider's documentation to determine the available models. -* `truncate`: specifies how inputs longer than the maximum token length of the model are handled. Passing `START` discards the start of the input. `END` discards the end of the input. In both cases, input is discarded until the remaining input is exactly the maximum input token length for the model. If `NONE` is selected, when the input exceeds the maximum input token length an error will be returned. -* `max_batch_size`: specifies the batch size to use when generating embeddings. We recommend setting this to 128 (default) or lower when using the cloud-hosted embedding NIM. When using a local NIM, this value can be tuned based on throughput/memory performance on your hardware. - -#### Steps to configure an alternate embedding provider -1. If using OpenAI embeddings, first obtain an API key, then update the `.env` file with the auth and base URL environment variables for the service as indicated in the [Supported LLM APIs](#supported-llm-apis) table. Otherwise, proceed to step 2. If you choose not to use the default embedding model (`nvidia/nv-embedqa-e5-v5`), you can also add `EMBEDDER_MODEL_NAME` to your `.env` file to override the default. -2. Update the `embedders` section of the config file as described above. - - - Example OpenAI embedding configuration: - ``` - nim_embedder: - _type: openai - model_name: ${EMBEDDER_MODEL_NAME:-text-embedding-3-small} - max_retries: 5 - ``` - - For OpenAI models, only a subset of parameters are supported. The full set of available parameters can be found in the config definitions [here](https://docs.nvidia.com/nemo/agent-toolkit/latest/api/nat/embedder/openai_embedder/index.html). Any non-supported parameters provided in the configuration will be ignored. -The current workflow uses [FAISS](https://ai.meta.com/tools/faiss/) to create the vector databases. Interested users can customize the source code to use other vector databases such as [cuVS](https://rapids.ai/cuvs/). +The helper prints the resolved digest and rootfs path, e.g. `/tmp/img-cache/sha256:.../rootfs`. Plug that into `PipelineInput`: -### Customizing the Output +- **`filesystem_path`**: path to the extracted rootfs +- **`image_name`**: image repository/name +- **`image_tag`**: image tag +- **`image_config_path`** (optional): path to `config.json` written alongside the rootfs -Currently, there are 3 types of outputs supported by the workflow: -- **File output**: The output data is written to a file in JSON format. -- **HTTP output**: The output data is posted to an HTTP endpoint. -- **Print output**: The output data is printed to the console. +This helper also shells out to `crane`; if `crane` is missing, install it from [Prerequisites](#prerequisites) or put your custom binary on `PATH`. -To customize the output, modify the workflow configuration file accordingly. Locate the `workflow` section to see the output destination used by the workflow. For example, in the configuration file `configs/config.yml`, the following snippet from the `functions` section defines the function that writes the workflow output as a single json file and individual markdown files per CVE-ID: -``` - cve_file_output: - _type: cve_file_output - file_path: .tmp/output.json - markdown_dir: .tmp/vulnerability_markdown_reports - overwrite: True -``` -The following snippet from the `workflow` section then configures the workflow to use the above function for output: -``` - workflow: - _type: cve_agent - ... - cve_output_config_name: cve_file_output -``` +## Usage -To post the output to an HTTP endpoint, you can add the following to the `functions` section of the config file, replacing the domain, port, and endpoint with the desired destination (note the trailing slash in the "url" field). The output will be sent as JSON data. +### Full Pipeline -``` - cve_http_output: - _type: cve_http_output - url: http://:/ - endpoint: "" -``` - -The workflow can then be updated to use the new function: -``` - workflow: - _type: cve_agent - ... - cve_output_config_name: cve_http_output -``` +Run all four agents end-to-end via NAT: -Additional output options will be added in the future. - - -### Adding Test Time Compute (TTC) -**Test time compute (TTC)** is a technique that can improve agentic workflow performance by allocating more compute at inference time. There are a variety of available TTC strategies. This blueprint includes an implementation of the **majority voting** TTC strategy using the NeMo Agent Toolkit's [Test Time Compute](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/test-time-compute.html) module. Majority voting is a simple strategy that executes the workflow multiple times, then takes a majority vote on the output, using the most frequent justification status as the final output status. This improves both the accuracy and the consistency of the workflow predictions. - -To run the workflow with the default majority voting test time compute settings, **it's important to first disable LLM caching** to avoid voting on identical results each time (see [Disable Caching](#disable-caching) below). Once LLM caching is disabled, use the `nat run` command as usual, while passing in the [config-ttc.yml](./src/vuln_analysis/configs/config-ttc.yml) configuration file: ```bash -nat run --config_file=configs/config-ttc.yml --input_file=data/input_messages/morpheus:23.11-runtime.json -``` - -This will run the workflow multiple times (3 times by default in `config-ttc.yml`) and return the first execution output for each CVE that matches the majority vote. - -#### Reviewing the TTC output - -By default, the output artifacts produced by TTC are identical to those of the regular workflow (e.g. `output.json`, markdown reports, workflow return object). See the [Reviewing the workflow output](#reviewing-the-workflow-output) section for more info. The key difference when running TTC is that it improves the accuracy and consistency of the *predictions* in the output (i.e., affected status) by taking the majority vote. - -#### Customizing the TTC configuration - -To customize test time compute, modify the [config-ttc.yml](./src/vuln_analysis/configs/config-ttc.yml) configuration file accordingly. - -First, modify the `ttc_strategies` section of the config file as needed: - -```yaml -ttc_strategies: - selection_strategy_majority_voting: - _type: majority_voting_selection - selection_mode: decisive +nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json ``` -* `selection_strategy_majority_voting`: The name of the TTC strategy that can be referenced in the workflow configuration below. -* `_type`: Specifies the type of TTC strategy. Currently only `majority_voting_selection` is implemented. -* `selection_mode`: Determines how the majority vote is calculated. Two modes are available (Default is `decisive`): - - `simple`: A standard majority vote where ties are marked as `UNKNOWN`. - - `decisive`: Ignores majority `UNKNOWN` votes in favor of the next most frequent label, except in the case of a tie. (This helps penalize `UNKNOWN` labels, which can be overrepresented in model outputs relative to real-world distributions.) - -Next, modify the `workflow` section of the configuration as needed: - -```yaml -workflow: - _type: execute_then_select_function - selector: selection_strategy_majority_voting - augmented_fn: cve_agent_workflow - output_fn: cve_file_output - num_executions: 3 - max_concurrency: 1 - early_stop_threshold: false -``` -* `_type`: Indicates the test time compute orchestration function to be run in the workflow. The `execute_then_select_function` is responsible for executing the workflow multiple times, then running the `selector` to choose the final result. -* `selector`: The name of the test time compute selector function used to select the final result. This must match a strategy name defined in the `ttc_strategies` section. Here, `selection_strategy_majority_voting` uses the majority voting selector. -* `augmented_fn`: The name of the workflow function to be executed multiple times. This must match a function defined in the `functions` section. Here, we reference the main `cve_agent_workflow` function. -* `output_fn`: The name of the output function to use in order to save the TTC output. This must match a function defined in the `functions` section. Set to `null` to not save any output. Default is `cve_file_output`. -* `num_executions`: The maximum number of times to execute the workflow before voting occurs. Must be at least 1. Default is 3. -* `max_concurrency`: The number of workflows to run simultaneously. Default is 1. -* `early_stop_threshold`: If set to an integer, _n_, stops executing the workflow for a given CVE after _n_ consecutive identical justification statuses are observed, improving efficiency for stable outputs. If `false`, always runs the full `num_executions` for all CVEs. The early stopping threshold number must be strictly less than the `num_executions`. Default is `false`. - -> [!NOTE] -> Since TTC executes the workflow multiple times, it's recommended to disable the output in the base workflow by setting `cve_agent_workflow.cve_output_config_name` to null to avoid generating output for each execution. Instead, configure the output function by setting `output_fn` in the `execute_then_select_function` to generate output for the final results from TTC. - -> [!WARNING] -> Increasing `num_executions` typically boosts accuracy and consistency. However, it also increases cost in LLM calls, token usage, and latency. It is important to benchmark your specific use case to find the optimal balance between accuracy, performance, and cost. Consider starting with `num_executions: 3` and incrementing based on your accuracy requirements. Also consider enabling early stopping to reduce costs when consecutive runs produce stable results. -> -> **Note:** When using NVIDIA-hosted model endpoints, you may encounter `429: Too Many Requests` errors with `max_concurrency` greater than 1. See [429 Errors - Too Many Requests](#429-errors---too-many-requests) for solutions. - - -#### Writing a custom TTC strategy - -You can also implement custom TTC strategies other than majority voting by following the NeMo Agent Toolkit [Test Time Compute reference](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/test-time-compute.html). +For automatic extraction, leave `filesystem_path` unset. For a pre-extracted rootfs, add `filesystem_path` and optionally `image_config_path`. -## Evaluating the Workflow -Similar to unit tests in software development, evaluation is an essential part of developing agentic workflows. As the agentic workflow is modified during development, evaluation pipelines enable a structured way of ensuring performance. In this section, we use NeMo Agent toolkit's [evaluation](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/evaluate.html) feature to implement an evaluation pipeline for the vulnerability analysis workflow. The core behavior of the vulnerability analysis workflow can be assessed by evaluating the two key metrics of `accuracy` and `consistency`. Advanced developers can add their own [custom evaluators](#writing-custom-evaluators) that target other metrics. +`nat run` can exit with shell status `0` even when the workflow result contains `"success": false`. For automation, inspect the top-level `success` field and each per-CVE `results[].success` value. -> [!NOTE] -> The evaluation pipeline is not a part of the stable core workflow API, and may be updated in future releases as agentic workflow evaluation best practices evolve. +**`PipelineInput` fields** (orchestrator flags in JSON form): -### Running Evaluation (Overview) -To run the evaluation pipeline, use the `nat eval` command with the `config-eval.yml` configuration file. - -``` -nat eval --config_file=configs/config-eval.yml -``` - -This will run evaluation for accuracy on the workflow's output fields of `status` and `label`. Results can be viewed in `.tmp/evaluators`, in the files with the suffix "accuracy_output." - -Accuracy is calculated by comparing the current workflow run's results against a ground truth answer key, known as an evaluation dataset. A mock evaluation dataset is given. Developers looking to use the evaluation pipeline should provide their own evaluation datasets; see [Inputting Evaluation Datasets](#inputting-evaluation-datasets) for details. More details on the implementation and outputs of the accuracy evaluator can be found in the [Accuracy Evaluator](#accuracy-evaluator) section. - -To measure the workflow's consistency, we examine the results of the workflow across multiple runs. To enable this evaluator, uncomment the last section of `config-eval.yml`. Then, follow the [Disable Caching](#disable-caching) section of the README to disable caching, and run the workflow with the `--reps` flag to enable multiple repetitions. Results can be viewed in `.tmp/evaluators`, in the files with the suffix "consistency_output." -``` -nat eval --config_file=configs/config-eval.yml --reps=3 -``` -More details on running with repetitions can be found in the [Running Multiple Evaluation Repetitions](#running-multiple-evaluation-repetitions) section. More details on the implementation and outputs of the consistency evaluator can be found in the [Consistency Evaluator](#consistency-evaluator) section. +| Field | Description | +|-------|-------------| +| `vulns` | Vulnerability list to analyze (at least one entry). Each entry has `vuln_id` (CVE or GHSA identifier) plus optional `package_hint` and `version_hint` | +| `image_name` | Container image repository/name without the tag | +| `image_tag` | Container image tag; required for both automatic extraction and pre-extracted filesystem inputs | +| `filesystem_path` | Path to a pre-extracted container filesystem. If omitted, the orchestrator pulls and extracts `image_name:image_tag` via `crane` | +| `image_config_path` | Path to an image config JSON file or `null` (auto-populated with the cached `config.json` path when `filesystem_path` is omitted) | +| `image_digest` | Resolved image digest (auto-populated when `filesystem_path` is omitted) | +| `skip_web_research` | If `true`, skip container web research (useful for private images) | +| `output_dir` | Root directory for all outputs (default: `outputs`) | +| `cache` | If `false`, disable on-disk stage caching (see [Caching](#caching)) | +| `cache_cve_researcher`, `cache_container_analyzer`, … | Optional per-agent cache overrides | -For advanced developers looking to write custom evaluators to measure other metrics, see the [Writing Custom Evaluators](#writing-custom-evaluators) section. +### Individual Agents +Each agent can be run independently for targeted analysis or debugging. The sample JSON files are starting points: some contain placeholder filesystem paths, and later-stage examples depend on reports generated by earlier stages. -### Inputting Evaluation Datasets -Evaluation datasets containing ground truth labels should be put in the `data/eval_datasets/` directory. A test set should be a JSON file with the following structure: - -``` -{ - "dataset_id": "...", - "dataset_description": "...", - "containers": { - "containerA": { - "container_image": { - "name": "...", - "tag": "...", - "source_info": [...], - "sbom_info": {...} - }, - "ground_truth": [ - { - "vuln_id": "CVE-XXXX-XXXX", - "status": "NOT AFFECTED", - "label": "..." - } - ] - }, - "containerB": { - "container_image": {...}, - "ground_truth": [...] - } - } -} -``` - -Multiple JSON files can be created to represent different test sets, which is useful for organization when running different evaluation experiments. The test set to be used is configurable via the `eval.general.dataset.filepath` field. - -A mock test set `eval_dataset.json` is provided, which includes two containers (`morpheus:23.11-runtime` and `morpheus:24.03-runtime`) that have a few CVEs each. Each entry in the `containers` field consists of container metadata, along with a list of CVEs and their corresponding ground truth labels. - -A parsing script (`eval/parse_eval_input.py`) is used to transform test sets into a format ingestible by the workflow. Advanced developers can choose to modify the input test set structure by creating their own parsing script. The path to this script is configurable at `eval.general.dataset.function`. Reference the NeMo Agent Toolkit documentation on [Custom Dataset Format](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/evaluate.html#custom-dataset-format) to learn more. - -Additionally, the output directory of evaluation results is configurable via the `eval.general.output_dir` field. - -> [!NOTE] -> For large input test sets, developers may encounter `429: Too Many Requests` errors if using NVIDIA-hosted model endpoints. See [Troubleshooting](#429-errors---too-many-requests) for how to mitigate this. - - -### Running Multiple Evaluation Repetitions - -The NeMo Agent Toolkit provides a `--reps` flag, which can be used to conveniently execute an evaluation run multiple times. This is useful for capturing metrics such as workflow consistency. Note that **caching must be disabled first** (see [Disable Caching](#disable-caching) below) in order for evaluation repetitions to re-run the workflow instead of relying on cached results. - -Below is an example of how to run an evaluation experiment with three repetitions: - -``` -nat eval --config_file=configs/config-eval.yml --reps=3 -``` +- `data/examples/container_analyzer_input.json` needs a real extracted filesystem path. +- `data/examples/exploitability_analyzer_input.json` expects a generated vulnerability report under `outputs/vulnerability_reports/`. +- `data/examples/vex_categorizer_input.json` expects generated exploitability and container reports under `outputs/`. -> [!NOTE] -> Running multiple repetitions will take extra time. Additionally, developers may encounter `429: Too Many Requests` errors if using hosted model endpoints, due to the concurrency introduced when running multiple repetitions. See [Troubleshooting](#429-errors---too-many-requests) for how to mitigate this. - - -#### Disable Caching -LLM caching should be disabled for evaluation with multiple reps to measure the workflow's accuracy distribution and consistency. Two options for disabling caching are provided below. - -##### Option 1: Disable Caching Globally -Globally disable all LLM caching by setting the `NVIDIA_API_BASE` variable to directly access the hosted model endpoint instead of going through NGINX. - -``` -export NVIDIA_API_BASE=https://integrate.api.nvidia.com/v1 -``` - -Re-enable caching by setting the `NVIDIA_API_BASE` variable back to NGINX. -``` -export NVIDIA_API_BASE=http://nginx-cache/nim_llm/v1 -``` - - -##### Option 2: Disable Caching on Specific Components -This option enables developers to disable caching for specific parts of the workflow. This approach is useful for in-depth evaluation experiments; for example, a developer may want to disable caching for just the `justify_llm` in order to test consistency of the justification part of the workflow without re-running everything before that point. +```bash +nat run --config_file=configs/config-cve-researcher.yml --input_file=data/examples/cve_researcher_input.json -In `config-eval.yml`, assign the `base_url` field of the target components to `"https://integrate.api.nvidia.com/v1"`. We recommend creating multiple config files to keep track of different evaluation experiments. To re-enable caching, simply assign the `base_url` back to `${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}`. +nat run --config_file=configs/config-container-analyzer.yml --input_file=data/examples/container_analyzer_input.json -For quick testing, developers can also change configuration in the CLI using the `--override` flag, which uses dot notation. This change will not persist across future runs. Below is an example that disables caching on the `checklist_llm`. +nat run --config_file=configs/config-exploitability-analyzer.yml --input_file=data/examples/exploitability_analyzer_input.json +nat run --config_file=configs/config-vex-categorizer.yml --input_file=data/examples/vex_categorizer_input.json ``` -nat eval --config_file=configs/config-eval.yml --reps=3 --override llms.checklist_llm.base_url https://integrate.api.nvidia.com/v1 -``` - -### Accuracy and Consistency Evaluators -In this section, the implementation of the two evaluators for accuracy and consistency are discussed in detail. - -#### Accuracy Evaluator -For the vulnerability analysis agentic workflow, the most basic method to assess workflow performance is to measure accuracy of the `status` and `label` fields. - -Each CVE has a ground truth `status` field, as well as a `label` field that corresponds to the status. Valid statuses include: `AFFECTED` or `NOT AFFECTED`. Valid labels include: `vulnerable` if the CVE status is `AFFECTED`, or one of 10 VEX statuses if the status is `NOT AFFECTED` (see **Justification Assignment** under the [Key Components section](#key-components)). - -For each CVE, if the workflow's output matches the ground truth label, a score of 1 is assigned. Otherwise, a score of 0 is assigned. The evaluator then takes the mean of all CVEs' scores to produce an accuracy score between 0 and 1. +### HTTP server (`nat serve`) -##### Configuration Options -The accuracy evaluator is implemented in `eval/evaluators/accuracy.py` and registered in NeMo Agent Toolkit with the unique identifer (`_type`) of `accuracy`. +Instead of one-shot `nat run` invocations, you can host the `cve_pipeline` workflow as a long-running HTTP service with **`nat serve`**. This launches a FastAPI server that loads the workflow once and accepts vulnerability-analysis requests over REST — convenient for integrating the pipeline into a CI/CD scan stage, a dashboard, or any system that already speaks HTTP. -The evaluator takes in a target field (`status` or `label`), which can be configured in `config-eval.yml` under `evaluators.[evaluator name].field`. This design follows a common NeMo Agent Toolkit design pattern to promote code reusability. In `config-eval.yml`, a `status_accuracy_evaluator` and a `label_accuracy_evaluator` are set up as two separate evaluators, but under the hood, they share the same `_type` and therefore the same source code. - -The evaluator also takes in a `duplicates_policy` field. This is to account for instances where, if an input set is an aggregation of ground truths from different sources, the set may contain duplicate CVEs with conflicting answers. For example, a CVE may be listed twice in the dataset as both `AFFECTED` and `NOT AFFECTED`, with different label justifications. We provide three policies for handling duplicates: -- `drop_all`: Any CVE with conflicting ground truth will be omitted and not contribute towards evaluation results. -- `keep_first`: Use the first encountered instance of a CVE as the truth, disregard all other instances. -- `keep_all`: Any answer listed in the ground truth for a CVE is considered valid (eg. if the ground truth has two listings of a CVE, with one being `AFFECTED` and the other being `NOT AFFECTED`, either answer given by the pipeline will be considered correct). - - -##### Output Format -The final scores are written to the files `status_accuracy_output.json` and `label_accuracy_output.json`. The output will include an `average_score` overview field, and a an `eval_output_items` field which gives a more detailed breakdown. - -###### average_score Field -The `average_score` field contains a dict with metrics corresponding to `pandas.Dataframe.describe` output. The goal of this field is to aggregate the accuracy score across all runs, e.g., when using the `--reps` option. If only one run is performed, `count` and `mean` would be the only meaningful metrics; for example, if the run yielded accuracy of 0.8, the `average_score` field would look like this: -```json -"average_score": { - "count": 1, - "mean": 0.8, - "std": null, - "min": 0.8, - "25%": 0.8, - "50%": 0.8, - "75%": 0.8, - "max": 0.8 -} -``` -Meanwhile, if there were multiple runs, the `average_score` field would capture the accuracy distribution across runs. For example, with 5 runs, the output might look like this: -```json -"average_score": { - "count": 5, - "mean": 0.74, - "std": 0.07, - "min": 0.65, - "25%": 0.7, - "50%": 0.75, - "75%": 0.8, - "max": 0.8 -} -``` -A script is provided in `eval/visualizations/box_and_whisker_plot.py` to visualize this distribution as a box and whisker plot. It is especially helpful for comparing the plots of multiple experiments side-by-side, or comparing `status` accuracy against `label` accuracy. It can be run like so: +#### Start the server ```bash -python3 src/vuln_analysis/eval/visualizations/box_and_whisker_plot.py \ - .tmp/evaluators/accuracy-output-1.json \ - .tmp/evaluators/accuracy-output-2.json \ - --title "Accuracy: Experiment 1 vs. Experiment 2" \ - --save src/vuln_analysis/eval/visualizations/my-plot.png \ - --labels my-experiment1 my-experiment2 -``` -Users can list any number of output files, as well as configure chart title, save location, and X-axis labels. - -###### eval_output_items Field -The `eval_output_items` field gives a more detailed output of the experiment. It lists the runs, giving each run a unique `id`, a `score` (the accuracy score for that run), and a `reasoning` field. - -This last field breaks the run down by container, and lists the specific CVEs that were run in each container (`question`), the ground truth answers `[target field]_answer`, and the answers generated by the workflow (`generated_[target field]_answer`). - - - -#### Consistency Evaluator -This evaluator measures the consistency of results across repeated runs. It can be configured to measure consistency of the `status` or `label` fields. - -Consistency is defined as: `∑p²` per CVE, where *p* is the probability of each unique status or label. For example, if the status for a CVE across 3 runs are `(AFFECTED, NOT_AFFECTED, NOT_AFFECTED)`, then the probabilities become `p(AFFECTED) = 1/3`, `p(NOT_AFFECTED) = 2/3`. Therefore, the consistency score is `(1/3)² + (2/3)² = 0.56`. A score of 1.0 means perfect consistency (all runs yielded the exact same values). The evaluator outputs a consistency score for each CVE. - -> [!NOTE] -> This evaluator should only be used if evaluation is being run with [multiple repetitions](#running-multiple-evaluation-repetitions). An error will occur if this criteria is not met. To enable this evaluator, uncomment the last section in `config-eval.yml`. - - -##### Configuration Options -The accuracy evaluator is implemented in `eval/evaluators/consistency.py` and registered in NeMo Agent Toolkit with the unique identifer (`_type`) of `consistency`. - -The evaluator takes in a configurable target field (`status` or `label`), following a similar design pattern as the [Simple Accuracy Evaluator](#configuration-options) - -##### Output Format -The final scores are written to the files `status_consistency_output.json` and `label_consistency_output.json`. - -The output will include an `average_score` field which gives the average consistency score across all containers, and an `eval_output_items` field which lists the input containers. - -For each container in the `eval_output_items` field, there is an unique `id` and a consistency `score`, which is the mean of the consistency scores of each CVE within that container. There is also a `reasoning` field that displays the following details: - - `status_consistencies`: consistency score for a specific CVE - - `total_cves`: the total number of CVEs in the container - - `num_reps`: number of evaluation repetitions that were run - -### Writing Custom Evaluators -A custom evaluator consists of three components, an evaluator configuration, the evaluator's core logic, and registration of the evaluator into the toolkit. - -**Configuration:** The evaluator must have a [configuration](https://docs.nvidia.com/nemo/agent-toolkit/1.2/extend/custom-evaluator.html#evaluator-configuration) class that defines the evaluator `name` and any evaluator-specific parameters. The evaluator `name` is a unique identifier that the evaluator will be registered under in the NeMo Agent toolkit. During configuration of an evaluation pipeline (for example, in `config-eval.yml`), the `name` corresponds to the `_type` field. The configuration class should inherit from NeMo Agent toolkit's `EvaluatorBaseConfig` class. - -**Evaluator Core Logic:** The main logic of the evaluator should be implemented in an async method. This method should take in an `EvalInput` (which contains a list of `EvalInputItem` objects), and output an `EvalOutput` (which contains a list of `EvalOutputItem` objects). Read more about these data structures [here](https://docs.nvidia.com/nemo/agent-toolkit/1.2/extend/custom-evaluator.html#understanding-evalinputitem-and-evaloutputitem). - -**Evaluator Registration:** Finally, like all functions used in the NeMo Agent toolkit, the custom evaluator must be registered within the toolkit. An async registration function should be decorated with the `register_evaluator` decorator, which takes in the evaluator configuration class defined earlier. To ensure that the evaluator is registered at runtime, import the evaluator function in the `register.py` file. The following command will list all registered evaluators. - -``` -nat info components -t evaluator +nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 ``` -Below is a skeleton example showing the structure of a custom evaluator: - -```python -from nat.builder.builder import EvalBuilder -from nat.builder.evaluator import EvaluatorInfo -from nat.cli.register_workflow import register_evaluator -from nat.data_models.evaluator import EvaluatorBaseConfig -from nat.eval.evaluator.evaluator_model import EvalInput, EvalOutput, EvalOutputItem -from pydantic import Field - -# Step 1: Define the evaluator configuration -class MyCustomEvaluatorConfig(EvaluatorBaseConfig, name="my_custom_evaluator"): - my_custom_parameter: float = Field(default=0.5, description="A custom parameter") - -# Step 2: Register the evaluator with the register_evaluator decorator -@register_evaluator(config_type=MyCustomEvaluatorConfig) -async def my_custom_evaluator(config: MyCustomEvaluatorConfig, builder: EvalBuilder): - # Instantiate the evaluator class with config parameters - evaluator = MyCustomEvaluator( - max_concurrency=builder.get_max_concurrency(), - my_custom_parameter=config.my_custom_parameter - ) - # Yield EvaluatorInfo with the evaluate function - yield EvaluatorInfo( - config=config, - evaluate_fn=evaluator.evaluate, - description="My Custom Evaluator" - ) - -# Step 3: Implement the evaluator core logic -class MyCustomEvaluator: - - def __init__(self, max_concurrency: int, my_custom_parameter: float): - self.max_concurrency = max_concurrency - self.my_custom_parameter = my_custom_parameter - - async def evaluate(self, eval_input: EvalInput) -> EvalOutput: - eval_output_items = [] - - # Handle each item in EvalInput - for item in eval_input.eval_input_items: - # Your custom evaluation logic here - eval_output_items.append( - EvalOutputItem(id=item.id, score=score, reasoning=reasoning) - ) - - # Calculate aggregate score - avg_score = 0.0 # Your aggregation logic here - - # return an EvalOutput - return EvalOutput(average_score=avg_score, eval_output_items=eval_output_items) -``` +This creates a persistent service listening on port `26466`. The same environment variables and `.env` file used by `nat run` apply (`NVIDIA_API_KEY`, `TAVILY_API_KEY`, etc.). Browse the auto-generated OpenAPI docs at and check liveness at . -Importing the custom evaluator in `register.py` to ensure it's registered at runtime can be done like so: +#### Endpoints -```python -# In register.py -from vuln_analysis.eval.evaluators.my_custom_evaluator import my_custom_evaluator -``` +With the shipped config, `nat serve` (NeMo Agent Toolkit 1.7) exposes: -For developers looking to build custom evaluators, we strongly recommend reviewing the full NeMo Agent toolkit [evaluation documentation](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/evaluate.html), as well as the NeMo Agent toolkit documentation for writing [custom evaluators](https://docs.nvidia.com/nemo/agent-toolkit/1.2/extend/custom-evaluator.html). Developers can also reference the source code of the `accuracy.py` and `consistency.py` evaluators inside the `eval/evaluators` directory. +| Method & path | Purpose | +|---------------|---------| +| `POST /generate` | Synchronous run — blocks until analysis completes, returns the full result | +| `POST /v1/workflow/async` | Submit a non-blocking background job; returns a `job_id` immediately | +| `GET /v1/workflow/async/job/{job_id}` | Poll the status/result of an async job | +| `GET /docs` | Swagger UI / OpenAPI docs | +| `GET /health` | Liveness/readiness probe | +These are the endpoints this workflow supports and is tested against. -## Troubleshooting +The request body for the workflow endpoints is a `PipelineInput` JSON document — identical to the `--input_file` used with `nat run` (see [`PipelineInput` fields](#full-pipeline)). -Several common issues can arise when running the workflow. Here are some common issues and their solutions. +Inspect the NAT server's generated API docs for the active endpoint details, and see the [NAT API server](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/api-server-endpoints.html) reference for additional endpoint behavior. -### Git LFS issues +#### Synchronous request (`/generate`) -If you encounter issues with Git LFS, ensure that you have Git LFS installed and that it is enabled for the repository. You can check if Git LFS is enabled by running the following command: -```bash -git lfs install -``` +Post a JSON file: -Verifying that all files are being tracked by Git LFS can be done by running the following command: ```bash -git lfs ls-files +curl -X POST http://localhost:26466/generate \ + -H 'Content-Type: application/json' \ + --data @data/examples/pipeline_input.json ``` -Files which are missing will show a `-` next to their name. To ensure all LFS files have been pulled correctly, you can run the following command: -```bash -git lfs fetch --all -git lfs checkout * -``` - -### Container build issues - -When building containers for self-hosted NIMs, certain issues may occur. Below are common troubleshooting steps to help resolve them. -#### Device error - -If you encounter an error resembling the following during the [container build process for self-hosted NIMs](#using-self-hosted-nims): +…or an inline body: ```bash -nvidia-container-cli: device error: {n}: unknown device: unknown +curl -X POST http://localhost:26466/generate \ + -H 'Content-Type: application/json' \ + -d '{ + "vulns": [{"vuln_id": "CVE-2025-47273"}], + "image_name": "nvcr.io/nvidia/morpheus/morpheus", + "image_tag": "25.06-runtime", + "output_dir": "outputs" + }' ``` -This error typically indicates that the container is attempting to access GPUs that are either unavailable or non-existent on the host. To resolve this, verify the GPU count specified in the [docker-compose.nim.yml](./docker-compose.nim.yml) configuration file: -- Navigate to the `deploy.resources.reservations.devices` section and check the count parameter. -- Set the environment variable `NIM_LLM_GPU_COUNT` to the actual number of GPUs available on the host machine before building the container. Note that the default value is set to 4. +The server blocks until the pipeline finishes (each stage has a 600-second default timeout — see [Configuration](#configuration)) and returns a `PipelineResult` JSON document. Top-level fields include `image_name`, `image_tag`, `success`, `elapsed_seconds`, and a `results` array with one entry per CVE — each carrying `vuln_id`, `vex_status`, `vex_justification`, and per-stage report metadata. The same Markdown/JSON reports are also written under `output_dir` exactly as with `nat run` (see [Output Structure](#output-structure)). -This adjustment ensures the container accurately matches the available GPU resources, preventing access errors during deployment. +#### Asynchronous jobs (`/v1/workflow/async`) -#### Deploy.Resources.Reservations.devices error +Vulnerability analysis can run for minutes — especially [multi-CVE requests](#multi-cve-requests). For non-blocking submission, post to the async endpoint, which returns a `job_id` right away: -If you encounter an error resembling the following during the [container build process for self-hosted NIMs](#using-self-hosted-nims) process: +```bash +curl -X POST http://localhost:26466/v1/workflow/async \ + -H 'Content-Type: application/json' \ + --data @data/examples/pipeline_input.json ``` -1 error(s) decoding: -* error decoding 'Deploy.Resources.Reservations.devices[0]': invalid string value for 'count' (the only value allowed is 'all') +```json +{"job_id": "f79951ee-aeea-4a2d-b103-a0a8c15e314b", "status": "submitted"} ``` -This is likely caused by an [outdated Docker Compose version](https://github.com/docker/compose/issues/11097). Please upgrade Docker Compose to at least `v2.21.0`. - -### NGINX caching server +Then poll for status and the final result: -Because the workflow makes such heavy use of the caching server to speed up API requests, it is important to ensure that the server is running correctly. If you encounter issues with the caching server, you can reset the cache. - -#### Resetting the entire cache - -To reset the entire cache, you can run the following command: ```bash -docker compose down -v +curl http://localhost:26466/v1/workflow/async/job/f79951ee-aeea-4a2d-b103-a0a8c15e314b ``` -This will delete all the volumes associated with the containers, including the cache. -#### Resetting just the LLM cache or the services cache +Async jobs are backed by a local Dask cluster (provided by the `nvidia-nat[async_endpoints]` extra, which this project already pins). You can tune the runtime when starting the server: -If you want to reset just the LLM cache or the services cache, you can run the following commands: ```bash -docker compose down - -# To remove the LLM cache -docker volume rm ${COMPOSE_PROJECT_NAME:-vuln_analysis}_llm-cache - -# To remove the services cache -docker volume rm ${COMPOSE_PROJECT_NAME:-vuln_analysis}_service-cache +# Threaded workers give cleaner interleaved logging; cap concurrent jobs to 1 +nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 \ + --dask_workers=threads --max_running_async_jobs=1 ``` -#### Vector databases -We've integrated VDB and embedding creation directly into the workflow with caching included for expediency. However, in a production environment, it's better to use a separately managed VDB service. +Useful related flags: `--workers` (FastAPI worker processes), `--scheduler_address` (attach to an existing Dask scheduler), and `--db_url` (persist async job metadata instead of the default temporary SQLite database). -NVIDIA offers optimized models and tools like NIMs ([build.nvidia.com/explore/retrieval](https://build.nvidia.com/explore/retrieval)) and cuVS ([github.com/rapidsai/cuvs](https://github.com/rapidsai/cuvs)). +### Multi-CVE Requests -### Service errors +To analyze multiple CVEs against the same container in a single request, pass multiple entries in `vulns`. The Container Analyzer runs once and all CVE-specific stages (CVE Researcher, Exploitability Analyzer, VEX Categorizer) fan out in parallel per CVE: -#### National Vulnerability Database (NVD) -These typically resolve on their own. Please wait and try running the workflow again later. Example errors: - -404 -``` -Error requesting [1/10]: (Retry 0.1 sec) https://services.nvd.nist.gov/rest/json/cves/2.0: 404, message='', url=URL('https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-6709') -``` - -503 -``` -Error requesting [1/10]: (Retry 0.1 sec) https://services.nvd.nist.gov/rest/json/cves/2.0: 503, message='Service Unavailable', url=URL('https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-50447') +```json +{ + "vulns": [ + {"vuln_id": "CVE-2025-47273"}, + {"vuln_id": "CVE-2024-12345", "package_hint": "openssl", "version_hint": "3.0.1"}, + {"vuln_id": "GHSA-xxxx-yyyy-zzzz"} + ], + "image_name": "nvcr.io/nvidia/morpheus/morpheus", + "image_tag": "25.06-runtime", + "filesystem_path": "/path/to/filesystem", + "output_dir": "outputs" +} ``` -#### NVIDIA API Catalog / NVIDIA-hosted NIMs - -##### 429 Errors - Too Many Requests +Each entry in `vulns` has a `vuln_id` (CVE or GHSA identifier — auto-classified by prefix) plus optional `package_hint` and `version_hint`. The response includes a `results` list with per-CVE outcomes (`vex_status`, `vex_justification`, per-stage reports). For a single CVE, pass a one-element list. -`Exception: [429] Too Many Requests` can occur when using NVIDIA-hosted model endpoints, if your requests exceed the rate limit. +When analyzing many CVEs at once, all per-CVE agent tasks run in parallel by default, which can cause API rate-limiting or stage timeouts. Two workflow settings in `config.yml` throttle this — `max_concurrency` (caps task parallelism per stage) and `llm_max_rate` (caps the global LLM request rate). Both default to unlimited; see [Concurrency & Rate Limiting](#concurrency--rate-limiting). -**Quick Fix:** +### Programmatic API -To resolve 429 errors, try reducing the rate of LLM requests. This involves adjusting `llm_max_rate` (controls request rate within a workflow) and, for certain operations, `max_concurrency` (controls how many workflows run in parallel). +Build a `PipelineInput` and pass it to NAT as JSON (e.g. via subprocess or HTTP). Example: -Start by reducing `llm_max_rate` from the default of 5, for example: +```python +from vuln_analysis.data_models.input import CVEEntry, PipelineInput + +# Automatic extraction: orchestrator pulls and caches the rootfs via crane. +inp = PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2025-47273")], + image_name="nvcr.io/nvidia/morpheus/morpheus", + image_tag="25.06-runtime", + output_dir="outputs", +) + +# Multiple CVEs against the same container, also using automatic extraction +inp = PipelineInput( + vulns=[ + CVEEntry(vuln_id="CVE-2025-47273"), + CVEEntry(vuln_id="CVE-2024-12345", package_hint="openssl"), + ], + image_name="nvcr.io/nvidia/morpheus/morpheus", + image_tag="25.06-runtime", + output_dir="outputs", +) + +# With a pre-extracted rootfs: +# inp = PipelineInput( +# vulns=[CVEEntry(vuln_id="CVE-2025-47273")], +# image_name="nvcr.io/nvidia/morpheus/morpheus", +# image_tag="25.06-runtime", +# filesystem_path="/path/to/filesystem", +# output_dir="outputs", +# ) +print(inp.model_dump_json()) +``` + +Use `nat run --config_file=configs/config.yml --input_file=...` with that JSON, or invoke the registered workflow from your own NAT entrypoint. + +## Output Structure + +``` +outputs/ +├── vulnerability_reports/ # CVE Researcher Markdown reports +│ └── CVE-2025-47273.md +├── container_reports/ # Container Analyzer Markdown reports +│ └── morpheus_25.06-runtime.md +├── exploitability_reports/ # Exploitability Analyzer Markdown reports +│ └── CVE-2025-47273_morpheus_25.06-runtime.md +└── vex_results/ # VEX Categorizer outputs (Markdown + JSON) + ├── CVE-2025-47273_morpheus_25.06-runtime.md + └── CVE-2025-47273_morpheus_25.06-runtime.json +``` + +The final VEX JSON output contains the structured classification: -```yaml -workflow: - llm_max_rate: 3 # Reduce from default of 5 to 3 requests per second +```json +{ + "cve_id": "CVE-2025-47273", + "container": "morpheus_25.06-runtime", + "status": "not_affected", + "justification": "code_not_reachable", + "reasoning": "The vulnerable package_index.py module was completely removed in setuptools 80.9.0, which is installed in the active environment...", + "exploitability_verdict": "NOT APPLICABLE", + "confidence": "High", + "risk_level": "Informational", + "container_type": "SDK/Development", + "report_path": "outputs/vex_results/CVE-2025-47273_morpheus_25.06-runtime.md", + "exploitability_report_path": "outputs/exploitability_reports/CVE-2025-47273_morpheus_25.06-runtime.md" +} ``` -Where to make this change: +## Benchmarking -- **Standard workflows** ([config.yml](src/vuln_analysis/configs/config.yml)): Reduce `workflow.llm_max_rate` -- **Test Time Compute** ([config-ttc.yml](src/vuln_analysis/configs/config-ttc.yml)): Reduce `cve_agent_workflow.llm_max_rate`, and set `workflow.max_concurrency: 1` to limit parallelization -- **Evaluation** ([config-eval.yml](src/vuln_analysis/configs/config-eval.yml)): Reduce `workflow.llm_max_rate`, and set `eval.general.max_concurrency: 1` to limit parallelization +Labeled datasets live under **`data/eval_datasets/`**. Evaluations run through **`nat eval`** and [`configs/config-eval.yml`](./src/vuln_analysis/configs/config-eval.yml). ---- +### Included Dataset: `morpheus_benchmark_2506.csv` -**Understanding Rate Limiting:** +29 hand-labeled CVE × container cases covering two NVIDIA Morpheus 25.06 images: -For more control and optimization beyond the quick fix above, you can fine-tune rate limiting at multiple levels: +| Image | Cases | +|-------|-------| +| `nvcr.io/nvidia/morpheus/morpheus:25.06-runtime` | 24 | +| `nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06` | 5 | -**1. Control request rate within a single workflow execution:** -- Set a global `llm_max_rate` in workflows or functions with `_type: cve_agent` (e.g., `workflow.llm_max_rate: 5` in [config.yml](src/vuln_analysis/configs/config.yml) or `cve_agent_workflow.llm_max_rate: 5` in [config-ttc.yml](src/vuln_analysis/configs/config-ttc.yml)) to limit the rate (requests per second) of LLM requests -- Set individual function `llm_max_rate` values (e.g., `cve_justify.llm_max_rate: 10`) to override the global setting for functions that make LLM calls: `cve_checklist`, `cve_agent_executor`, `cve_summarize`, `cve_justify`. This is useful when functions use model endpoints with varying rate limits. +**Ground-truth label distribution:** -See [Configuration file reference](#configuration-file-reference) for more details on `llm_max_rate` configuration. +| Label | Count | +|-------|-------| +| `code_not_reachable` | 15 | +| `vulnerable` | 13 | +| `code_not_present` | 1 | -**2. Control parallelism across multiple workflow executions:** +The dataset includes a mix of CVE and GHSA identifiers. Each case has been manually assessed to produce the ground-truth VEX justification label. -Certain operations parallelize multiple workflow executions simultaneously, which can quickly exceed rate limits: -- **Evaluation** (`nat eval`): Parallelizes evaluation input items and evaluation repetitions. Set `eval.general.max_concurrency` to a low value such as 1 to reduce parallelization. -- **Test Time Compute**: Parallelizes runs across multiple TTC repetitions. Set `workflow.max_concurrency` in workflows with `_type: execute_then_select_function` (e.g., in [config-ttc.yml](src/vuln_analysis/configs/config-ttc.yml)) to a low value such as 1 to reduce parallelization. - -> [!NOTE] -> Reducing `llm_max_rate` and `max_concurrency` will result in lower throughput when processing large workloads (e.g., many CVEs, large evaluation test sets, or many repetitions). For production use cases requiring higher throughput, we recommend switching to [self-hosting](#using-self-hosted-nims). - -#### Authentication errors - -Authentication errors will occur if required API key(s) are invalid or have not been set as environment variables as described in [Set up the environment file](#set-up-the-environment-file). For example, the following error will occur if `NVIDIA_API_KEY` is not properly set: +### Running Benchmarks ```bash -Error: [401] Unauthorized -Authentication failed +nat eval --config_file=configs/config-eval.yml ``` -Note that exporting the required environment variables in a container shell will not persist outside of that shell. Instead, we recommend shutting down the containers (`docker compose down`), [setting the required environment variables](#set-up-the-environment-file), and then [starting the containers](#start-the-docker-containers) again. +Use `--override` to point at a specific CSV: -#### Controlling NAT error log verbosity - -When errors occur in NAT functions (such as LLM calls or agent execution), the error logs include the input data that was being processed. For large inputs, this can result in extremely long error messages. You can control the verbosity of these error logs using the `NAT_ERROR_LOG_MAX_LENGTH` environment variable. - -**Setting the environment variable:** - -Update your `.env` file with the desired value, for example: -```bash -# Truncate error logs to 1000 characters (default is 500) -NAT_ERROR_LOG_MAX_LENGTH=1000 -``` -To disable NAT log truncation, set the value to 0. This can be helpful for debugging. ```bash -# Disable truncation (show full error messages) -NAT_ERROR_LOG_MAX_LENGTH=0 +nat eval --config_file=configs/config-eval.yml \ + --override eval.general.dataset.file_path data/eval_datasets/morpheus_benchmark_2506.csv ``` -Then, shut down the containers (`docker compose down`), [export the variables in your `.env` file](#set-up-the-environment-file), and [start the containers](#start-the-docker-containers) again. +**Benchmark CSV format:** -### macOS Workarounds +| Column | Description | +|--------|-------------| +| `vuln_id` | CVE or GHSA identifier | +| `image_name` | Container image repository/name without the tag | +| `image_tag` | Container image tag | +| `filesystem_path` | Optional path to extracted filesystem | +| `image_config_path` | Optional path to image config JSON | +| `ground_truth_label` | Expected VEX justification label | -Testing and development of the workflow may be possible on macOS, however macOS is not officially supported or tested for this blueprint. Platform differences may require extra troubleshooting or impact performance. (See [Operating system requirements](#operating-system-requirements) for more info.) +NAT evaluators write metrics under the directory configured in **`eval.general.output_dir`** in `config-eval.yml` (default **`./.tmp/evaluators`**). Accuracy semantics (exact justification match vs. binary vulnerable/not-vulnerable classification) follow the evaluator definitions in that file. -For users wishing to test on macOS, please see below for a list of known issues and workarounds. Additional user-reported issues and workarounds can be found by searching [open issues for macOS](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/issues?q=is%3Aissue%20macOS). +## Configuration -#### Embedding API Connection Errors +### Environment Variables -**Error:** +| Variable | Required | Description | +|----------|----------|-------------| +| `NVIDIA_API_KEY` | Yes | NVIDIA API key for NAT `nim` LLM inference | +| `NV_BASE_URL` | No | NVIDIA API endpoint (config / Compose may route via a local cache proxy; direct default is `https://inference-api.nvidia.com/v1`) | +| `TAVILY_API_KEY` | Yes | Tavily API key for web search | +| `CVE_RESEARCHER_MODEL` | No | Model override for CVE Researcher | +| `CONTAINER_ANALYZER_MODEL` | No | Model override for Container Analyzer | +| `EXPLOITABILITY_ANALYZER_MODEL` | No | Model override for Exploitability Analyzer | +| `VEX_CATEGORIZER_MODEL` | No | Model override for VEX Categorizer | +| `DEFAULT_MODEL_NAME` | No | Fallback model when per-agent overrides are unset | +| `CACHE_ENABLED` | No | When input JSON omits `cache`, `false` disables on-disk caching | +| `VULN_ANALYSIS_IMAGE_CACHE` | No | Override the rootfs cache root used when `filesystem_path` is omitted (default: `.cache/vuln_analysis/images`) | -`ConnectionError` / `RemoteDisconnected` error that occurs during vector database (VDB) generation. -``` -requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) -``` +### Concurrency & Rate Limiting -**Cause:** +When analyzing many CVEs against one container, the per-CVE agent tasks (CVE Researcher, Exploitability Analyzer, VEX Categorizer) fan out in parallel. Two independent **workflow-level** knobs in `config.yml` control how hard the pipeline drives the LLM endpoint: -This error can occur during VDB generation when the embedding batch size causes the request to exceed the NVIDIA-hosted API's timeout limits. On macOS, the default `max_batch_size` of 128 can result in a payload too large to process in a single HTTP request within the timeout. +| Setting | Controls | Mechanism | Disable with | +|---------|----------|-----------|--------------| +| `workflow.max_concurrency` | Max agent tasks running simultaneously per stage | `asyncio.Semaphore` | `null` or `0` (unlimited) | +| `workflow.llm_max_rate` | Max LLM requests per second across all agent functions | token-bucket `AsyncLimiter` | `null` (no limit) | -**Solution:** +- **`max_concurrency`** bounds task-level parallelism. Set a positive integer (e.g. `3`) to limit concurrent agents per stage; `null` or `0` means unlimited. +- **`llm_max_rate`** bounds the request rate. It is enforced as a strict, **no-burst** limit: `llm_max_rate: 5` spaces calls exactly 0.2s apart (`AsyncLimiter(max_rate=1, time_period=0.2)`) rather than allowing a burst of 5 and then waiting. `null` disables it. -Try reducing the embedding batch size in your configuration file: - -```yaml -embedders: - nim_embedder: - max_batch_size: 32 # Reduced from default of 128 -``` +Both default to unlimited in the shipped configs. The upstream blueprint set `llm_max_rate: 5` to stay under its LLM provider's rate limit — set a number here **only if your inference endpoint enforces one**. -This change should be made in all configuration files you plan to use (e.g., `config.yml`, `config-eval.yml`, `config-ttc.yml`). +**Per-function override:** each agent function (`cve_researcher`, `container_analyzer`, `exploitability_analyzer`, `vex_categorizer`) accepts its own `llm_max_rate`, which **overrides** the workflow-level value. Leaving a function's value unset (or `null`) **inherits** the workflow setting rather than disabling it — so to effectively remove the limit for a single function while others stay capped, set it to a high number. The limiter is resolved at run time and shared across all concurrent invocations of a function, so the cap is global, not per-task. +### Key Defaults -## Testing and validation -Test-driven development is essential for building reliable LLM-based agentic systems, especially when deploying or scaling them in production environments. +| Setting | Value | Source | +|---------|-------|--------| +| Default LLM model | `aws/anthropic/claude-opus-4-5` | [`src/vuln_analysis/configs/config.yml`](./src/vuln_analysis/configs/config.yml) | +| Context window | 262,144 tokens | Agent blocks in `config.yml` | +| Stage timeout | 600 seconds | `workflow.stage_timeout` in `config.yml` | +| Max concurrency | `null` (unlimited) | `workflow.max_concurrency` in `config.yml` | +| LLM rate limit | `null` (no limit) | `workflow.llm_max_rate` in `config.yml` | +| Tavily search depth | `advanced` | `functions.web_search` in `config.yml` | +| Search results per query | 5 | `functions.web_search` | +| Web fetch timeout | 30 seconds | `functions.web_fetch` | +| Max content length | 50,000 characters | `functions.web_fetch` | -In our development process, we use the Morpheus public container as a case study. We perform security scans and collaborate with developers and security analysts to assess the exploitability of identified CVEs. Each CVE is labeled as either vulnerable or not vulnerable. For non-vulnerable CVEs, we provide a justification based on one of the ten VEX statuses. Team members document their investigative steps and findings to validate and compare results at different stages of the system. +Per-agent settings (temperature, max tokens, max iterations, tool lists) are defined in the same YAML files (`config.yml` and `config-*.yml`). -We have collected labels for 38 CVEs, which serve several purposes: -- Human-generated checklists, findings, and summaries are used as ground truth during various stages of prompt engineering to refine LLM output. -- The justification status for each CVE is used as a label to measure end-to-end workflow accuracy. Every time there is a change to the system, such as adding a new agent tool, modifying a prompt, or introducing an engineering optimization, we run the labeled dataset through the updated workflow to detect performance regressions. +## Caching -As a next step, we plan to integrate this process into our CI/CD pipeline to automate testing. While LLMs' non-deterministic nature makes it difficult to assert exact results for each test case, we can adopt a statistical approach, where we run the workflow multiple times and ensure that the average accuracy stays within an acceptable range. +By default, the pipeline caches agent outputs on disk. If a report file already exists for a given CVE × container combination, that agent is skipped on subsequent runs. This allows resuming interrupted benchmark runs or re-running the pipeline without repeating expensive analyses. -We recommend that teams looking to test or optimize their CVE analysis system curate a similar dataset for testing and validation. Note that in test-driven development, it's important that the model has not achieved perfect accuracy on the test set, as this may indicate overfitting or that the set lacks sufficient complexity to expose areas for improvement. The test set should be representative of the problem space, covering both scenarios where the model performs well and where further refinement is needed. Investing in a robust dataset ensures long-term reliability and drives continued performance improvements. +- **Disable via input**: set `"cache": false` on `PipelineInput` +- **Disable via environment**: set `CACHE_ENABLED=false` when the JSON omits `cache` +- **Per-agent overrides**: `cache_cve_researcher`, `cache_container_analyzer`, `cache_exploitability_analyzer`, `cache_vex_categorizer` -## Roadmap +Cached reports are standard Markdown files in the output directory — they can be inspected, edited, or deleted manually. -- [x] Configurable option for skipping the `VulnerableDependencyChecker` -- [x] Configurable retries and timeouts for intel fetching -- [x] Upgrade to [NAT v1.2](https://docs.nvidia.com/nemo/agent-toolkit/1.2/index.html) -- [x] Configurable NIM error handling -- [x] Configurable missing source info and VDB handling -- [x] [NAT Evaluation](https://docs.nvidia.com/nemo/agent-toolkit/latest/workflows/evaluate.html) integration -- [ ] [NAT W&B Weave](https://docs.nvidia.com/nemo/agent-toolkit/1.2/workflows/observe/observe-workflow-with-weave.html) integration -- [x] [NAT Test Time Compute](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/test-time-compute.html) integration -- [x] Upgrade to NAT v1.3 +## Troubleshooting -## Cite -Please consider citing [our paper](https://ceur-ws.org/Vol-3920/paper09.pdf) when using this code in a project. You can use the citation BibTeX: -``` -@inproceedings{zemicheal2024llm, - title={LLM agents for vulnerability identification and verification of CVEs}, - author={ZeMicheal, Tadesse and Chen, Hsin and Davis, Shawn and Allen, Rachel and Demoret, Michael and Song, Ashley}, - booktitle={Proceedings of the Conference on Applied Machine Learning in Information Security (CAMLIS 2024)}, - pages={161--173}, - year={2024}, - publisher={CEUR Workshop Proceedings}, - volume={3920}, - url={https://ceur-ws.org/Vol-3920/} -} +- **System Python is older than 3.13**: use `uv venv --python 3.13 .venv`. `uv` can find or download a compatible Python even when `python3 --version` reports an older interpreter. +- **`crane` is missing**: install it with `brew install crane` on macOS, or `go install github.com/google/go-containerregistry/cmd/crane@latest` on Linux/Go-based setups and ensure `$HOME/go/bin` is on `PATH`. +- **API keys are missing**: export `NVIDIA_API_KEY` and `TAVILY_API_KEY` before running agent commands. Missing keys may trigger repeated retries or long waits from the LLM/search clients. +- **Example inputs fail with missing paths**: edit the sample JSON files to point at a real extracted filesystem or run earlier stages first so downstream reports exist. +- **`nat run` exits `0` but reports failure**: inspect the returned JSON `success` fields. A shell-successful command can still produce a workflow result with `"success": false`. + +## Design Principles + +- **Security isolation** — Agents with both web and filesystem access are split into two phases, each with access to only one domain. This prevents cross-contamination between network and filesystem execution contexts. +- **Deterministic pre-triage** — The Exploitability Analyzer uses a deterministic pre-triage phase that programmatically verifies package presence on the filesystem, compares versions, and checks distro backports before invoking the LLM. Early exit outcomes (`NOT APPLICABLE`, `ENVIRONMENT ISOLATED`) are generated from templates with zero LLM calls. +- **Filesystem as ground truth** — All package verification uses the container filesystem directly, including Go binary scanning via `go version -m` for Go modules. No external SBOM or inventory file is needed. +- **Parallel execution** — Per-CVE agent tasks fan out via `asyncio.gather` at every stage (CVE Researcher, Exploitability Analyzer, VEX Categorizer). Within each agent, multiple tool calls in a single LLM turn are also dispatched in parallel. The `max_concurrency` workflow setting limits how many agent tasks run simultaneously per stage, and `llm_max_rate` caps the global LLM request rate (both `null`/unlimited by default; see [Concurrency & Rate Limiting](#concurrency--rate-limiting)). +- **Fail-safe defaults** — If any pipeline stage fails or times out, the system defaults to `unknown` status and `uncertain` justification to reflect that the analysis could not be completed. +- **Robustness** — Loop detection prevents agents from repeating the same tool calls. An exponential-backoff retry mechanism handles transient API errors. Context window compaction manages long conversations. +- **Auditability** — The VEX Categorizer's `reason()` tool records a step-by-step decision trace through a decision flowchart, making each classification decision traceable and auditable. +- **Agents return content, not files** — Agents return report content as text, which the orchestrator writes to disk, eliminating file-writing failure modes within agents. + +## Project Structure + +``` +vulnerability-analysis/ +├── src/vuln_analysis/ # Main Python package (NAT entry: register.py) +│ ├── configs/ # config.yml, config-*.yml, config-eval.yml +│ ├── data_models/ # PipelineInput, PipelineResult, agent I/O models +│ ├── functions/ # NAT functions per agent +│ ├── tools/ # Web, filesystem, OSV, binary, package tools +│ ├── utils/ # Pre-triage, reports, parsers, HTTP +│ ├── eval/ # Eval dataset parser, evaluators +│ └── data/ +│ ├── examples/ # Sample JSON inputs +│ └── eval_datasets/ # Benchmark CSVs +├── scripts/ +│ └── extract_image.py # Standalone crane-based rootfs extractor +├── deploy/ # Docker Compose deployment +│ ├── docker-compose.yml # Default (NVIDIA-hosted NIMs) +│ ├── docker-compose.nim.yml # Overlay for self-hosted NIMs +│ ├── Dockerfile # vuln-analysis image (build context = repo root) +│ ├── nginx/ # Caching/SSL proxy config +│ ├── 1_Deploy_CVE.ipynb # Deploy walkthrough +│ ├── 2_Customize_CVE.ipynb # Customization walkthrough +│ └── README.md +├── images/ # README and documentation images +├── .env.example # Local environment variable template +├── pyproject.toml # nvidia-nat, dependencies, Python 3.13 +└── README.md ``` -## License - -By using this software or microservice, you are agreeing to the [terms and conditions](https://www.nvidia.com/en-us/data-center/products/nvidia-ai-enterprise/eula/) of the license and acceptable use policy. - - -## Terms of Use - -GOVERNING TERMS: The NIM container is governed by the [NVIDIA Software License Agreement](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-software-license-agreement/) and [Product-Specific Terms for AI Products](https://www.nvidia.com/en-us/agreements/enterprise-software/product-specific-terms-for-ai-products/); and use of this model is governed by the [NVIDIA AI Foundation Models Community License Agreement](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-ai-foundation-models-community-license-agreement/). - -ADDITIONAL Terms: [Meta Llama 3.1 Community License](https://www.llama.com/llama3_1/license/), Built with Meta Llama 3.1. diff --git a/aiq b/aiq new file mode 160000 index 000000000..444996351 --- /dev/null +++ b/aiq @@ -0,0 +1 @@ +Subproject commit 444996351eca33cf1fb8b2fe70d0a446300510a4 diff --git a/deploy/1_Deploy_CVE.ipynb b/deploy/1_Deploy_CVE.ipynb index a552ce05e..5e95e064f 100644 --- a/deploy/1_Deploy_CVE.ipynb +++ b/deploy/1_Deploy_CVE.ipynb @@ -1,1072 +1,481 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "9e60cb36-f153-4991-a31f-702f11144446", - "metadata": {}, - "source": [ - "# Try out the NVIDIA AI Blueprint for Vulnerability Analysis!" - ] - }, - { - "cell_type": "markdown", - "id": "8a0f2acb-52d9-49dd-9676-2d58715786f5", - "metadata": {}, - "source": [ - "This notebook will deploy the NVIDIA AI Blueprint for vulnerability analysis using NVIDIA NIM microservices and [NVIDIA NeMo Agent Toolkit](https://github.com/NVIDIA/NeMo-Agent-Toolkit) which demonstrates accelerated analysis on common vulnerabilities and exposures (CVEs) at an enterprise scale, reducing mitigation from days and hours to just seconds. You will install the necessary prerequisites, spin up a NVIDIA NeMo Agent Toolkit workflow and connect to NVIDIA hosted NIM APIs.\n", - "\n", - "Once deployed, you will have a fully functional workflow which determines whether a software package includes exploitable and vulnerable components, be able to run the blueprint on an example input including viewing the results from an output file with all the gathered and generated information for a final review by the security analyst.\n", - "\n", - "\n", - "**Note**: this notebook is designed to run as a [brev.dev launchable](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-2olGFXbhH0qEtU47MJ4tpxCAZIn)." - ] - }, - { - "cell_type": "markdown", - "id": "eb8c7758-746f-4e00-8a28-cbdc463a7925", - "metadata": {}, - "source": [ - "# Getting Started\n", - ">[Prerequisites](#Prerequisites) \n", - ">[Spin Up Blueprint](#Spin-Up-Blueprint) \n", - ">[Running the Workflow](#Running-the-Workflow) \n", - ">[Validate Deployment](#Validate-Deployment) \n", - ">[API Reference](#API-Reference) \n", - ">[Next Steps](#Next-Steps) \n", - ">[Appendix](#Appendix) \n", - "________________________\n" - ] - }, - { - "cell_type": "markdown", - "id": "0f6fcc47-fb41-4e54-9d30-4d17bc483779", - "metadata": {}, - "source": [ - "## Prerequisites" - ] - }, - { - "cell_type": "markdown", - "id": "97ac0e45-27c2-414a-9216-efbaae3dab0d", - "metadata": {}, - "source": [ - "- NVIDIA AI Enterprise developer license\n", - "- API keys for vulnerability databases, search engines, and LLM model service(s).\n", - " - Details can be found in this later section: [Obtain API keys](#obtain-api-keys)" - ] - }, - { - "cell_type": "markdown", - "id": "20ccb058-fded-4570-a9df-949145e6c319", - "metadata": {}, - "source": [ - "### (OPTIONAL) Hardware Requirements for Self-Hosting NIMs\n", - "\n", - "This workflow uses model API calls. You have a choice to self-host the models, or use NVIDIA-hosted models. Note that if using NVIDIA-hosted models (as is default in this notebook), then **this workflow does not require GPUs to run**. If you choose to self-host, the models and hardware requirements are as follows. \n", - "\n", - "- LLM NIM: [Meta Llama 3.1 70B Instruct Support Matrix](https://docs.nvidia.com/nim/large-language-models/latest/support-matrix.html#llama-3-1-70b-instruct).\n", - " This workflow makes heavy use of parallel LLM calls to accelerate processing. For improved parallel performance (for example, in production workloads), we recommend 8x or more H100s for LLM inference.\n", - "- Embedding NIM: [NV-EmbedQA-E5-v5 Support Matrix](https://docs.nvidia.com/nim/nemo-retriever/text-embedding/latest/support-matrix.html#nv-embedqa-e5-v5)." - ] - }, - { - "cell_type": "markdown", - "id": "d09678c9-8fbe-41d7-84ad-ce624bec582c", - "metadata": {}, - "source": [ - "### Clone repository and Modify Deployment Files" - ] - }, - { - "cell_type": "markdown", - "id": "9f0ded68-437d-4ad2-be82-b36ff6100b30", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, - "source": [ - "1. **Clone** workflow repository" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "448a97da-daf6-4069-a6a4-3e4713a445b9", - "metadata": {}, - "outputs": [], - "source": [ - "!git clone https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis.git" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5cb50a04-95f6-4abd-bf34-341ab3b641ee", - "metadata": {}, - "outputs": [], - "source": [ - "%cd vulnerability-analysis" - ] - }, - { - "cell_type": "markdown", - "id": "1ffec8ef-e4a4-46a1-9bec-ae9a1e41556e", - "metadata": {}, - "source": [ - "2. **Set** up the workflow repository" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aeeec243-cc02-4b52-99f7-c34938fa6a59", - "metadata": {}, - "outputs": [], - "source": [ - "#set an environment variable for the path to the repository root.\n", - "import os\n", - "os.environ['REPO_ROOT'] = os.getcwd()" - ] - }, - { - "cell_type": "markdown", - "id": "4b11fd65-b17e-470c-ab9f-d6c5bd09d0b6", - "metadata": {}, - "source": [ - "
\n", - " Tip: All commands are run from the repository root unless otherwise specified.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "5d26d7bf-0044-45c4-bace-18b63c41dd04", - "metadata": {}, - "source": [ - "### Obtain API Keys\n", - "To run the workflow, you need to obtain API keys for the following APIs. These will be needed in a later step to Set up the environment file.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "1689c5c2-d414-43e5-b600-580a520218b5", - "metadata": {}, - "source": [ - "- **Vulnerability databases and search engines**: These APIs are required by the workflow to retrieve vulnerability information from databases and online search.\n", - " \n", - " - [GitHub Security Advisory (GHSA) Database](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)\n", - " - Follow [these](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) instructions to create a personal access token. No repository access or permissions are required for this API. This will be used in the `GHSA_API_KEY` environment variable.\n", - " - [National Vulnerability Database (NVD)](https://nvd.nist.gov/developers/request-an-api-key) \n", - " - Follow [these](https://nvd.nist.gov/developers/request-an-api-key) instructions to create an API key. This will be used in the `NVD_API_KEY` environment variable.\n", - " - [SerpApi](https://serpapi.com/)\n", - " - Go to (https://serpapi.com/) and create a SerpApi account. Once signed in, navigate to Your Account > Api Key. This will be used in the `SERPAPI_API_KEY` environment variable.\n" - ] - }, - { - "cell_type": "markdown", - "id": "c50df6ff-8b35-48e0-9f6d-3e2449633911", - "metadata": {}, - "source": [ - "### NVIDIA NIM Microservices \n", - "The workflow requires LLM API calls for model inference. By default, the workflow uses NVIDIA NIM APIs, which provide optimized inference for each model." - ] - }, - { - "cell_type": "markdown", - "id": "d220ce41-6210-4b9e-b0cb-e39c8e6750a6", - "metadata": {}, - "source": [ - "There are two possible methods to generate an API key for NIM:\n", - "1. Through [the NVIDIA Build](https://build.nvidia.com/explore/discover?signin=true) portal with your email\n", - "2. Through [NVIDIA NGC portal](https://ngc.nvidia.com/) with your email\n", - "\n", - "In this notebook, the API Key generation via [the NVIDIA Build](https://build.nvidia.com/explore/discover?signin=true) is explained.\n", - "In addition to LLM API access, the NVIDIA API Key is also required to log in to the NVIDIA container registry, nvcr.io, and to pull secure base container images used in this NIM Blueprint. Perform the following steps if you do not already have an API key. You can use different model API endpoints with the same API key. This will be used in the `NVIDIA_API_KEY` environment variable." - ] - }, - { - "cell_type": "markdown", - "id": "dd47fa8f-7b46-4188-aa77-73d57438058c", - "metadata": {}, - "source": [ - "1. Navigate to **[NVIDIA API Catalog](https://build.nvidia.com/explore/discover)**." - ] - }, - { - "cell_type": "markdown", - "id": "1b707189-4848-4ebc-ab4e-d55d7084bff5", - "metadata": {}, - "source": [ - "2. Select a model, such as [meta/llama-3.1-70b-instruct](https://build.nvidia.com/meta/llama-3_1-70b-instruct).\n", - " " - ] - }, - { - "cell_type": "markdown", - "id": "4217edc7-4689-4c91-8f32-59fe32d4240b", - "metadata": {}, - "source": [ - "3. Select an **Input** option. The following example is of a model that offers a Docker option. Not all of the models offer this option, but all include a “Get API Key” link" - ] - }, - { - "cell_type": "markdown", - "id": "9fff83e0-cfee-4c77-910b-317f087df453", - "metadata": {}, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "id": "1931c98c-4419-44e9-85d1-edb98c40d655", - "metadata": {}, - "source": [ - "3. Click **Get API Key**." - ] - }, - { - "cell_type": "markdown", - "id": "ed353f53-1998-413c-9a64-1bcaad83913c", - "metadata": {}, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "id": "5764837f-217c-43db-b48b-30ce34b6daf4", - "metadata": {}, - "source": [ - "4. Select **\"Generate Key\"**" - ] - }, - { - "cell_type": "markdown", - "id": "67e5065b-0472-4947-a4b1-be13cace84d1", - "metadata": {}, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "id": "646b06b0-c691-4c0e-b288-19d3fa7ca109", - "metadata": {}, - "source": [ - "5. **Copy your key** and store it in a secure place. Do not share it." - ] - }, - { - "cell_type": "markdown", - "id": "9c8e6fdc-e035-44b9-87c8-3564f00b01ad", - "metadata": {}, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "id": "d6d68974-f114-4763-badb-9a158582f2e3", - "metadata": {}, - "source": [ - "
\n", - " Tip: The key begins with the letters nvapi-.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "5aa7f3c4-d8c3-4b96-bb43-5c15c4c4918b", - "metadata": {}, - "source": [ - "## Spin Up Blueprint\n", - "In this section, we'll export the necessary API keys into the environment, build the blueprint Docker container, and start the containers required to run the workflow." - ] - }, - { - "cell_type": "markdown", - "id": "5e654cfa-771e-4f1c-9249-03fcb3a677d1", - "metadata": {}, - "source": [ - "Set up the **environment file**\n", - "\n", - "Either configure your API keys as environment variables, or replace the environment variables in the cell below with the exact key value." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9c942c85-f72f-4d49-9b41-d41399dc64ce", - "metadata": {}, - "outputs": [], - "source": [ - "%%bash\n", - "cd $REPO_ROOT\n", - "cat < .env\n", - "GHSA_API_KEY=$GHSA_API_KEY\n", - "NVD_API_KEY=$NVD_API_KEY\n", - "NVIDIA_API_KEY=$NVIDIA_API_KEY\n", - "SERPAPI_API_KEY=$SERPAPI_API_KEY\n", - "EOF" - ] - }, - { - "cell_type": "markdown", - "id": "3d6a8a62-fc0c-4c26-87d4-f2b3b1aaba95", - "metadata": {}, - "source": [ - "Authenticate Docker with **NGC**\n", - "\n", - "In order to pull images required by the workflow from NGC, you must first authenticate Docker with NGC. You can use same the NVIDIA API Key obtained in the [Obtain API Keys](#obtain-api-keys) section (saved as `NVIDIA_API_KEY` in the `.env` file)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6dcc09bd-8b92-4366-a14c-5bf82bad8ae5", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "%%bash\n", - "source .env && echo $NVIDIA_API_KEY | docker login nvcr.io -u '$oauthtoken' --password-stdin" - ] - }, - { - "cell_type": "markdown", - "id": "943ac521-1a53-4496-975f-b491bc0b9272", - "metadata": {}, - "source": [ - "Use oauthtoken as the username and your API key as the password. The $oauthtoken username is a special name that indicates that you will authenticate with an API key and not a user name and password" - ] - }, - { - "cell_type": "markdown", - "id": "eb8a1bf8-582a-44fb-add2-b6259d49cbdd", - "metadata": {}, - "source": [ - "Build the **docker container**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "974d7744-84c6-4f61-97f1-0c9f9b400b8d", - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [], - "source": [ - "%%bash\n", - "cd $REPO_ROOT\n", - "\n", - "# Build the vuln-analysis container\n", - "docker compose build vuln-analysis" - ] - }, - { - "cell_type": "markdown", - "id": "cfd0ca5f-24c0-41a1-ab34-a4976239f1d9", - "metadata": {}, - "source": [ - "
\n", - " Tip: Refer to the deploy/compose/docker-compose.yml for complete details.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "4096f2e9-96ea-47ea-9fb3-03db5b9515b7", - "metadata": {}, - "source": [ - "### Starting the Containers" - ] - }, - { - "cell_type": "markdown", - "id": "f8463b37-e1b3-4386-a42f-67ff0644269f", - "metadata": {}, - "source": [ - "To start the containers, there are two supported configurations. Both configurations utilize `docker compose` to start the service:\n", - " - When using NVIDIA-hosted NIMs, only the `docker-compose.yml` configuration file is required. This is the default configuration and is recommended for most users getting started with the workflow.\n", - " - When using self-hosted NIMs, both the `docker-compose.yml` and `docker-compose.nim.yml` configuration files are required." - ] - }, - { - "attachments": { - "5edbbe07-82f6-4511-a136-c069ff05039f.png": { - "image/png": "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zq5QoUKTJk2UTWCMESs+w4YNU73XfplhYWHu7u7h4eGFXoWDe63Q/UeBzkAAetUZetnAbUSktuHJsoVIvXPnTisCNVmxYoWDB1XPnj3d3d1PnTpV6PUiUhc6UhTICThIr4aEhFBK169fD+7OSYBHQUJI586dlQRs1quVK1fu06ePskDLOQ7+e2qzXt23bx8hZNq0aarN6fTvzzPPPEMI8fHxkWbPnz9f9V49Zjq41/SICD4XOQGpXk1JSSlyf+AACOSLACK1DXrVQqQ+depUp39/ihUrRghp27atNHvPnj356iYtGyNSa7l37O2bEc6zgV619yjRePk8CtatW5cQlUeJtunV+Ph4QoiB9eqkSZMIIVaehdimTRtCiA2zzbKRk5qampubK8vUwldEQS30AnywTGDy5MmPlgP73L5927IxroKA1gggUtugV/MVqUuUKEEISUpKKmDXP3jwoIAl2Ol2RGo7gdVFsVu2bKGUmptlKWATML9aQIC43SoCPApGRkY+8cQTzz77bHJysvQ2Vb1669at4ODg2rVrly5dukyZMl5eXhMnThRvcvbp00e6zMbLy0sUuH///u7duz/77LMuLi6VKlXq3bv3iRMnxFW+krZ8+fKZmZmTJ0/29PQsVarU888/P3jw4Bs3bkjNsrOz58+f37hx4/Lly5cqVcrd3f2dd945d+6c1IYxtmXLlg4dOjz99NMuLi7PPfdc586dt2/fLrVRzq+eO3fumWeeKV269O7du6WWsrSfn1/FihVzcnJk+apfzenV+Pj4oUOHuru7lypVqkKFCq1bt5Ytc5gyZQohZOXKlYsWLapcuXKxYsXu37+/d+9eQsiQIUPi4+P9/f0rVqxYpkyZRo0a/f7774yxbdu2NWvWrFy5cs8//3yPHj2uXLkiXJK9v8q9SkxMXL16daNGjcqWLVumTJkmTZps27ZN3MITq1atat26NaXUxcXl2Wef7d69+5EjR6Q2iIJSGkhrk8DHH3+sa706f/58f3//P//8U5t44ZW9CdgQqaOjowcMGFC5cmUXF5ennnqqSZMm3333nQhbFiL1kiVLmjVr9tRTT7m6unp6egYGBspCMCHEeJHanF59bKTmPySuXr06evToihUr+vj4MMZsjtSy91cRqe39L8tJyo+MjIRedZK+NmwzeRQ8e/bsf//7X0JI//79pU1V6tWb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ubm5kyZNsvn9ampqqo+Pz23XXv369fvpp5+Y4N955x2ut++//575e1OnTp3+8Ic//Pvf/87OzuYaoK1OAs4WZnvvTIhDAAAgAElEQVR6VZLCvKFezcjIGDJkyMMPP9ylS5c77rjDy8vr+++/54KdN28eQRBbtmxJTEzs37//rbfeetddd7355ptMwYaGhj755JNdu3Z9+OGHP/vsM5tn0dt3XldXN2LEiGHDhrW1tXFH3LJlC0EQb775JvegI+3c3Nxbbrmlf//+rMMJEyYQBLFmzRpu9/Ly8q5du951113Nzc3M8V808+23397c3Dx79uxHHnmkc+fOPXr0GDVqFL/AuX7kaGdmZvr6+kZERMjhHM8HloMqfIIACIAACICAxgi4//5qWFjYwYMHO3To8Nhjj9lcLDqoV/38/AICAgiC2Llzpw3ud95556abbmLUlP3fr/7zn/8kCCIgIOChhx5asmTJDz/8EBQU1KlTpy5dupw9e5am6by8PIIgSJJkLyWZsaKiogiC8PLyshma+7ampua5555jvsUXEBDg7+/PfKlv4MCBVquVsczMzCQIYtCgQY899ti0adO2bdvm7+/fsWPHLl26bNy48b777lu8ePHWrVv/85///DLTZ555hvXPXNB/8skn3bp1++CDD6ZNm/bSSy8x169nzpxhzUaPHk0QxH333TdhwoSgoKCXX36ZIIiHHnqIK30ZoXL77bdPmjRp/vz5gwcPvvvuu319fbl6NSsrq2vXrh06dBgyZMiCBQvGjBlz2223MVfVw4YNY4dj5P299947ZsyYgIAABm/Xrl0hWVlEqm2wetXBwmxPr0pSmPb1anR0dOfOnbt06TJkyJDp06d/8MEHXbt25d6fpGl68eLFBEGMGzfugQceWLJkyZYtW9566y2CIJ5++unZs2c///zzYWFhGzZsYCr0448/ZvPiiHPWmNtYsGABQRBTp07lHnSk7ePjc/PNN3M3Af3LX/5CEAS3SBk/3t7eBEGkpKQwbwmCuPvuu999990HHnhg/PjxU6dOfeyxxwiCeOqppxobGx0ZWhM2MupVmqax/6omFgGCBAEQAAEQAAGKokwmk9lsdgMK5rKY+eYbI3gCAgK44zqoVwcPHnzmzBmCIP75z39yu1+9erVz585vvPEGc9a+Xn3jjTcIgujRowf3QSmTJ08mCCI4OJhx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fVOpVMKm3C5dusyZMycwMLBFixa9evUCr6vmzZvPnTv35cuX2Df524d8lUejVDL74si7IYkyaOfn58Oc9ddff/3nP/9pY2NjaWnZoUOHhQsXsl6nqRKQ+Jt9OgU1eGsAPyyNcXCoqFGjhq2tLYyRz58/NzU1lclk7PMYlKiFryqVSojuwCK74JxY093E46VBQO1L3MjIiGd927dv5xNL7NixQ7pxZd68edWrV//555+hMtLAD2rz3evCV5VKZatWrVTfkvg8kFBtQRCgOFjb0WX/amZmpi59UztfdXR0JISwXO3YN0vzBEqv5fnqrFmzpAp4pLgIZGZmhoWFff755xYWFlZWVi1btpw4cSLzUABrKvfgadOmtW3btlatWubm5q1atfLz80tNTdW9rNatWxNCPvnkE+klpTR+/PhxR0fH1q1b16pVy8rKqlWrVmPHjuXfUazEly9fBgQEtGjRwszMzNLSsn379nPmzOEXr3TkkI8ePerSpYuZmZmtrS28WC5cuNCrVy8bGxtra+vOnTvHxsaqAr3cunWrbdu2pqam3bt3ZyGp2JKOjmVRSk+ePDlo0KD69eubmJg0aNCgd+/efPQKSinyVXaL9VkoAV9VKpWLFi368ssva9asWb169Q8++KBr166//vorP489cuRIixYtTExMGjdunJiYqImvUkpjY2O/+OILMzMza2vrLl26HDhw4NSpU4QQuVzO47Zv375evXrVqlXLxMRE1WGnTZuWlZXFFETFseNqhT///DMoKKhDhw7169c3MzNr3rx5v379NmzYwDw12FXl1zdTUlIEQbCzszM3N2/ZsuX06dNzc3Ozs7P79etnbm7euHHj5ORktVMd7XvLoeaVsm8iX2WPZWkFXTJop6WlwTdXY2Pjzp07jxkzpkePHrCvrF+/fnxXl9Zm0aJFvDsfuNfXrl3bzMwM9ouWxjildMmSJSL7w4cPJ4RAOmZWH018NS8vD/avNmjQgAWOwjkxww2FMkRA7Utce4J7+Gzp6urKV6OwsJDvdFK+CvMtkbOuLnz19OnTsELCF5eWlubp6cnmhbrzVR37pha+unv3bhMTEyMjI+YEaBB904Dyrz558oStr0ZERPD3HWVEABFABBABRKDSIxAfHy+TyaZPn14eLS1HvpqRkREZGblp06byqLce2tQlg3ZKSsrAgQOHDRvG7/q4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stoP7i9fvpRqymQyURJUqMbZs2dlMtkHH3zQp0+fkSNHNm/eHK5V++E4NjZWrWW1b6fp06cz5Xbt2v3nP/8ZOXLk2LFjfX19165dy2JEQTXGjBnDlJkwevRohhUvfPnll0yHCWpzpebl5TEFXvj11195gyDfuHGD12Eyi33IX7Jz506mwAtqU83Pnz+f1wH5008/5Q0y2c3NTao8YMCA+fPnq8JHi0b5b7/9VqqsidGxm8tfonaGDzsZeTWQ1fL8I0eOSDVlMpnaTx7Lli2TKjdq1Ig1nxf8/f2lyt988w2vw+TBgwdLlTV9Zf7888+lynPmzGHWmPDq1SuppkwmU7v2cO7cOaZsePtXWZtRQAQQAUQAEUAEEAFEABFABBABRAARQASKi0A5xlsqblVQHxFABBABRAARQAQQAUQAEUAEEAFEABFgCCBfZVCggAggAogAIoAIIAKIACKACCACiAAioEcIIF/Vo5uBVUEEEAFEABFABBABRAAL0unFAAAAwUlEQVQRQAQQAUQAEWAIIF9lUKCACCACiAAigAggAogAIoAIIAKIACKgRwggX9Wjm4FVQQQQAUQAEUAEEAFEABFABBABRAARYAggX2VQoIAIIAKIACKACCACiAAigAggAogAIqBHCCBf1aObgVVBBBABRAARQAQQAUQAEUAEEAFEABFgCCBfZVCggAggAogAIoAIIAKIACKACCACiAAioEcIIF/Vo5uBVUEEEAFEABFABBABRAARQAQQAUQAEWAI/H+EoQFHn/TfpAAAAABJRU5ErkJggg==" + "cells": [ + { + "cell_type": "markdown", + "id": "59917f00", + "metadata": {}, + "source": [ + "# Deploy the Container Vulnerability Analysis Pipeline\n" + ] + }, + { + "cell_type": "markdown", + "id": "bd997cdf", + "metadata": {}, + "source": [ + "This notebook deploys the current NVIDIA vulnerability-analysis workflow described in the README. The workflow is registered with NeMo Agent Toolkit (NAT) as the `cve_pipeline` graph and can be run as a one-shot job with `nat run` or hosted as an HTTP service with `nat serve`.\n", + "\n", + "The pipeline accepts one or more CVE/GHSA identifiers plus `image_name` and `image_tag`. If `filesystem_path` is omitted, the orchestrator pulls and extracts the container rootfs with `crane`, caches it, and writes per-agent Markdown reports plus final VEX JSON under `output_dir`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "cab6f172", + "metadata": {}, + "source": [ + "# Getting Started\n", + "\n", + ">[Prerequisites](#Prerequisites) \n", + ">[Repository Setup](#Repository-Setup) \n", + ">[Configure Environment](#Configure-Environment) \n", + ">[Run Locally](#Run-Locally) \n", + ">[Deploy with Docker Compose](#Deploy-with-Docker-Compose) \n", + ">[Run the HTTP Workflow](#Run-the-HTTP-Workflow) \n", + ">[Inspect Outputs](#Inspect-Outputs) \n", + ">[API Reference](#API-Reference) \n", + ">[Next Steps](#Next-Steps) \n", + ">[Troubleshooting](#Troubleshooting)\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d62e49a", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- Python 3.13+\n", + "- `uv` for local environment management\n", + "- Docker and Docker Compose for the containerized path\n", + "- `crane` for local automatic image extraction; the project Dockerfile already includes it\n", + "- `NVIDIA_API_KEY` for NVIDIA OpenAI-compatible NIM inference\n", + "- `TAVILY_API_KEY` for web search\n", + "\n", + "The current workflow does not require separate GHSA, NVD, SerpApi, SBOM, or source repository inputs for normal pipeline runs.\n" + ] + }, + { + "cell_type": "markdown", + "id": "281a45a5", + "metadata": {}, + "source": [ + "## Repository Setup\n", + "\n", + "If you launched this notebook outside the repository, clone the project first. If the notebook is already inside `vulnerability-analysis/deploy`, the setup cell below will find the repository root automatically.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32416e55", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional, only if you do not already have the repository locally:\n", + "# !git clone https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis.git\n", + "# %cd vulnerability-analysis\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff29582e", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import json\n", + "import os\n", + "import time\n", + "\n", + "\n", + "def find_repo_root(start: Path) -> Path:\n", + " for candidate in [start, *start.parents]:\n", + " if (candidate / \"pyproject.toml\").exists() and (candidate / \"src\" / \"vuln_analysis\").exists():\n", + " return candidate\n", + " raise RuntimeError(\"Run this notebook from inside the vulnerability-analysis repository.\")\n", + "\n", + "\n", + "REPO_ROOT = find_repo_root(Path.cwd())\n", + "os.chdir(REPO_ROOT)\n", + "os.environ[\"REPO_ROOT\"] = str(REPO_ROOT)\n", + "print(f\"Repository root: {REPO_ROOT}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "0dca7fa1", + "metadata": {}, + "source": [ + "## Configure Environment\n", + "\n", + "Create a `.env` file in the repository root or export these variables before running the workflow. `NV_BASE_URL` is optional; leave it unset to use the config default for local runs.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4355172e", + "metadata": {}, + "outputs": [], + "source": [ + "# env_path = REPO_ROOT / \".env\"\n", + "env_path = Path(\".tmp/.env\")\n", + "if not env_path.exists():\n", + " env_path.write_text(\n", + " \"NVIDIA_API_KEY=key\\n\"\n", + " \"NV_BASE_URL=https://inference-api.nvidia.com/v1\\n\"\n", + " \"TAVILY_API_KEY=key\\n\"\n", + " )\n", + " print(f\"Created {env_path}. Edit it with your API keys before running the workflow.\")\n", + "else:\n", + " print(f\"Using existing {env_path}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af2ee035", + "metadata": {}, + "outputs": [], + "source": [ + "def load_dotenv_values(path: Path) -> dict[str, str]:\n", + " values = {}\n", + " if not path.exists():\n", + " return values\n", + " for raw_line in path.read_text().splitlines():\n", + " line = raw_line.strip()\n", + " if not line or line.startswith(\"#\") or \"=\" not in line:\n", + " continue\n", + " key, value = line.split(\"=\", 1)\n", + " values[key.strip()] = value.strip().strip('\"').strip(\"'\")\n", + " return values\n", + "\n", + "\n", + "file_env = load_dotenv_values(env_path)\n", + "for key, value in file_env.items():\n", + " os.environ.setdefault(key, value)\n", + "\n", + "required = [\"NVIDIA_API_KEY\", \"TAVILY_API_KEY\"]\n", + "missing = [key for key in required if not os.environ.get(key)]\n", + "if missing:\n", + " print(\"Missing required environment variables:\", \", \".join(missing))\n", + " print(\"Fill them in .env, then re-run this cell.\")\n", + "else:\n", + " print(\"Required API keys are present.\")\n", + "print(\"NV_BASE_URL:\", os.environ.get(\"NV_BASE_URL\", \"config/compose default\"))\n" + ] + }, + { + "cell_type": "markdown", + "id": "366223a6", + "metadata": {}, + "source": [ + "## Run Locally\n", + "\n", + "For local execution, install the project with `uv`, then run the NAT workflow against the shipped sample input. The example analyzes two CVEs against `nvcr.io/nvidia/morpheus/morpheus:25.06-runtime` and uses automatic rootfs extraction.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bbd3637", + "metadata": {}, + "outputs": [], + "source": [ + "!uv sync\n" + ] + }, + { + "cell_type": "markdown", + "id": "ccc7e374", + "metadata": {}, + "source": [ + "Inspect the sample `PipelineInput` document. This is the same JSON shape accepted by `nat run`, `/generate`, and the async workflow endpoint.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c9cf3c0", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m json.tool data/examples/pipeline_input.json\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a63d6042", + "metadata": {}, + "outputs": [], + "source": [ + "# Enable telemetry to help NVIDIA improve the product. You can disable it at any time with `nat configure telemetry --disable`.\n", + "!nat configure telemetry --enable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41fec3ba", + "metadata": {}, + "outputs": [], + "source": [ + "# This can take several minutes on the first run because the image rootfs is pulled and cached.\n", + "!nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json\n" + ] + }, + { + "cell_type": "markdown", + "id": "8e810f7e", + "metadata": {}, + "source": [ + "## Deploy with Docker Compose\n", + "\n", + "Docker Compose builds the `vuln-analysis` image, starts the Jupyter container, and starts the nginx cache service used by the default container environment. The Dockerfile includes `uv`, `crane`, NAT, and the project package.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1032b372", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "set -euo pipefail\n", + "\n", + "COMPOSE=(docker compose -f deploy/docker-compose.yml --env-file ./.env)\n", + "\n", + "\"${COMPOSE[@]}\" build vuln-analysis\n", + "\"${COMPOSE[@]}\" up -d vuln-analysis nginx-cache\n", + "\"${COMPOSE[@]}\" ps\n" + ] + }, + { + "cell_type": "markdown", + "id": "4eb2aec3", + "metadata": {}, + "source": [ + "## Run the HTTP Workflow\n", + "\n", + "Start `nat serve` inside the `vuln-analysis` container. The server listens on port `26466`, exposes Swagger docs at `/docs`, and accepts the same `PipelineInput` JSON used by `nat run`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f9bd109", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "set -euo pipefail\n", + "\n", + "COMPOSE=(docker compose -f deploy/docker-compose.yml --env-file ./.env)\n", + "\n", + "if \"${COMPOSE[@]}\" exec -T vuln-analysis pgrep -af '[n]at serve' >/dev/null; then\n", + " echo \"nat server is running\"\n", + "else\n", + " \"${COMPOSE[@]}\" exec -d vuln-analysis sh -lc 'exec nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 > /var/tmp/nat-serve.log 2>&1'\n", + " echo \"started nat serve\"\n", + "fi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82bcbfc5", + "metadata": {}, + "outputs": [], + "source": [ + "!curl -sS http://localhost:26466/health\n" + ] + }, + { + "cell_type": "markdown", + "id": "0aa599ed", + "metadata": {}, + "source": [ + "Submit a synchronous request to `/generate`. The request blocks until the pipeline completes, then returns a `PipelineResult` JSON document. Reports are also written to the configured `output_dir`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "499cc137", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "set -euo pipefail\n", + "mkdir -p .tmp\n", + "\n", + "curl -sS -X POST http://localhost:26466/generate -H 'Content-Type: application/json' --data @data/examples/pipeline_input.json -o .tmp/deploy_generate_response.json\n", + "\n", + "python -m json.tool .tmp/deploy_generate_response.json | sed -n '1,160p'\n" + ] + }, + { + "cell_type": "markdown", + "id": "91320515", + "metadata": {}, + "source": [ + "For longer multi-CVE requests, use the async endpoints. Submit once, copy the returned `job_id`, then poll the job URL.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15bee027", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "set -euo pipefail\n", + "mkdir -p .tmp\n", + "\n", + "curl -sS -X POST http://localhost:26466/v1/workflow/async -H 'Content-Type: application/json' --data @data/examples/pipeline_input.json -o .tmp/deploy_async_submit.json\n", + "\n", + "python -m json.tool .tmp/deploy_async_submit.json\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "018f3801", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "submit_path = Path(\".tmp/deploy_async_submit.json\")\n", + "if submit_path.exists():\n", + " submitted = json.loads(submit_path.read_text())\n", + " job_id = submitted.get(\"job_id\")\n", + " if job_id:\n", + " print(f\"Poll with: curl http://localhost:26466/v1/workflow/async/job/{job_id}\")\n", + " else:\n", + " print(\"No job_id returned; inspect .tmp/deploy_async_submit.json\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "bb9fdc62", + "metadata": {}, + "source": [ + "## Inspect Outputs\n", + "\n", + "The orchestrator writes agent reports and final VEX JSON under `output_dir`, which defaults to `outputs` unless overridden by the input JSON.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dc3995f", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "output_root = Path(\"outputs\")\n", + "for subdir in [\"vulnerability_reports\", \"container_reports\", \"exploitability_reports\", \"vex_results\"]:\n", + " path = output_root / subdir\n", + " print(f\"{path}\")\n", + " if path.exists():\n", + " for item in sorted(path.iterdir())[:10]:\n", + " print(\" -\", item)\n", + " else:\n", + " print(\" - not created yet\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "67a0af6c", + "metadata": {}, + "source": [ + "A final VEX JSON record includes the CVE/GHSA identifier, container, VEX status and justification, exploitability verdict, confidence, risk level, and paths to the generated reports.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f33a8e71", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import json\n", + "\n", + "for result_path in sorted(Path(\"outputs/vex_results\").glob(\"*.json\"))[:3]:\n", + " print(f\"{result_path}\")\n", + " print(json.dumps(json.loads(result_path.read_text()), indent=2)[:4000])\n" + ] + }, + { + "cell_type": "markdown", + "id": "8e4db193", + "metadata": {}, + "source": [ + "## API Reference\n", + "\n", + "With the shipped NAT 1.7 configuration, `nat serve` exposes:\n", + "\n", + "| Method and path | Purpose |\n", + "|---|---|\n", + "| `POST /generate` | Synchronous run; returns the full result |\n", + "| `POST /v1/workflow/async` | Submit a background job and receive a `job_id` |\n", + "| `GET /v1/workflow/async/job/{job_id}` | Poll async job status/result |\n", + "| `GET /docs` | Swagger UI / OpenAPI docs |\n", + "| `GET /health` | Liveness/readiness check |\n" + ] + }, + { + "cell_type": "markdown", + "id": "6e8298f6", + "metadata": {}, + "source": [ + "## Next Steps\n", + "\n", + "Open `deploy/2_Customize_CVE.ipynb` to create custom `PipelineInput` files, analyze multiple CVEs, use pre-extracted rootfs paths, change model settings, and tune concurrency/rate limiting.\n" + ] + }, + { + "cell_type": "markdown", + "id": "dc0dee57", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- If `nat run` cannot pull an image, confirm `crane` is installed locally and registry authentication is available.\n", + "- If Docker Compose starts but `/health` fails, inspect the server log with `docker compose -f deploy/docker-compose.yml --env-file ./.env exec vuln-analysis tail -n 100 /var/tmp/nat-serve.log`.\n", + "- If API calls fail, re-check `NVIDIA_API_KEY`, `TAVILY_API_KEY`, and `NV_BASE_URL`.\n", + "- If a run is too aggressive for your inference endpoint, set `workflow.max_concurrency` or `workflow.llm_max_rate` in a custom config as shown in the customization notebook.\n", + "- If you want a clean rerun, delete the relevant Markdown/JSON files under `outputs` or set `\"cache\": false` in the input JSON.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.13.2)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" } - }, - "cell_type": "markdown", - "id": "1d4f6e05-a60b-411e-9ecd-a52b18bd4862", - "metadata": {}, - "source": [ - "![image.png](attachment:5edbbe07-82f6-4511-a136-c069ff05039f.png)" - ] - }, - { - "cell_type": "markdown", - "id": "28612b11-d586-4bd5-8e0a-e12089ad9295", - "metadata": {}, - "source": [ - "
\n", - " Tip: The container binds to port 8080 by default. If you encounter a port collision error (e.g. Bind for 0.0.0.0:8080 failed: port is already allocated), you can set the environment variable NGINX_HOST_HTTP_PORT to specify a custom port before launching docker compose. For example:\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b5f3468f-539e-4457-baba-ed1bd6daad07", - "metadata": {}, - "outputs": [], - "source": [ - "#!export NGINX_HOST_HTTP_PORT=8081\n", - "\n", - "#... docker compose commands..." - ] - }, - { - "cell_type": "markdown", - "id": "1908d02f-905a-45fa-809c-6ae4e2410429", - "metadata": {}, - "source": [ - "#### For this Launchable: Use **NVIDIA-hosted NIMs** and run the workflow locally via this instance! " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7dbecfba", - "metadata": {}, - "outputs": [], - "source": [ - "!docker compose -f ./docker-compose.yml down\n", - "!docker compose -f ./docker-compose.yml up -d" - ] - }, - { - "cell_type": "markdown", - "id": "a85f4dbb-99c9-4d49-bab0-509b6dfa5cfd", - "metadata": {}, - "source": [ - "#### OPTIONAL SECTION : Pull **Self-hosted NIMs**\n", - "\n", - "**Note**: Only run this section if you are using notebook to deploy self-hosted NIMs, e.g. for production purposes. To self-host this entire blueprint, you will need a 4XA100 configuration at minimum. (This configuration can be launched using [brev.dev](https://www.brev.dev/)). Refer to the [Hardware Requirements](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis?tab=readme-ov-file#hardware-requirements) section for more information." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ae11c91-6b3d-4d61-849f-cde8b5e5ee66", - "metadata": {}, - "outputs": [], - "source": [ - "#docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/docker-compose.nim.yml down\n", - "#docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/docker-compose.nim.yml pull nim-llm nim-embed" - ] - }, - { - "cell_type": "markdown", - "id": "eee4439e-d66d-45c6-a2e7-db2bff835556", - "metadata": {}, - "source": [ - "
\n", - " Note: The self-hosted NIM services require additional GPU resources to run. With this configuration, the LLM NIM, embedding model NIM, and the vuln-analysis service will all be launched on the same machine. Ensure that you have the necessary hardware requirements for all three services before proceeding (multiple services can share the same GPU).\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "4de24814-dc5c-4415-ba33-570ad717153c", - "metadata": {}, - "source": [ - "For example, to start the `vuln-analysis` service with the self-hosted NIMs, you would run:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78c65a8a-9b6b-43ff-ba1d-46d7b8bea5f7", - "metadata": {}, - "outputs": [], - "source": [ - "#cd ${REPO_ROOT}\n", - "#docker compose -f docker-compose.yml -f docker-compose.nim.yml up -d" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f179c0a3-d1b0-4f7a-bbb3-417cc6a1003f", - "metadata": {}, - "outputs": [], - "source": [ - "#docker compose -f deploy/compose/docker-compose.yml exec vuln-analysis pip install click\n", - "\n", - "#docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/docker-compose.nim.yml exec vuln-analysis pip install click" - ] - }, - { - "cell_type": "markdown", - "id": "79c45ac3-af80-42bd-8d13-080163617f60", - "metadata": {}, - "source": [ - "Deploy **microservices**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ea67b59-4575-4289-ac12-c7c375155bf0", - "metadata": {}, - "outputs": [], - "source": [ - "#docker compose -f deploy/compose/docker-compose.yaml up -d --build" - ] - }, - { - "cell_type": "markdown", - "id": "1e88f196-e021-4f91-ba04-7f823f348a34", - "metadata": {}, - "source": [ - "
\n", - " Tip: If you would like to monitor progress, refer to https://docs.docker.com/reference/cli/docker/compose/logs/.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "976a7d5e-4b53-493e-aabd-13f7447a2ac0", - "metadata": {}, - "source": [ - "
\n", - " Note: By default, the blueprint uses the NVIDIA API Catalog hosted endpoints for LLM,and embedding models.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "430c6e5d-0804-45be-97cc-2c7b45bbb903", - "metadata": {}, - "source": [ - "To validate the deployment of the blueprint, execute the following command to ensure the container are running." - ] - }, - { - "cell_type": "markdown", - "id": "b746a9f6-1216-4e12-92cb-fdba420a3df3", - "metadata": {}, - "source": [ - "If the deployment was successful, you should see four running containers for the Vulnerability Analysis workflow, LLM NIM, Embed NIM, and the request caching server when using the self-hosted NIM option. With the NVIDIA hosted NIM option, we see only two containers by running the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6faad06b-a42b-45bc-a231-dba02999ae14", - "metadata": {}, - "outputs": [], - "source": [ - "!docker compose ps" - ] - }, - { - "cell_type": "markdown", - "id": "e9d11266-4230-4630-a38f-bb1e278c946b", - "metadata": {}, - "source": [ - "```bash\n", - "NAME COMMAND SERVICE STATUS PORTS\n", - "vuln_analysis-vuln-analysis-1 \"/opt/conda/envs/mor…\" vuln-analysis running 0.0.0.0:8888->8888/tcp, 0.0.0.0:26466->26466/tcp, :::8000->8000/tcp, :::26466->26466/tcp\n", - "vuln_analysis-nginx-cache-1 \"/docker-entrypoint.…\" nginx-cache running 0.0.0.0:8080->80/tcp, :::8080->80/tcp\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "1f69c0c8-a48f-4e47-9486-1b8ff434a822", - "metadata": {}, - "source": [ - "## Running the Workflow\n", - "\n", - "Now that the containers are up and running, the next step is to get the Vulnerability Analysis workflow running in the container, and listening for HTTP requests. " - ] - }, - { - "cell_type": "markdown", - "id": "59fa2fbc-0737-4fb2-a2e1-79018da1806d", - "metadata": {}, - "source": [ - "Execute the following cell to attach to the `vuln-analysis` container and start the HTTP server using the `nat serve` CLI command. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4070ecdd", - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "# Copy the config file inside the container\n", - "os.system(f\"docker exec vuln_analysis-vuln-analysis-1 sh -c 'cp configs/config.yml .tmp/config.yml'\")\n", - "\n", - "# Run the nat serve command inside the container\n", - "os.system(f\"docker exec -d vuln_analysis-vuln-analysis-1 sh -c 'nat serve --config_file=.tmp/config.yml --host 0.0.0.0 --port 26466 > /var/tmp/test.log 2>&1 &'\")\n", - "\n", - "# Sleep to ensure the server is up and running\n", - "time.sleep(30)" - ] - }, - { - "cell_type": "markdown", - "id": "5925ad34-ba95-4e5c-9e64-b00d6d91645d", - "metadata": {}, - "source": [ - "
\n", - " Note: This command starts an HTTP server that listens on port 26466 and runs the workflow indefinitely, waiting for incoming data to process. This is useful if you want to trigger the workflow on demand via HTTP requests.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "4fa39112-efc4-457f-bbf4-dac5aaa722b3", - "metadata": {}, - "source": [ - "Open a new terminal and tail the log file that was created in the previous step to monitor the HTTP server initialization process:\n", - "\n", - "```\n", - "docker exec vuln_analysis-vuln-analysis-1 sh -c 'tail /var/tmp/test.log -f'\n", - "```\n", - "\n", - "\n", - "You should see the workflow start up like so:\n", - "```bash\n", - "INFO: Application startup complete.\n", - "INFO: Uvicorn running on http://0.0.0.0:26466 (Press CTRL+C to quit) \n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "404c702e-a8e3-4a25-9afa-b0f43e093d7b", - "metadata": {}, - "source": [ - "We'll issue our first vulnerability analysis request to the workflow, which, by default will run the triage on the Morpheus 23.11 container release. Let's first explore what a request to the workflow looks like:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "daf9b243-a343-47e7-aa99-4943047fc36f", - "metadata": {}, - "outputs": [], - "source": [ - "cat data/input_messages/morpheus:23.11-runtime.json" - ] - }, - { - "cell_type": "markdown", - "id": "24fec46e-a500-4267-868e-2e25faca8844", - "metadata": {}, - "source": [ - "Above, we see that we are indeed asking for a vulnerability triage on the Morpheus 23.11.01-runtime container, giving the Agent context from all of the container's source code, documentation, build files, and even python notebooks. Furthermore, we include a Software Bill of Materials (SBOM) for package version identification, and a list of vulnerabilities to scan for. Let's issue a request with the above configuration. Be sure to keep an eye on the terminal to watch the agent reason and look at the output.\n" - ] - }, - { - "cell_type": "markdown", - "id": "d6634103-ed26-40ab-9bfb-90a3bdf70fe6", - "metadata": {}, - "source": [ - "
\n", - " Note: Your first analysis request for any new container or CVE could take several minutes as the service builds and creates a request cache for you. It is also possible to encounter 404 or 503 errors when issuing the request. Please refer to the Troubleshooting section at the end of this notebook for additional guidance. If you notice a 502 from the nginx service when issuing the request, please wait a few minutes and try again.\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "772aadfa-26b3-4c83-a651-09025b7f1dc3", - "metadata": {}, - "outputs": [], - "source": [ - "! curl -v -X POST http://0.0.0.0:26466/generate --header 'Content-Type: application/json' -d @data/input_messages/morpheus:23.11-runtime.json -o /dev/null" - ] - }, - { - "cell_type": "markdown", - "id": "53021943-1529-4d6c-b93a-07a84553b7e5", - "metadata": {}, - "source": [ - "When you make the curl request, you will see something like below in the log file as the workflow is scanning the container:" - ] - }, - { - "cell_type": "markdown", - "id": "89271696-012b-4e80-acc3-470944a0b3e3", - "metadata": {}, - "source": [ - "```bash\n", - "Collecting documents from git repos. Source Infos: [{\"type\": \"code\", \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\", \"ref\": \"v23.11.01\", \"include\": [\"**/*.cpp\", \"**/*.cu\", \"**/*.cuh\", \"**/*.h\", \"**/*.hpp\", \"**/*.ipynb\", \"**/*.py\", \"**/*Dockerfile\"], \"exclude\": [\"tests/**/*\"]}]\n", - "Cloning repository from URL: 'https://github.com/nv-morpheus/Morpheus.git' @ 'v23.11.01'\n", - "Loaded Git repository at path: '.cache/am_cache/git/https:/github.com/nv-morpheus/Morpheus.git' @ 'v23.11.01'\n", - "Scanning documents for Git repository at path: '.cache/am_cache/git/https:/github.com/nv-morpheus/Morpheus.git'\n", - "Processing 615 files in the Git repository at path: '.cache/am_cache/git/https:/github.com/nv-morpheus/Morpheus.git'\n", - "Collected documents for '.cache/am_cache/git/https:/github.com/nv-morpheus/Morpheus.git', Document count: 2250\n", - "Creating FAISS database from source documents. Doc count: 2250, Chunks: 7965, Location: .cache/am_cache/vdb/code/ccf658f72228c497\n", - "``` " - ] - }, - { - "cell_type": "markdown", - "id": "56eb58e7-90bf-4fd0-8ee7-c4f81c4bcfd1", - "metadata": {}, - "source": [ - "Exploring the **Output of the Workflow**\n", - "\n", - "Upon completion of the workflow, you should see messages similar to the following, depending on the CVE you submitted in the previous step. The output is stashed by default at `.tmp/output.json`. Sample logs are as follows:\n", - "\n", - "```bash\n", - "Vulnerability 'GHSA-3f63-hfp8-52jq' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'CVE-2024-51301' affected status: FALSE. Label: code_not_reachable\n", - "Vulnerability 'CVE-2023-36632' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'CVE-2023-43804' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'GHSA-cxfr-5q3r-2rc2' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'GHSA-554w-xh4j-8w64' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'GHSA-3ww4-gg4f-jr7f' affected status: FALSE. Label: code_not_present\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77626a98-65d3-4de7-ac97-7cea1f5b5482", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "! cat .tmp/output.json" - ] - }, - { - "cell_type": "markdown", - "id": "e1348365-3caa-4d3d-9099-1178fdc924d0", - "metadata": {}, - "source": [ - "3. After testing queries, explore the output of the workflow. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ce9fe78c-5b31-4787-9125-c9240266a471", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "import json\n", - "with open('.tmp/output.json', 'r') as result_in:\n", - " # Read just the first result in case multiple requests were sent\n", - " first_scan_result = result_in.readline()\n", - " scan_results = json.loads(first_scan_result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60dd21da-371f-4eb5-807b-d8730b917afa", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# Print the structure of the results\n", - "print(json.dumps(scan_results, indent=2))\n", - "\n", - "# Or see the keys at the top level\n", - "print(scan_results.keys())" - ] - }, - { - "cell_type": "markdown", - "id": "6cca11d1-df47-4cba-b667-5cccf105849e", - "metadata": {}, - "source": [ - "4. Explore the vulnerability analysis outputs using `meta/llama-3.1-70b-instruct`!" - ] - }, - { - "cell_type": "markdown", - "id": "f0181f3a", - "metadata": {}, - "source": [ - "
\n", - " Note: This explore section is optional and independent of the deployment. You will need to restart the kernel after installing the following modules.\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "44ec8875", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m ensurepip\n", - "%pip install openai\n", - "%pip install python-dotenv" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28becec9-47dd-4a6d-8daf-1cf9ca28a772", - "metadata": {}, - "outputs": [], - "source": [ - "%cd vulnerability-analysis\n", - "\n", - "%load_ext dotenv\n", - "%dotenv" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "be2ad682-dc26-478c-b58f-a992d73a1a8d", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "import os\n", - "from openai import OpenAI\n", - "import json\n", - "\n", - "\n", - "nvidia_api_key = os.environ.get('NVIDIA_API_KEY')\n", - "\n", - "# Initialize NVIDIA API client\n", - "client = OpenAI(\n", - " api_key=nvidia_api_key,\n", - " base_url=\"https://integrate.api.nvidia.com/v1\"\n", - ")\n", - "\n", - "# Load vulnerability data\n", - "with open('.tmp/output.json', 'r') as f:\n", - " # Read just the first result in case multiple requests were sent\n", - " first_result = f.readline()\n", - " data = json.loads(first_result)\n", - "\n", - "# Format the context once\n", - "context = \"\"\n", - "for vuln in data['output']:\n", - " context += f\"\"\"\n", - "Vulnerability ID: {vuln['vuln_id']}\n", - "Summary: {vuln['summary']}\n", - "Justification: {vuln['justification']['label']}\n", - "Reason: {vuln['justification']['reason']}\n", - "Status: {vuln['justification']['status']}\n", - "\"\"\"\n", - "\n", - "# Create completion\n", - "prompt = \"\"\"Based on the following vulnerability information:\n", - "{}\n", - "Question: What vulnerabilities were found to be exploitable?\n", - "Please provide a detailed answer focusing on the vulnerability details provided.\"\"\".format(context)\n", - "\n", - "completion = client.chat.completions.create(\n", - " model=\"meta/llama-3.1-70b-instruct\",\n", - " messages=[\n", - " {\"role\": \"system\", \"content\": \"You are a security expert analyzing vulnerability reports.\"},\n", - " {\"role\": \"user\", \"content\": prompt}\n", - " ],\n", - " temperature=0.5,\n", - " top_p=1,\n", - " max_tokens=1024,\n", - " stream=True\n", - ")\n", - "\n", - "# Print the response\n", - "for chunk in completion:\n", - " if chunk.choices[0].delta.content is not None:\n", - " print(chunk.choices[0].delta.content, end=\"\")" - ] - }, - { - "cell_type": "markdown", - "id": "3d774fc0-3b2c-4482-83c4-cbfa32851eae", - "metadata": {}, - "source": [ - "## API Reference\n", - "For detailed API references, please refer to the following locations in the current directory:\n", - ">- API Definition and Summarization:\n", - "./configs/openapi/openapi.json" - ] - }, - { - "cell_type": "markdown", - "id": "a4b0c82e-9d62-4f21-ab17-e3af1369d309", - "metadata": {}, - "source": [ - "# Next Steps" - ] - }, - { - "cell_type": "markdown", - "id": "6d5e506f-6a11-4e1f-ae43-b87a2b2d647f", - "metadata": {}, - "source": [ - "Open the **2_workflow_customization.ipynb** notebook. Within this notebook you will customize the workflow to generate a new configuration file, define the workflow settings, such as the input data, the LLM models used, and the output format. You will also be able to swap in CVE IDs and custom code repository." - ] - }, - { - "cell_type": "markdown", - "id": "c23cdd4d-2f22-46c1-ab94-99847c1c7dbb", - "metadata": {}, - "source": [ - "# Appendix\n", - "________________________" - ] - }, - { - "cell_type": "markdown", - "id": "7239d79e-d6a4-491c-bd7f-289e479078cc", - "metadata": {}, - "source": [ - "## NGINX caching server" - ] - }, - { - "cell_type": "markdown", - "id": "80fcdcbd-8492-4bc3-96e0-9377dce50641", - "metadata": {}, - "source": [ - "The docker compose file includes an nginx-cache proxy server container that enables caching for API requests made by the workflow. It is highly recommend to route API requests through the proxy server to reduce API calls for duplicate requests and improve workflow speed. This is especially useful when running the workflow multiple times with the same configuration (e.g., for debugging) and can help keep costs down when using paid APIs." - ] - }, - { - "cell_type": "markdown", - "id": "fa41adad-0209-46a0-9d08-03aa86ad1296", - "metadata": {}, - "source": [ - "The NGINX proxy server is started by default when running the `vuln-analysis` service. However, it can be started separately using the following command:" - ] - }, - { - "cell_type": "markdown", - "id": "adbad08f-19da-4dea-a77f-8a8ba125b5e9", - "metadata": {}, - "source": [ - "```bash\n", - "cd ${REPO_ROOT}\n", - "docker compose up --detach nginx-cache\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "850c7be0-4b48-4a07-b320-a6a5fb75f7b4", - "metadata": {}, - "source": [ - "## Troubleshooting" - ] - }, - { - "cell_type": "markdown", - "id": "6acb22af-807f-4386-99a3-efe6753ad676", - "metadata": {}, - "source": [ - "\n", - "Several common issues can arise when running the workflow. Here are some common issues and their solutions.\n", - "\n", - "### Git LFS issues\n", - "\n", - "If you encounter issues with Git LFS, ensure that you have Git LFS installed and that it is enabled for the repository. You can check if Git LFS is enabled by running the following command:\n", - "```bash\n", - "git lfs install\n", - "```\n", - "\n", - "Verifying that all files are being tracked by Git LFS can be done by running the following command:\n", - "```bash\n", - "git lfs ls-files\n", - "```\n", - "Files which are missing will show a `-` next to their name. To ensure all LFS files have been pulled correctly, you can run the following command:\n", - "```bash\n", - "git lfs fetch --all\n", - "git lfs checkout *\n", - "```\n", - "\n", - "### NGINX caching server\n", - "\n", - "Because the workflow makes such heavy use of the caching server to speed up API requests, it is important to ensure that the server is running correctly. If you encounter issues with the caching server, you can reset the cache.\n", - "\n", - "#### Resetting the entire cache\n", - "\n", - "To reset the entire cache, you can run the following command:\n", - "```bash\n", - "docker compose -f docker-compose.yml -f docker-compose.nim.yml down -v\n", - "```\n", - "This will delete all the volumes associated with the containers, including the cache.\n", - "\n", - "#### Resetting just the LLM cache or the services cache\n", - "\n", - "If you want to reset just the LLM cache or the services cache, you can run the following commands:\n", - "```bash\n", - "docker compose -f docker-compose.yml down\n", - "\n", - "# To remove the LLM cache\n", - "docker volume rm ${COMPOSE_PROJECT_NAME:-vuln_analysis}-llm-cache\n", - "\n", - "# To remove the services cache\n", - "docker volume rm ${COMPOSE_PROJECT_NAME:-vuln_analysis}-service-cache\n", - "```\n", - "\n", - "### Service outages\n", - "\n", - "#### National Vulnerability Database (NVD)\n", - "These typically resolve on their own. Please wait and try running the workflow again later. Example errors:\n", - "\n", - "404\n", - "```\n", - "Error requesting [1/10]: (Retry 0.1 sec) https://services.nvd.nist.gov/rest/json/cves/2.0: 404, message='', url=URL('https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-6709')\n", - "```\n", - "\n", - "503\n", - "```\n", - "Error requesting [1/10]: (Retry 0.1 sec) https://services.nvd.nist.gov/rest/json/cves/2.0: 503, message='Service Unavailable', url=URL('https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-50447')\n", - "```" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/deploy/2_Customize_CVE.ipynb b/deploy/2_Customize_CVE.ipynb index 618b1d11a..a15c91d30 100644 --- a/deploy/2_Customize_CVE.ipynb +++ b/deploy/2_Customize_CVE.ipynb @@ -1,1150 +1,442 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "a8cd5927-ff0f-4be4-a308-3aee67d6d96a", - "metadata": {}, - "source": [ - "# Customize the Vulnerablity Analysis Blueprint" - ] - }, - { - "cell_type": "markdown", - "id": "76740610-ac1d-4925-83e5-2f0e03022071", - "metadata": {}, - "source": [ - "This notebook is a continuation of the previous [CVE deploy notebook](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/blob/main/deploy/1_Deploy_CVE.ipynb). Within this notebook, you will walk through the steps to customize the workflow in order to analyze different containers and CVEs. You will also generate a custom configuration file defining workflow settings, such as the input data, the LLM models used, and the output format. There is also a try-it-yourself portion where you will be able to make a curl request using your custom configuration and input message, and analyze the output.\n", - "\n", - "A comprehensive guide to modifying the input to the workflow and workflow configuration options can be found in the [README](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/blob/main/README.md)." - ] - }, - { - "cell_type": "markdown", - "id": "0cf4188e-bda6-45dc-bf45-9c30130ad835", - "metadata": {}, - "source": [ - "# Table of Contents\n", - ">[Prerequisites and Setup](#prerequisites-and-setup)\n", - ">\n", - ">[Customize the Input Message](#customize-the-input-message)\n", - ">\n", - ">[Customize the Configuration File](#customize-the-configuration-file)\n", - ">\n", - ">[Try It Yourself](#try-it-yourself)\n", - ">\n", - ">[Wrapping Up](#wrapping-up)\n", - "> " - ] - }, - { - "cell_type": "markdown", - "id": "55d676d1-c4de-45ce-9b49-dbcac6fdfd44", - "metadata": {}, - "source": [ - "## Prerequisites and Setup" - ] - }, - { - "cell_type": "markdown", - "id": "f7d21d29-b059-4d07-ab9e-9e7db4b53aee", - "metadata": {}, - "source": [ - "Make sure the containers you deployed in the previous [CVE Deploy notebook](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/blob/main/deploy/1_Deploy_CVE.ipynb) are still up and running." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "78ea5254", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import json" - ] - }, - { - "cell_type": "markdown", - "id": "4148f9ba", - "metadata": {}, - "source": [ - "Change the notebook's working directory to the repository root. All the directory and file paths below will now be relative to `REPO_ROOT`." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "aa88a441", - "metadata": {}, - "outputs": [], - "source": [ - "REPO_ROOT = !echo $(git rev-parse --show-toplevel)\n", - "os.chdir(REPO_ROOT[0])" - ] - }, - { - "cell_type": "markdown", - "id": "0c416dcb", - "metadata": {}, - "source": [ - "## Customize the Input Message" - ] - }, - { - "cell_type": "markdown", - "id": "66411e3c-37ca-45df-82c5-377ee3e4a581", - "metadata": {}, - "source": [ - "In this section, we will explore how you can customize the input message to scan any other container image / repository as an input to the workflow. Please keep in mind that the preprocessing could take a while for very large repositories. Let's first begin by pulling the container of your choice. Feel free to modify the link below to the URL you'd prefer." - ] - }, - { - "cell_type": "markdown", - "id": "f0589f5a-656e-4fb4-80d0-99cadbd8842c", - "metadata": {}, - "source": [ - " 1. **Generate** the SBOM for your container" - ] - }, - { - "cell_type": "markdown", - "id": "7a55174c", - "metadata": {}, - "source": [ - "The first dependency is generating an SBOM. This information is used by the workflow to check for vulnerable packages or dependencies in the container.\n", - "\n", - "Here, we will demonstrate using the public Langchain docker container from `dockerhub` and we'll install the open source [Anchore Syft](https://github.com/anchore/syft) CLI to generate the SBOM. Syft will automatically pull the container from the `image_url` and save it to the `sbom_file` location specified below. Be aware that depending on the size of the container, the SBOM generation process can take several minutes to complete." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "d80516ca-e2c7-42e4-b0d9-8a30b066ba52", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "docker.io/langchain/langchain:0.1.0\n" - ] - } - ], - "source": [ - "# Specify the url of container image to generate an SBOM for\n", - "image_base = \"docker.io/langchain/langchain\"\n", - "image_tag = \"0.1.0\"\n", - "image_url = f\"{image_base}:{image_tag}\"\n", - "print(image_url)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "299dfc96-11b5-4192-9e56-1aae6ff2d2e9", - "metadata": {}, - "outputs": [], - "source": [ - "# Specify SBOM output file location\n", - "sbom_file = \"data/sboms/langchain.sbom\"" - ] - }, - { - "cell_type": "markdown", - "id": "6c001ae4", - "metadata": {}, - "source": [ - "Install the syft CLI by running the following command in a terminal (requires sudo).\n", - "\n", - "```bash\n", - "curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "3327788e", - "metadata": {}, - "source": [ - "Then run the following syft command to generate the SBOM." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "5da98688-5eab-46ca-9007-30043b87b981", - "metadata": { - "collapsed": true, - "jupyter": { - "outputs_hidden": true - } - }, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Customize the Vulnerability Analysis Pipeline\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b]11;?\u001b\\\u001b[6n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook continues from `1_Deploy_CVE.ipynb` and focuses on customizing the current README workflow. The pipeline input is a `PipelineInput` JSON document: one or more CVE/GHSA identifiers, `image_name`, `image_tag`, optional rootfs paths, cache controls, and output settings.\n", + "\n", + "The previous SBOM/source-repository payload format is no longer the primary workflow input. Package verification is now filesystem-based, and automatic image extraction is handled by the orchestrator when `filesystem_path` is omitted.\n" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - " \u001b[92m✔\u001b[0m \u001b[1mLoaded image\u001b[0m \u001b[90mindex.docker.io/langchain/langchain:0.1.0\u001b[0m\u001b[0K\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[0K\u001b[80D\u001b[2A\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mParsing image\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90msha256:f98f743b99fd63\u001b[0m\u001b[90m92\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B \u001b[92m✔\u001b[0m \u001b[1mParsed image\u001b[0m \u001b[90msha256:f98f743b99fd6392d4dd139b6800dbc629ec\u001b[0m \u001b[90m\u001b[0m\n", - "\u001b[80D\u001b[2A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[3A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠙\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠹\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠹\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠸\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠸\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠼\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠼\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠴\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠴\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠦\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠦\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠧\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠧\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠇\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠇\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠏\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠏\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠋\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠋\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠙\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠹\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠹\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠸\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠸\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠼\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠼\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠴\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠦\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠦\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠧\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠇\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠇\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠏\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠋\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠹\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠹\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠸\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠸\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠼\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠼\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠼\u001b[0m Dpkg db cataloger \u001b[0K\n", - " ├── \u001b[95m⠋\u001b[0m \u001b[0K\n", - " ├── \u001b[95m⠋\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠋\u001b[0m \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[8A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Cargo auditable binary cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠴\u001b[0m Packages \u001b[90m[0 packages]\u001b[0m \u001b[0K\n", - " ├── \u001b[95m⠴\u001b[0m Dpkg db cataloger \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠙\u001b[0m Cargo auditable binary cataloger \u001b[0K\n", - " ├── \u001b[95m⠙\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠋\u001b[0m \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[8A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Dotnet portable executable cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠙\u001b[0m Dotnet portable executable cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Python installed package cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[8A\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m Packages \u001b[90m[435 packages]\u001b[0m \u001b[0K\n", - "\u001b[B ├── \u001b[95m⠹\u001b[0m Cargo auditable binary cataloger \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[8A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠦\u001b[0m Packages \u001b[90m[435 packages]\u001b[0m \u001b[0K\n", - " ├── \u001b[95m⠹\u001b[0m Cargo auditable binary cataloger \u001b[0K\n", - " ├── \u001b[95m⠙\u001b[0m Dotnet portable executable cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Python installed package cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠙\u001b[0m Python installed package cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Go module binary cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[8A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B ├── \u001b[95m⠙\u001b[0m Dotnet portable executable cataloger \u001b[0K\n", - " ├── \u001b[95m⠙\u001b[0m Python installed package cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Go module binary cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B ├── \u001b[95m⠹\u001b[0m Dotnet portable executable cataloger \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B ├── \u001b[95m⠹\u001b[0m Python installed package cataloger \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠧\u001b[0m Packages \u001b[90m[435 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠹\u001b[0m Python installed package cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Go module binary cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B └── \u001b[95m⠹\u001b[0m Go module binary cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m Packages \u001b[90m[472 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠹\u001b[0m Go module binary cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Graalvm native image cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠇\u001b[0m Packages \u001b[90m[472 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Graalvm native image cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠹\u001b[0m Graalvm native image cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Nix store cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠏\u001b[0m Packages \u001b[90m[472 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠙\u001b[0m Nix store cataloger \u001b[0K\n", - " └── \u001b[95m⠋\u001b[0m \u001b[0K\n", - "\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Binary classifier cataloger \u001b[0K\n", - "\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B ├── \u001b[95m⠹\u001b[0m Nix store cataloger \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠋\u001b[0m Packages \u001b[90m[472 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Binary classifier cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Binary classifier cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠙\u001b[0m Packages \u001b[90m[472 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B └── \u001b[95m⠸\u001b[0m Binary classifier cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠸\u001b[0m Binary classifier cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Elf binary package cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠹\u001b[0m Packages \u001b[90m[472 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠙\u001b[0m Elf binary package cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Linux kernel cataloger \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠼\u001b[0m Binary classifier cataloger \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B ├── \u001b[95m⠹\u001b[0m Elf binary package cataloger \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B ├── \u001b[95m⠹\u001b[0m Elf binary package cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Linux kernel cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " └── \u001b[95m⠸\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m Linux kernel cataloger \u001b[0K\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Linux kernel cataloger \u001b[0K\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠸\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " │ └── \u001b[95m⠹\u001b[0m Linux kernel cataloger \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/etc/apt/trusted\u001b[0m\n", - "\u001b[0K\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠼\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/etc/apt/trusted\u001b[0m\n", - "\u001b[0K\u001b[0J\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/etc/apt/trusted\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠴\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/split]\u001b[0m \n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/split]\u001b[0m \n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠦\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/split]\u001b[0m \n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠧\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠇\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠦\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/include/lin\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠏\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠦\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/include/lin\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/include/lin\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/include/lin\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠋\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/dpkg/me\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/dpkg/me\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠇\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/dpkg/me\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠙\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/dpkg/me\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/gcc/x86\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠹\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/gcc/x86\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠸\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/gcc/x86\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/gcc/x86\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠼\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/gcc/x86\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠴\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/python3\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/python3\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠦\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/python3\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠧\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠇\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠦\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠏\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠋\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠇\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠙\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠹\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠸\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠼\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠴\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠦\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠧\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠇\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/local/bin/p\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠦\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/local/bin/p\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠦\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/local/bin/p\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠏\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/local/bin/p\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠋\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/doc/g\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠧\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/doc/g\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠇\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/doc/g\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠇\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/doc/g\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠙\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/doc/g\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/glib-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠹\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/glib-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠏\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/glib-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/glib-\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠸\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠋\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/local\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/local\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠙\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/local\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠼\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/local\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠴\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/man/m\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/man/m\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/man/m\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠦\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/man/m\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠧\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - "\u001b[80D\u001b[5A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - " └── \u001b[95m⠙\u001b[0m File metadata \u001b[90m[/usr/bin/import-im6.q16]\u001b[0m \u001b[0K\n", - "\u001b[0K\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠇\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠴\u001b[0m File digests \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/share/perl5\u001b[0m\n", - "\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B ├── \u001b[92m✔\u001b[0m File digests \u001b[90m[20,160 files]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m File metadata \u001b[90m[/usr/bin/import-im6.q16]\u001b[0m \u001b[0K\n", - "\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠏\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[80D\u001b[6A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[92m✔\u001b[0m File metadata \u001b[90m[20,160 locations]\u001b[0m \u001b[0K\n", - "\u001b[80D\u001b[6A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠋\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B ├── \u001b[92m✔\u001b[0m File metadata \u001b[90m[20,160 locations]\u001b[0m \u001b[0K\n", - " └── \u001b[95m⠙\u001b[0m Executables \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/convert\u001b[0m\n", - "\u001b[0K\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Executables \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/bin/convert\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠙\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠙\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Executables \u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠹\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠹\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠹\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠸\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠸\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[95m⠼\u001b[0m Executables \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90m[/usr/lib/x86_64-\u001b[0m\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B\u001b[B └── \u001b[92m✔\u001b[0m Executables \u001b[90m[1,474 executables]\u001b[0m \u001b[0K\n", - "\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠸\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - "\u001b[B\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠼\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠼\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠴\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠴\u001b[0m Packages \u001b[90m[481 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B\u001b[B ├── \u001b[95m⠴\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠦\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠦\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠧\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠧\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠇\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠇\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠏\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠏\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[95m⠋\u001b[0m \u001b[1mCataloging contents\u001b[0m \u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[93m━\u001b[0m\u001b[90m━\u001b[0m \u001b[90mc5768e1119e52c2a6b20a\u001b[0m\u001b[90m0a\u001b[0m\u001b[90m\u001b[0m\u001b[90m\u001b[0m\n", - " ├── \u001b[95m⠋\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[B\u001b[B\u001b[B\u001b[80D\u001b[7A\u001b[B\u001b[B \u001b[92m✔\u001b[0m \u001b[1mCataloged contents\u001b[0m \u001b[90mc5768e1119e52c2a6b20a0a4627b1db82978ff9cd21\u001b[0m \u001b[90m\u001b[0m\n", - " ├── \u001b[92m✔\u001b[0m Packages \u001b[90m[473 packages]\u001b[0m \u001b[0K\n", - "\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l" - ] - } - ], - "source": [ - "# Generate the SBOM\n", - "! syft {image_url} -o syft-table={sbom_file}" - ] - }, - { - "cell_type": "markdown", - "id": "0709695f", - "metadata": {}, - "source": [ - "2. **Specify** source code and documentation info" - ] - }, - { - "cell_type": "markdown", - "id": "c0955e87", - "metadata": {}, - "source": [ - "The second input required is information about the source repositories for the container, specified by the `source_info` variable below. The workflow uses this info to clone the repositories in order to analyze the source code.\n", - "\n", - "Note that `source_info` is a list that can support multi-repository containers. Also note that there are 2 source infos `type`, `code` and `doc`, for source code and documentation respectively, since the workflow uses different analysis methodology for each.\n", - "\n", - "To provide the this information to the workflow, first specify the HTTPS clone URLs and the git tags to clone as below. Next, determine the `include` and `exclude` glob patterns for files that should be included the analysis." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "41c698c3-4afa-4753-be18-bbf3ae354888", - "metadata": {}, - "outputs": [], - "source": [ - "# Set Up Code URL and Tags\n", - "source_code_repo = \"https://github.com/langchain-ai/langchain.git\"\n", - "source_code_tag = \"v0.1.0\"\n", - "doc_repo = \"https://github.com/langchain-ai/langchain.git\"\n", - "doc_tag = \"v0.1.0\"\n", - "\n", - "\n", - "# Modify the include and exclude sections based on what you want as in-scope for analysis\n", - "source_info = [\n", - " {\n", - " \"type\": \"code\",\n", - " \"git_repo\": source_code_repo,\n", - " \"tag\": source_code_tag,\n", - " \"include\": [\n", - " \"**/*.cpp\",\n", - " \"**/*.cu\",\n", - " \"**/*.cuh\",\n", - " \"**/*.h\",\n", - " \"**/*.hpp\",\n", - " \"**/*.ipynb\",\n", - " \"**/*.py\",\n", - " \"**/*.js\",\n", - " \"**/*.ts\",\n", - " \"**/*Dockerfile\"\n", - " ],\n", - " \"exclude\": [\n", - " \"tests/**/*\",\n", - " \"docs/**/*\",\n", - " ]\n", - " },\n", - " {\n", - " \"type\": \"doc\",\n", - " \"git_repo\": doc_repo,\n", - " \"tag\": doc_tag,\n", - " \"include\": [\n", - " \"**/*.md\",\n", - " \"docs/**/*.rst\",\n", - " \"docs/**/*.txt\"\n", - " ]\n", - " }\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "cfb9a9c4-331b-40eb-ab21-9b5f57165f7f", - "metadata": {}, - "source": [ - "3. **Identify** a list of vulnerability IDs" - ] - }, - { - "cell_type": "markdown", - "id": "e306ae3b-f2e2-4b30-ab5c-31d75766d1a4", - "metadata": {}, - "source": [ - "Next, you will need a list of vulnerabilities such as CVE IDs to analyze for exploitability for your custom container/repo. The workflow uses this info to query vulnerability databases such as National Vulnerability Database, GitHub Security Advisory, Ubuntu Security Notices, and Red Hat Security Advisory.\n", - "\n", - "The list of vulnerabilities are typically identified by traditional vulnerability scanners such as [Anchore](https://anchore.com/). Here, you will understand how to scan the container for vulnerabilities and generate a CVE list using the open source [Trivy](https://github.com/aquasecurity/trivy) scanner." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1ea1b655", - "metadata": {}, - "outputs": [], - "source": [ - "# Specify the vulnerability output file\n", - "vulnerabilities_file = \".tmp/langchain_vulnerabilities.json\"" - ] - }, - { - "cell_type": "markdown", - "id": "069735ef-b518-4972-83a9-558f52333873", - "metadata": {}, - "source": [ - "Install the Trivy CLI by running the following command in a terminal (requires sudo).\n", - "\n", - "```bash\n", - "curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin\n", - "```\n", - "\n", - "Then run the next cell to run the trivy command. The output will be saved to the `vulnerabilities_file` location, where the list of CVE IDs can be found in the \"Results\" field." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "d8ad792a-53db-49bb-96c2-4c611f5d37c5", - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Table of Contents\n", + "\n", + ">[Prerequisites and Setup](#Prerequisites-and-Setup) \n", + ">[Customize PipelineInput](#Customize-PipelineInput) \n", + ">[Use a Pre-Extracted Rootfs](#Use-a-Pre-Extracted-Rootfs) \n", + ">[Run Custom Inputs](#Run-Custom-Inputs) \n", + ">[Customize Configuration](#Customize-Configuration) \n", + ">[Run Individual Agents](#Run-Individual-Agents) \n", + ">[Benchmarking](#Benchmarking) \n", + ">[Wrapping Up](#Wrapping-Up)\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\t[vuln] Vulnerability scanning is enabled\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\t[secret] Secret scanning is enabled\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\t[secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\t[secret] Please see also https://aquasecurity.github.io/trivy/v0.58/docs/scanner/secret#recommendation for faster secret detection\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\tDetected OS\tfamily=\"debian\" version=\"12.4\"\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\t[debian] Detecting vulnerabilities...\tos_version=\"12\" pkg_num=429\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\tNumber of language-specific files\tnum=1\n", - "2025-01-02T18:27:03-08:00\t\u001b[94mINFO\u001b[0m\t[python-pkg] Detecting vulnerabilities...\n", - "2025-01-02T18:27:03-08:00\t\u001b[93mWARN\u001b[0m\tUsing severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.58/docs/scanner/vulnerability#severity-selection for details.\n" - ] - } - ], - "source": [ - "# Get list of vulnerabilities using trivy\n", - "! trivy image {image_url} --format json --output {vulnerabilities_file}" - ] - }, - { - "cell_type": "markdown", - "id": "11bed251", - "metadata": {}, - "source": [ - "For this demo, we've limited the list of CVEs to just the few below, selected from the output file. A more realistic scenario in production is to scan every vulnerability detected by the scanner above a given severity threshold." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "9612f533", - "metadata": {}, - "outputs": [], - "source": [ - "# Specify the list of vulnerabilities here\n", - "vuln_ids = [\"CVE-2011-3374\", \"CVE-2024-28085\", \"CVE-2024-8096\"]\n", - "vuln_list = [{\"vuln_id\": vuln} for vuln in vuln_ids]" - ] - }, - { - "cell_type": "markdown", - "id": "fc7e5930-d5a9-48da-b8b6-580db48a03ff", - "metadata": {}, - "source": [ - "4. **Create** custom JSON `input_message` file for your Container" - ] - }, - { - "cell_type": "markdown", - "id": "1e842db7-0eed-4167-b5e6-8b2405963d98", - "metadata": {}, - "source": [ - "Now, we'll need to populate the JSON input message with the necessary metadata required by the workflow to do a scan. We'll use one of the existing pre-generated sample `input_messages` json file by utilizing the content [here](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/tree/main/data/input_messages) to create the custom JSON file using your generated custom SBOM file, selected CVE IDs from the CVE list and other repository metadata.\n", - "\n", - "Please be sure to modify the relevant URLs and tags to the container and code you want to scan by updating the `image name`, `tags`, `source_info` etc relating to the new container" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "9754e7fa", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the path to the config files\n", - "sample_config_path = \"data/input_messages/morpheus:23.11-runtime.json\"\n", - "custom_config_path = \"data/input_messages/langchain.json\"\n", - "\n", - "# Load the sample configuration from the file\n", - "with open(sample_config_path, \"r\") as file:\n", - " sample_config = json.load(file)\n", - "\n", - "# Modify the configuration\n", - "def modify_config(config):\n", - " # Update the image name and tag\n", - " config[\"image\"][\"name\"] = image_base\n", - " config[\"image\"][\"tag\"] = image_tag\n", - "\n", - " # Add a new source_info entry\n", - " config[\"image\"][\"source_info\"] = source_info\n", - "\n", - " # Update SBOM file path\n", - " config[\"image\"][\"sbom_info\"][\"file_path\"] = sbom_file\n", - "\n", - " # Replace the vulnerability list\n", - " config[\"scan\"][\"vulns\"] = vuln_list\n", - "\n", - " return config\n", - "\n", - "# Apply modifications to the config\n", - "updated_config = modify_config(sample_config)\n", - "\n", - "# Save the modified configuration to the new file\n", - "os.makedirs(os.path.dirname(custom_config_path), exist_ok=True)\n", - "with open(custom_config_path, \"w\") as config_out:\n", - " json.dump(updated_config, config_out, indent=2)" - ] - }, - { - "cell_type": "markdown", - "id": "69ce4f78-3490-465e-bfcb-a1f0fd1c595b", - "metadata": {}, - "source": [ - "Let us view the input message to validate it's contents." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "aa68d35c-0d48-4bc4-a756-bb0e09fc5f63", - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites and Setup\n", + "\n", + "Make sure the repository is installed locally with `uv sync`, or that the Docker Compose deployment from the first notebook is still running. All paths below are relative to the repository root.\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"image\": {\n", - " \"name\": \"docker.io/langchain/langchain\",\n", - " \"tag\": \"0.1.0\",\n", - " \"source_info\": [\n", - " {\n", - " \"type\": \"code\",\n", - " \"git_repo\": \"https://github.com/langchain-ai/langchain.git\",\n", - " \"tag\": \"v0.1.0\",\n", - " \"include\": [\n", - " \"**/*.cpp\",\n", - " \"**/*.cu\",\n", - " \"**/*.cuh\",\n", - " \"**/*.h\",\n", - " \"**/*.hpp\",\n", - " \"**/*.ipynb\",\n", - " \"**/*.py\",\n", - " \"**/*.js\",\n", - " \"**/*.ts\",\n", - " \"**/*Dockerfile\"\n", - " ],\n", - " \"exclude\": [\n", - " \"tests/**/*\",\n", - " \"docs/**/*\"\n", - " ]\n", - " },\n", - " {\n", - " \"type\": \"doc\",\n", - " \"git_repo\": \"https://github.com/langchain-ai/langchain.git\",\n", - " \"tag\": \"v0.1.0\",\n", - " \"include\": [\n", - " \"**/*.md\",\n", - " \"docs/**/*.rst\",\n", - " \"docs/**/*.txt\"\n", - " ]\n", - " }\n", - " ],\n", - " \"sbom_info\": {\n", - " \"_type\": \"file\",\n", - " \"file_path\": \"data/sboms/langchain.sbom\"\n", - " }\n", - " },\n", - " \"scan\": {\n", - " \"vulns\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2011-3374\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"CVE-2024-28085\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"CVE-2024-8096\"\n", - " }\n", - " ]\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "with open('data/input_messages/langchain.json', 'r') as input_message_in:\n", - " input_message = json.load(input_message_in)\n", - "print(json.dumps(input_message, indent=2))" - ] - }, - { - "cell_type": "markdown", - "id": "83c6d07c-dbe4-4ddc-bc71-40634ed993f1", - "metadata": {}, - "source": [ - "5. **Send** input message to workflow" - ] - }, - { - "cell_type": "markdown", - "id": "6a7930dc-771c-4ff1-88db-6a8b6bab227e", - "metadata": {}, - "source": [ - "This looks good! Now, we will submit it to the HTTP server of the Vulnerability Analysis workflow. If your HTTP server is no longer running, you can bring it back up by running the following command again in your container.\n", - "\n", - "```bash\n", - "nat serve --config_file=.tmp/config.yml --host 0.0.0.0 --port 26466\n", - "```\n", - "\n", - "We can submit the custom scan request as below by running the curl command. Like last time, keep an eye on the output in the terminal where you ran this command `docker compose exec -it vuln-analysis bash`." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "18dc59d3", - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import json\n", + "import os\n", + "\n", + "\n", + "def find_repo_root(start: Path) -> Path:\n", + " for candidate in [start, *start.parents]:\n", + " if (candidate / \"pyproject.toml\").exists() and (candidate / \"src\" / \"vuln_analysis\").exists():\n", + " return candidate\n", + " raise RuntimeError(\"Run this notebook from inside the vulnerability-analysis repository.\")\n", + "\n", + "\n", + "REPO_ROOT = find_repo_root(Path.cwd())\n", + "os.chdir(REPO_ROOT)\n", + "os.environ[\"REPO_ROOT\"] = str(REPO_ROOT)\n", + "print(f\"Repository root: {REPO_ROOT}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Customize PipelineInput\n", + "\n", + "The full workflow accepts the fields below.\n", + "\n", + "| Field | Description |\n", + "|---|---|\n", + "| `vulns` | List of vulnerability entries. Each entry has `vuln_id` and optional `package_hint` / `version_hint`. |\n", + "| `image_name` | Container repository/name without the tag. |\n", + "| `image_tag` | Container tag. Required for automatic and pre-extracted inputs. |\n", + "| `filesystem_path` | Optional path to a pre-extracted rootfs. If omitted, the pipeline pulls and extracts `image_name:image_tag` with `crane`. |\n", + "| `image_config_path` | Optional path to image config JSON; auto-populated for automatic extraction. |\n", + "| `skip_web_research` | Set `true` to skip container web research, useful for private images. |\n", + "| `output_dir` | Root directory for reports and VEX results. |\n", + "| `cache` | Set `false` to disable stage caching for this run. |\n", + "| `cache_cve_researcher`, `cache_container_analyzer`, `cache_exploitability_analyzer`, `cache_vex_categorizer` | Optional per-agent cache overrides. |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a single-CVE input that uses automatic rootfs extraction.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "custom_input = {\n", + " \"vulns\": [{\"vuln_id\": \"CVE-2025-47273\"}],\n", + " \"image_name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", + " \"image_tag\": \"25.06-runtime\",\n", + " \"output_dir\": \"outputs/custom-morpheus\",\n", + " \"cache\": True,\n", + "}\n", + "\n", + "custom_input_path = Path(\".tmp/custom_pipeline_input.json\")\n", + "custom_input_path.parent.mkdir(parents=True, exist_ok=True)\n", + "custom_input_path.write_text(json.dumps(custom_input, indent=2) + \"\\n\")\n", + "print(custom_input_path)\n", + "print(json.dumps(custom_input, indent=2))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a multi-CVE input against the same container. The Container Analyzer runs once, while CVE Researcher, Exploitability Analyzer, and VEX Categorizer fan out per CVE.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "multi_cve_input = {\n", + " \"vulns\": [\n", + " {\"vuln_id\": \"CVE-2025-47273\"},\n", + " {\"vuln_id\": \"CVE-2026-25990\", \"package_hint\": \"setuptools\"},\n", + " ],\n", + " \"image_name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", + " \"image_tag\": \"25.06-runtime\",\n", + " \"output_dir\": \"outputs/custom-morpheus-multi\",\n", + " \"cache\": True,\n", + "}\n", + "\n", + "multi_input_path = Path(\".tmp/custom_multi_cve_input.json\")\n", + "multi_input_path.write_text(json.dumps(multi_cve_input, indent=2) + \"\\n\")\n", + "print(multi_input_path)\n", + "print(json.dumps(multi_cve_input, indent=2))\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: Unnecessary use of -X or --request, POST is already inferred.\n", - "* Trying 0.0.0.0:26466...\n", - "* TCP_NODELAY set\n", - "* Connected to 0.0.0.0 (127.0.0.1) port 26466 (#0)\n", - "> POST /scan HTTP/1.1\n", - "> Host: 0.0.0.0:26466\n", - "> User-Agent: curl/7.68.0\n", - "> Accept: */*\n", - "> Content-Length: 1050\n", - "> Content-Type: application/x-www-form-urlencoded\n", - "> Expect: 100-continue\n", - "> \n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For private images, set `skip_web_research` to `true`. The filesystem analysis still runs, but the Container Analyzer skips public registry/vendor searches.\n" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "* Done waiting for 100-continue\n", - "* We are completely uploaded and fine\n", - "* Mark bundle as not supporting multiuse\n", - "< HTTP/1.1 201 Created\n", - "< Content-Type: text/plain\n", - "< Content-Length: 0\n", - "< \n", - "* Connection #0 to host 0.0.0.0 left intact\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "private_image_input = {\n", + " \"vulns\": [{\"vuln_id\": \"CVE-2025-47273\"}],\n", + " \"image_name\": \"registry.example.com/team/private-app\",\n", + " \"image_tag\": \"2026.06.03\",\n", + " \"skip_web_research\": True,\n", + " \"output_dir\": \"outputs/private-app\",\n", + " \"cache\": False,\n", + "}\n", + "\n", + "print(json.dumps(private_image_input, indent=2))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use a Pre-Extracted Rootfs\n", + "\n", + "Automatic extraction is recommended for most runs. To warm the cache, debug extraction, or pass an already extracted filesystem, use the standalone helper from the README. It prints the resolved digest and rootfs path.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# Example only. This can take several minutes and may require registry auth.\n", + "# uv run python scripts/extract_image.py nvcr.io/nvidia/morpheus/morpheus:25.06-runtime .tmp/image-cache\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After extraction, add `filesystem_path` and, if available, `image_config_path` to your `PipelineInput`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pre_extracted_input = {\n", + " \"vulns\": [{\"vuln_id\": \"CVE-2025-47273\"}],\n", + " \"image_name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", + " \"image_tag\": \"25.06-runtime\",\n", + " # \"filesystem_path\": \"/path/to/extracted/rootfs\",\n", + " # \"image_config_path\": \"/path/to/extracted/image_config.json\",\n", + " \"output_dir\": \"outputs/pre-extracted-morpheus\",\n", + "}\n", + "\n", + "print(json.dumps(pre_extracted_input, indent=2))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Custom Inputs\n", + "\n", + "Run a custom input locally with `nat run`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This can take several minutes on first run because the rootfs and reports are cached.\n", + "!nat run --config_file=configs/config.yml --input_file=.tmp/custom_pipeline_input.json\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the HTTP server from the deploy notebook is running, submit the same input to `/generate`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "set -euo pipefail\n", + "\n", + "curl -sS -X POST http://localhost:26466/generate -H 'Content-Type: application/json' --data @.tmp/custom_pipeline_input.json -o .tmp/custom_generate_response.json\n", + "\n", + "python -m json.tool .tmp/custom_generate_response.json | sed -n '1,160p'\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Customize Configuration\n", + "\n", + "The default config is `configs/config.yml`, a repo-root symlink to `src/vuln_analysis/configs/config.yml`. Key defaults from the README:\n", + "\n", + "| Setting | Default |\n", + "|---|---|\n", + "| Default LLM model | `aws/anthropic/claude-opus-4-5` |\n", + "| Stage timeout | `600` seconds |\n", + "| Max concurrency | `null` (unlimited) |\n", + "| LLM rate limit | `null` (no limit) |\n", + "| Output directory | `outputs` |\n", + "\n", + "Model names are controlled with environment variables: `DEFAULT_MODEL_NAME`, `CVE_RESEARCHER_MODEL`, `CONTAINER_ANALYZER_MODEL`, `EXPLOITABILITY_ANALYZER_MODEL`, and `VEX_CATEGORIZER_MODEL`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "sed -n '1,80p' configs/config.yml\n", + "sed -n '/^workflow:/,$p' configs/config.yml\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a custom config that caps concurrent agent tasks and spaces LLM calls. Use these knobs only when your inference endpoint needs throttling.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "base_config = Path(\"configs/config.yml\").read_text()\n", + "rate_limited_config = base_config.replace(\" max_concurrency: null\", \" max_concurrency: 3\")\n", + "rate_limited_config = rate_limited_config.replace(\" llm_max_rate: null\", \" llm_max_rate: 5\")\n", + "\n", + "custom_config_path = Path(\".tmp/config-rate-limited.yml\")\n", + "custom_config_path.parent.mkdir(parents=True, exist_ok=True)\n", + "custom_config_path.write_text(rate_limited_config)\n", + "print(custom_config_path)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!uv run nat run --config_file=.tmp/config-rate-limited.yml --input_file=.tmp/custom_pipeline_input.json\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Environment Overrides\n", + "\n", + "Update `.env` or export values in your shell before starting `nat run` / `nat serve`.\n", + "\n", + "```env\n", + "NVIDIA_API_KEY=your-nvidia-api-key\n", + "NV_BASE_URL=https://inference-api.nvidia.com/v1\n", + "TAVILY_API_KEY=your-tavily-api-key\n", + "DEFAULT_MODEL_NAME=aws/anthropic/claude-opus-4-5\n", + "CVE_RESEARCHER_MODEL=aws/anthropic/claude-opus-4-5\n", + "CONTAINER_ANALYZER_MODEL=aws/anthropic/claude-opus-4-5\n", + "EXPLOITABILITY_ANALYZER_MODEL=aws/anthropic/claude-opus-4-5\n", + "VEX_CATEGORIZER_MODEL=aws/anthropic/claude-opus-4-5\n", + "CACHE_ENABLED=true\n", + "VULN_ANALYSIS_IMAGE_CACHE=.cache/vuln_analysis/images\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Individual Agents\n", + "\n", + "Each agent can be run independently for targeted debugging. Their sample inputs live under `data/examples/`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# CVE research only\n", + "# uv run nat run --config_file=configs/config-cve-researcher.yml --input_file=data/examples/cve_researcher_input.json\n", + "\n", + "# Container analysis only\n", + "# uv run nat run --config_file=configs/config-container-analyzer.yml --input_file=data/examples/container_analyzer_input.json\n", + "\n", + "# Exploitability analysis only\n", + "# uv run nat run --config_file=configs/config-exploitability-analyzer.yml --input_file=data/examples/exploitability_analyzer_input.json\n", + "\n", + "# VEX categorization only\n", + "# uv run nat run --config_file=configs/config-vex-categorizer.yml --input_file=data/examples/vex_categorizer_input.json\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Benchmarking\n", + "\n", + "Labelled datasets live under `data/eval_datasets/`, and evaluations run through `nat eval` with `configs/config-eval.yml`. The README highlights `morpheus_benchmark_2506.csv`, which covers Morpheus 25.06 runtime and tritonserver-models images.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# Run the default evaluator config\n", + "# uv run nat eval --config_file=configs/config-eval.yml\n", + "\n", + "# Or point at the Morpheus 25.06 benchmark explicitly\n", + "# uv run nat eval --config_file=configs/config-eval.yml # --override eval.general.dataset.file_path data/eval_datasets/morpheus_benchmark_2506.csv\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Wrapping Up\n", + "\n", + "Useful cleanup commands:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# Stop the Docker Compose services\n", + "# docker compose down\n", + "\n", + "# Remove cached reports for a clean rerun\n", + "# rm -rf outputs/custom-morpheus outputs/custom-morpheus-multi .tmp/outputs\n", + "\n", + "# Disable caching for one input by setting \"cache\": false in PipelineInput\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.13.2)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" } - ], - "source": [ - "! curl -v -X POST --url http://0.0.0.0:26466/generate --header 'Content-Type: application/json' --data @data/input_messages/langchain.json" - ] - }, - { - "cell_type": "markdown", - "id": "d3aabfb0-4a5e-4c7b-a6cf-16a38c6b2be3", - "metadata": {}, - "source": [ - "You see the workflow starting to scan the repository:\n", - "\n", - "```bash\n", - "Collecting documents from git repos. Source Infos: [{\"type\": \"code\", \"git_repo\": \"https://github.com/langchain-ai/langchain.git\", \"ref\": \"v0.1.0\", \"include\": [\"**/*.cpp\", \"**/*.cu\", \"**/*.cuh\", \"**/*.h\", \"**/*.hpp\", \"**/*.ipynb\", \"**/*.js\", \"**/*.py\", \"**/*.ts\", \"**/*Dockerfile\"], \"exclude\": [\"docs/**/*\", \"tests/**/*\"]}]\n", - "Source: 0 messages [00:11, ? messages/s]s://github.com/langchain-ai/langchain.git' @ 'v0.1.0'\n", - "Loaded Git repository at path: '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git' @ 'v0.1.0'\n", - "Source: 0 messages [00:19, ? messages/s] path: '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git'\n", - "Source: 0 messages [00:22, ? messages/s]\n", - "Source: 0 messages [00:23, ? messages/s]ory at path: '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git'\n", - "Source: 0 messages [00:29, ? messages/s]tests/examples/non-utf8-encoding.py'. Ignoring this file. Error: 'utf-8' codec can't decode byte 0xb1 in position 23: invalid start byte\n", - "100%|██████████| 4042/4042 [00:10<00:00, 376.42it/s]ation_tests/examples/non-utf8-encoding.py'. Ignoring this file. Error: 'utf-8' codec can't decode byte 0xb1 in position 23: invalid start byte\n", - "Collected documents for '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git', Document count: 11396e. Error: 'utf-8' codec can't decode byte 0xb1 in position 23: invalid start byte\n", - "Source: 0 messages [00:44, ? messages/s] 264.54it/s]\n", - "Source: 0 messages [05:22, ? messages/s]ments. Doc count: 11396, Chunks: 34699, Location: .cache/am_cache/vdb/code/b264f94617ad9265\n", - "Source: 0 messages [05:24, ? messages/s]r '.cache/am_cache/vdb/code/b264f94617ad9265'\n", - "Collecting documents from git repos. Source Infos: [{\"type\": \"doc\", \"git_repo\": \"https://github.com/langchain-ai/langchain.git\", \"ref\": \"v0.1.0\", \"include\": [\"**/*.md\", \"docs/**/*.rst\", \"docs/**/*.txt\"], \"exclude\": []}]\n", - "Source: 0 messages [05:29, ? messages/s]s://github.com/langchain-ai/langchain.git' @ 'v0.1.0'\n", - "Loaded Git repository at path: '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git' @ 'v0.1.0'\n", - "Source: 0 messages [05:30, ? messages/s] path: '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git'\n", - "100%|██████████| 162/162 [00:00<00:00, 1988.74it/s]\n", - "Collected documents for '.cache/am_cache/git/https:/github.com/langchain-ai/langchain.git', Document count: 162in.git'\n", - "Creating FAISS database from source documents. Doc count: 162, Chunks: 976, Location: .cache/am_cache/vdb/doc/a942b5c397a6f20e\n", - "Source: 0 messages [05:38, ? messages/s]\n", - "Completed embedding in 7.72 seconds for '.cache/am_cache/vdb/doc/a942b5c397a6f20e'\n", - "Source: 0 messages [05:43, ? messages/s]3374\n", - "100%|██████████| 468/468 [00:00<00:00, 637.87it/s] \n", - "Collected additional dependency info for 0 of 468 SBOM packages.ock.nvidia.com/cwe-details for docker.io/langchain/langchain:0.1.0\n", - "No vulnerabilities were found in either GHSA or NVD intel for CVE-2024-28085.\n", - "Found vulnerable dependencies for CVE-2024-28085.\n", - "No vulnerabilities were found in either GHSA or NVD intel for CVE-2024-8096.\n", - "Found vulnerable dependencies for CVE-2024-8096.\n", - "Source: 0 messages [05:45, ? messages/s]\n", - "No vulnerable dependencies found for CVE-2011-3374.\n", - "Source: 2 messages [06:05, 173.11s/ messages]cies to the LLM Engine\n", - "Message elapsed time: 20.593317 secs]\n", - "Vulnerability 'CVE-2011-3374' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'CVE-2024-28085' affected status: FALSE. Label: requires_configuration\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "4423eaad-9181-4025-9e5e-4a23f8c6d1c3", - "metadata": {}, - "source": [ - "In the command above:\n", - "\n", - "- `http://localhost:26466/scan` is the URL of the server and endpoint.\n", - "- The `-d` option specifies the data file being sent in the request body. In this case, it's pointing to the input file `langchain.json` in the `data/input_messages/` directory. You can refer to this file as an example of the expected data format." - ] - }, - { - "cell_type": "markdown", - "id": "c8d31ff9-667f-475c-9a82-4d56578a4fab", - "metadata": {}, - "source": [ - "You can monitor the workflow output in the terminal or explore the output file below if you'd like!" - ] - }, - { - "cell_type": "markdown", - "id": "0a905c39-66e4-42f5-8eb7-c2e0a9302c38", - "metadata": {}, - "source": [ - "An overview of each provided CVE's affected status and vulnerability label will be printed to the logs upon completion of the workflow, for example:\n", - "\n", - "```\n", - "Vulnerability 'CVE-2011-3374' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'CVE-2024-28085' affected status: TRUE. Label: vulnerable\n", - "Vulnerability 'CVE-2024-8096' affected status: FALSE. Label: code_not_present\n", - "```\n", - "\n", - "
\n", - " Note: The output you receive from the workflow may not be identical as the output in the example above. The output may vary due to the non-deterministic nature of the LLM models.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "914cd41f-851b-4773-86ed-ddce2b1c0b46", - "metadata": {}, - "source": [ - "## Customize the Configuration File\n", - "\n", - "The configuration file also allows many types of customizations, such as customizing the LLM model and parameters for each component of the workflow, as well which LLM service is used when invoking the model. A comprehensive guide to modifying the input to the workflow and workflow configuration options can be found in the [README](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/blob/main/README.md). However, in this section, we will explore changes to two configuration options: customizing the LLM model, and customizing the output." - ] - }, - { - "cell_type": "markdown", - "id": "f7d3a274-13dc-456b-8020-c78194df5fab", - "metadata": {}, - "source": [ - "1. **Customize** the LLM model\n", - "\n", - "In any configuration file, locate the `llms` section to see the current settings. For example, the following snippet defines the LLM used for the default LLM model:\n", - "\n", - "```bash\n", - " default_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 2000\n", - " top_p: 0.01\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "577e3893-a398-43de-b0a6-b2d02f2bdf64", - "metadata": {}, - "source": [ - "Additional steps to configure the LLM model can be found in the [README](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/tree/main?tab=readme-ov-file#customizing-the-llm-models)." - ] - }, - { - "cell_type": "markdown", - "id": "ca22a6ca-e70c-41ef-bb1c-b68cb11084eb", - "metadata": {}, - "source": [ - "
\n", - " Note: You would need an API key and any other required auth info for the selected service." - ] - }, - { - "cell_type": "markdown", - "id": "3b56ecad-5e66-4abf-a30b-50570a538b72", - "metadata": {}, - "source": [ - "## Try It Yourself!\n", - "\n", - "Above, we showed how to customize the workflow to scan CVEs for a custom container (`langchain`) and how to change the LLM models to other supported services like `openai` or `nemo`.\n", - "\n", - "You can follow the same steps to customize the workflow to your own container and CVEs. Other customization options can be found [here](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis/tree/main?tab=readme-ov-file#customizing-the-workflow)." - ] - }, - { - "cell_type": "markdown", - "id": "1cb72cb2-6668-428e-bf90-db52d459b305", - "metadata": {}, - "source": [ - "## Wrapping up\n", - "**For more configurations and customizations, check out the Blueprint source code [here](https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis)**\n", - "\n", - "Optionally, you can shut down the microservices using below commands" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "69380bf0", - "metadata": {}, - "outputs": [], - "source": [ - "%%bash\n", - "\n", - "cd vulnerability-analysis\n", - "\n", - "export REPO_ROOT=$(git rev-parse --show-toplevel)\n", - "export $(cat .env | xargs)\n", - "\n", - "cd $REPO_ROOT/vulnerability-analysis\n", - "\n", - "# Down Microservices\n", - "docker compose -f docker-compose.yml down" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/Dockerfile b/deploy/Dockerfile similarity index 71% rename from Dockerfile rename to deploy/Dockerfile index bb2d2f1e6..39a2f9fac 100755 --- a/Dockerfile +++ b/deploy/Dockerfile @@ -13,21 +13,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -ARG BASE_IMAGE_URL=nvcr.io/nvidia/base/ubuntu +ARG BASE_IMAGE_NAME=nvcr.io/nvidia/base/ubuntu ARG BASE_IMAGE_TAG=22.04_20240212 -ARG PYTHON_VERSION=3.12 +ARG PYTHON_VERSION=3.13 # Specified on the command line with --build-arg VULN_ANALYSIS_VERSION=$(python -m setuptools_scm) -ARG VULN_ANALYSIS_VERSION=2.1.0 +ARG VULN_ANALYSIS_VERSION=3.0.0 -FROM ${BASE_IMAGE_URL}:${BASE_IMAGE_TAG} AS base +FROM ${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG} AS base COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /uvx /bin/ +COPY --from=gcr.io/go-containerregistry/crane:v0.21.6 /ko-app/crane /usr/local/bin/crane ARG VULN_ANALYSIS_VERSION ARG PYTHON_VERSION ENV PYTHONDONTWRITEBYTECODE=1 RUN apt-get update && apt-get install -y \ + binutils \ ca-certificates \ curl \ git \ @@ -50,14 +52,26 @@ SHELL ["/bin/bash", "-c"] # Set working directory WORKDIR /workspace -# Copy the project into the container -COPY ./ /workspace +# Copy only what's needed to install the package and run the pipeline, examples and notebooks. +# Build/install inputs (pyproject + lockfile) and the package source: +COPY pyproject.toml uv.lock README.md /workspace/ +COPY src/ /workspace/src/ +# Standalone pipeline utilities (e.g. crane-based rootfs extractor): +COPY scripts/ /workspace/scripts/ +# Notebooks: +COPY deploy/ /workspace/deploy/ +COPY quick_start/ /workspace/quick_start/ + +# Recreate the repo-root convenience symlinks so `configs/` and `data/examples/` +# paths used by the examples and notebooks resolve. +RUN ln -s src/vuln_analysis/configs /workspace/configs && \ + ln -s src/vuln_analysis/data /workspace/data # Install the NeMo Agent toolkit package and vuln analysis package RUN --mount=type=cache,id=uv_cache,target=/root/.cache/uv,sharing=locked \ export SETUPTOOLS_SCM_PRETEND_VERSION=${VULN_ANALYSIS_VERSION} && \ uv venv --python ${PYTHON_VERSION} /workspace/.venv && \ - uv sync + uv sync --locked # Activate the environment (make it default for subsequent commands) RUN echo "source /workspace/.venv/bin/activate" >> ~/.bashrc diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 000000000..5df705c32 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,92 @@ +# Docker Compose Deployment + +This directory contains everything needed to run the Vulnerability Analysis pipeline +with **Docker Compose**. + +``` +deploy/ +├── docker-compose.yml # Default stack (NVIDIA-hosted NIMs) +├── docker-compose.nim.yml # Overlay adding a self-hosted nim-llm service +├── Dockerfile # vuln-analysis image (build context is the repo root) +├── nginx/ # nginx caching proxy (nginx-cache) and SSL (nginx-ssl) config +├── 1_Deploy_CVE.ipynb # Step-by-step deploy walkthrough +└── 2_Customize_CVE.ipynb # Customizing scans against the deployed service +``` + +## Prerequisites + +- Docker with the Compose v2 plugin (`docker compose version`) +- A `.env` file **at the repo root** with your API keys (see the top-level + [README](../README.md#configuration)). At minimum: + + ```env + NVIDIA_API_KEY=your-nvidia-api-key + TAVILY_API_KEY=your-tavily-api-key + ``` + +## Running + +Run all commands **from the repo root**. Point Compose at the file in this directory and +at the root `.env` (the `--env-file` flag is what lets variable substitution find your keys, +since the compose file no longer sits next to `.env`): + +```bash +# Build the application image +docker compose -f deploy/docker-compose.yml --env-file ./.env build vuln-analysis + +# Start the stack (vuln-analysis + nginx caching proxy) +docker compose -f deploy/docker-compose.yml --env-file ./.env up -d + +# Tear it down +docker compose -f deploy/docker-compose.yml --env-file ./.env down +``` + +JupyterLab is published on port `8000` and the scan HTTP server on `26466` by default. + +### Running the pipeline inside the container + +Once the stack is up, open an interactive shell in the `vuln-analysis` container: + +```bash +docker compose -f deploy/docker-compose.yml exec vuln-analysis bash +``` + +You land in `/workspace` (the repo, mounted from the host) with the project's virtual +environment already activated. From here you can run any of the pipeline commands from the +top-level [README](../README.md#usage) — `nat run`, `nat serve`, `nat eval` — exactly as +documented, for example: + +```bash +nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json +``` + +Running them in the container routes API calls through the `nginx-cache` proxy (via +`NV_BASE_URL`/`TAVILY_API_BASE_URL`), so repeated requests are served from cache — faster +runs and fewer paid API calls than running the same commands on the host. Type `exit` to +leave the shell; the containers keep running. + +### Self-hosted NIMs + +To run an LLM NIM locally instead of using NVIDIA-hosted endpoints, layer in the overlay: + +```bash +docker compose \ + -f deploy/docker-compose.yml \ + -f deploy/docker-compose.nim.yml \ + --env-file ./.env up -d +``` + +This requires NVIDIA GPUs and the NVIDIA Container Toolkit. + +### SSL proxy (optional) + +An `nginx-ssl` service is defined under the `ssl` profile. Provide `nginx/cert.pem` and +`nginx/key.pem`, then start it with `--profile ssl`. + +## How paths resolve + +The compose files live here, but the Docker **build context is the repo root** +(`context: ../`) so the `Dockerfile` can copy the package source, scripts, and +notebooks. All other relative paths (`nginx/`, the `../:/workspace` mount, and the +`../.env` `env_file`) are resolved relative to this directory. Always invoke Compose +from the repo root as shown above so these paths line up. diff --git a/docker-compose.nim.yml b/deploy/docker-compose.nim.yml similarity index 61% rename from docker-compose.nim.yml rename to deploy/docker-compose.nim.yml index 315d3a1d3..7cddd1bfe 100755 --- a/docker-compose.nim.yml +++ b/deploy/docker-compose.nim.yml @@ -21,13 +21,11 @@ services: # Route Variables - NGINX_UPSTREAM_NVAI=${NGINX_UPSTREAM_NVAI:-http://nim-llm:8000} - NGINX_UPSTREAM_NIM_LLM=${NGINX_UPSTREAM_NIM_LLM:-http://nim-llm:8000} - - NGINX_UPSTREAM_NIM_EMBED=${NGINX_UPSTREAM_NIM_EMBED:-http://nim-embed:8000} depends_on: - nim-llm - - nim-embed nim-llm: - image: ${NGC_NIM_LLM_CONTAINER:-nvcr.io/nim/meta/llama-3.1-70b-instruct:1.3.0} + image: ${NGC_NIM_LLM_CONTAINER:-nvcr.io/nim/nvidia/nemotron-3-super-120b-a12b:latest} # Increase the shared memory available to the container shm_size: 16G runtime: nvidia @@ -41,36 +39,13 @@ services: networks: - app_network environment: - - NGC_API_KEY=${NVIDIA_API_KEY:?"NVIDIA_API_KEY is required"} + - NGC_API_KEY=${NVIDIA_API_KEY:-} volumes: - nim-llm-cache:/opt/nim/.cache ports: - "8081:8000" - nim-embed: - image: ${NGC_NIM_EMBED_CONTAINER:-nvcr.io/nim/nvidia/nv-embedqa-e5-v5:1.0.1} - # Increase the shared memory available to the container - shm_size: 16G - runtime: nvidia - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: ${NIM_EMBED_GPU_COUNT:-1} - capabilities: [ gpu ] - networks: - - app_network - environment: - - NGC_API_KEY=${NVIDIA_API_KEY:?"NVIDIA_API_KEY is required"} - volumes: - - nim-embedding-cache:/opt/nim/.cache - ports: - - "8082:8000" - volumes: nim-llm-cache: driver: local - nim-embedding-cache: - driver: local diff --git a/docker-compose.yml b/deploy/docker-compose.yml similarity index 50% rename from docker-compose.yml rename to deploy/docker-compose.yml index 441c9f8fc..e14bc3ff2 100755 --- a/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -20,61 +20,42 @@ name: ${DOCKER_COMPOSE_PROJECT_NAME:-vuln_analysis} services: vuln-analysis: - image: vuln-analysis:2.1.0 + image: vuln-analysis:3.0.0 init: true build: - context: ./ - dockerfile: ./Dockerfile + # Build context is the repo root (one level up from deploy/) so the + # Dockerfile can COPY the package source, scripts, notebooks, etc. Paths below + # are resolved relative to this compose file's directory (deploy/). + context: ../ + dockerfile: ./deploy/Dockerfile target: runtime ports: - "${WORKFLOW_HOST_JUPYTER_PORT:-8000}:8000" - "${WORKFLOW_HOST_SCAN_PORT:-26466}:26466" - "${WORKFLOW_HOST_PHOENIX_PORT:-6006}:6006" + - "${WORKFLOW_HOST_DASK_PORT:-8787}:8787" working_dir: /workspace networks: - app_network + env_file: + - path: ../.env + required: false environment: - TERM=${TERM:-} - HF_HUB_CACHE=/workspace/.cache/huggingface - - XDG_CACHE_HOME=/workspace/.cache/am_cache # Allow logs to be written back to host + - XDG_CACHE_HOME=/workspace/.cache/vuln_analysis # Allow logs to be written back to host - # Required API Keys - - NVD_API_KEY=${NVD_API_KEY:?"NVD_API_KEY is required"} - - NVIDIA_API_KEY=${NVIDIA_API_KEY:?"NVIDIA_API_KEY is required"} - - SERPAPI_API_KEY=${SERPAPI_API_KEY:?"SERPAPI_API_KEY is required"} - # Optional API Keys - - GHSA_API_KEY=${GHSA_API_KEY:-""} - - NGC_API_KEY=${NGC_API_KEY:-""} - - NGC_ORG_ID=${NGC_ORG_ID:-""} - - OPENAI_API_KEY=${OPENAI_API_KEY:-""} - - # Base URLs for API endpoints - - CWE_DETAILS_BASE_URL=${CWE_DETAILS_BASE_URL:-http://nginx-cache/cwe-details} - - DEPSDEV_BASE_URL=${DEPSDEV_BASE_URL:-http://nginx-cache/depsdev} - - FIRST_BASE_URL=${FIRST_BASE_URL:-http://nginx-cache/first} - - GHSA_BASE_URL=${GHSA_BASE_URL:-http://nginx-cache/ghsa} - - NGC_API_BASE=${NGC_API_BASE:-http://nginx-cache/nemo/v1} - - NIM_EMBED_BASE_URL=${NIM_EMBED_BASE_URL:-http://nginx-cache/nim_embed/v1} - - NVD_BASE_URL=${NVD_BASE_URL:-http://nginx-cache/nvd} - - NVIDIA_API_BASE=${NVIDIA_API_BASE:-http://nginx-cache/nim_llm/v1} - - OPENAI_API_BASE=${OPENAI_API_BASE:-http://nginx-cache/openai/v1} # Used by `langchain` for embedding generation - - OPENAI_BASE_URL=${OPENAI_BASE_URL:-http://nginx-cache/openai/v1} # Used by `openai` for LLM inference - - RHSA_BASE_URL=${RHSA_BASE_URL:-http://nginx-cache/rhsa} - - SERPAPI_BASE_URL=${SERPAPI_BASE_URL:-http://nginx-cache/serpapi} - - UBUNTU_BASE_URL=${UBUNTU_BASE_URL:-http://nginx-cache/ubuntu} + # NAT / nim + nginx use NVIDIA_API_KEY. Use defaults (not :?) so `docker compose build` works without a .env; + # keys are still required at runtime (set in .env — loaded via env_file — or in the shell before `up`). + - NVIDIA_API_KEY=${NVIDIA_API_KEY:-} + - NV_BASE_URL=${NV_BASE_URL:-http://nginx-cache/nim_llm/v1} + - DEFAULT_MODEL_NAME=${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} - # Model names - - DEFAULT_MODEL_NAME=${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - CHECKLIST_MODEL_NAME=${CHECKLIST_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - CODE_VDB_RETRIEVER_MODEL_NAME=${CODE_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - DOC_VDB_RETRIEVER_MODEL_NAME=${DOC_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - CVE_AGENT_EXECUTOR_MODEL_NAME=${CVE_AGENT_EXECUTOR_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - SUMMARIZE_MODEL_NAME=${SUMMARIZE_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - JUSTIFY_MODEL_NAME=${JUSTIFY_MODEL_NAME:-meta/llama-3.1-70b-instruct} - - EMBEDDER_MODEL_NAME=${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} + - TAVILY_API_KEY=${TAVILY_API_KEY:-} + - TAVILY_API_BASE_URL=${TAVILY_API_BASE_URL:-http://nginx-cache/tavily} volumes: - - ./:/workspace - - /workspace/.venv + - ../:/workspace # Mount the repo root (one level up from deploy/) + - /workspace/.venv # Anonymous volume prevents host .venv from being mounted cap_add: - sys_nice depends_on: @@ -93,17 +74,14 @@ services: # Set custom ports in environment variables to avoid port collision - "${NGINX_HOST_HTTP_PORT:-8080}:80" environment: - # API Keys - - GHSA_API_KEY=${GHSA_API_KEY:-""} + # API Keys (substituted into nginx templates for proxied routes) - NGC_API_KEY=${NGC_API_KEY:-""} - NGC_ORG_ID=${NGC_ORG_ID:-""} - - NVD_API_KEY=${NVD_API_KEY:-""} - NVIDIA_API_KEY=${NVIDIA_API_KEY:-""} - OPENAI_API_KEY=${OPENAI_API_KEY:-""} - # Route Variables + # Route Variables (/nim_llm proxies OpenAI-compatible inference) - NGINX_UPSTREAM_NVAI=${NGINX_UPSTREAM_NVAI:-https://api.nvcf.nvidia.com} - - NGINX_UPSTREAM_NIM_LLM=${NGINX_UPSTREAM_NIM_LLM:-https://integrate.api.nvidia.com} - - NGINX_UPSTREAM_NIM_EMBED=${NGINX_UPSTREAM_NIM_EMBED:-https://integrate.api.nvidia.com} + - NGINX_UPSTREAM_NIM_LLM=${NGINX_UPSTREAM_NIM_LLM:-https://inference-api.nvidia.com} networks: - app_network restart: always diff --git a/nginx/.gitignore b/deploy/nginx/.gitignore similarity index 100% rename from nginx/.gitignore rename to deploy/nginx/.gitignore diff --git a/nginx/nginx_cache.conf b/deploy/nginx/nginx_cache.conf similarity index 93% rename from nginx/nginx_cache.conf rename to deploy/nginx/nginx_cache.conf index 911f3d09a..0b4e48387 100644 --- a/nginx/nginx_cache.conf +++ b/deploy/nginx/nginx_cache.conf @@ -18,13 +18,13 @@ http { '"$http_referer" "$http_user_agent"' 'rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"'; - log_format cache_log '[$time_local] ($upstream_cache_status) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests. Auth: $http_authorization'; + log_format cache_log '[$time_local] ($upstream_cache_status) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests'; - log_format no_cache_log '[$time_local] (BYPASSED) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests. Auth: $http_authorization'; + log_format no_cache_log '[$time_local] (BYPASSED) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests'; - log_format mirror_log '[$time_local] (MIRROR) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests. Auth: $http_authorization'; + log_format mirror_log '[$time_local] (MIRROR) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests'; - log_format nvai_cache_log '[$time_local] ($upstream_cache_status) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests. Auth: $http_authorization. Final Auth: $nvai_http_authorization'; + log_format nvai_cache_log '[$time_local] ($upstream_cache_status) "$request" $status - $body_bytes_sent bytes {$remote_addr} "$http_user_agent" $request_time - $connection_requests'; include /etc/nginx/conf.d/variables/*.conf; diff --git a/nginx/nginx_ssl.conf b/deploy/nginx/nginx_ssl.conf similarity index 100% rename from nginx/nginx_ssl.conf rename to deploy/nginx/nginx_ssl.conf diff --git a/nginx/templates/routes/intel.conf.template b/deploy/nginx/templates/routes/intel.conf.template similarity index 70% rename from nginx/templates/routes/intel.conf.template rename to deploy/nginx/templates/routes/intel.conf.template index 739c1ac69..3f7eb62f6 100644 --- a/nginx/templates/routes/intel.conf.template +++ b/deploy/nginx/templates/routes/intel.conf.template @@ -1,26 +1,16 @@ ####################### Intel APIs ####################### -set $serpapi_upstream https://serpapi.com; +set $tavily_upstream https://api.tavily.com; -location /serpapi { - rewrite ^\/serpapi(\/.*)$ $1 break; - proxy_pass $serpapi_upstream; +location /tavily { + rewrite ^\/tavily(\/.*)$ $1 break; + proxy_pass $tavily_upstream; + proxy_set_header Connection ''; + proxy_ssl_server_name on; proxy_cache intel_cache; - proxy_cache_methods GET; - proxy_cache_key "$request_method|$request_uri"; - add_header X-Cache-Status $upstream_cache_status; - client_body_buffer_size 4m; -} - -set $nvd_upstream https://services.nvd.nist.gov; - -location /nvd { - rewrite ^\/nvd(\/.*)$ /rest$1 break; - proxy_pass $nvd_upstream; - proxy_set_header apiKey $nvd_http_api_key; - proxy_cache intel_cache; - proxy_cache_methods GET; - proxy_cache_key "$request_method|$request_uri"; + proxy_cache_methods GET HEAD POST; + proxy_cache_key "$request_method|$request_uri|$request_body|$http_authorization"; + proxy_cache_valid 200 7d; add_header X-Cache-Status $upstream_cache_status; client_body_buffer_size 4m; } @@ -63,18 +53,6 @@ location /recf { client_body_buffer_size 4m; } -set $ghsa_upstream https://api.github.com; - -location /ghsa { - rewrite ^\/ghsa(\/.*)$ $1 break; - proxy_pass $ghsa_upstream; - proxy_cache intel_cache; - proxy_cache_methods GET; - proxy_cache_key "$request_method|$request_uri"; - add_header X-Cache-Status $upstream_cache_status; - client_body_buffer_size 4m; -} - set $rhsa_upstream https://access.redhat.com; location /rhsa { diff --git a/nginx/templates/routes/nemo.conf.template b/deploy/nginx/templates/routes/nemo.conf.template similarity index 100% rename from nginx/templates/routes/nemo.conf.template rename to deploy/nginx/templates/routes/nemo.conf.template diff --git a/nginx/templates/routes/nim.conf.template b/deploy/nginx/templates/routes/nim.conf.template similarity index 50% rename from nginx/templates/routes/nim.conf.template rename to deploy/nginx/templates/routes/nim.conf.template index b04c02cd4..dcc15d3a2 100644 --- a/nginx/templates/routes/nim.conf.template +++ b/deploy/nginx/templates/routes/nim.conf.template @@ -1,6 +1,5 @@ set $upstream_nim_llm ${NGINX_UPSTREAM_NIM_LLM}; -set $upstream_nim_embed ${NGINX_UPSTREAM_NIM_EMBED}; location /nim_llm { location ~* ^\/nim_llm\/v1\/(?:chat\/completions|completions|edits|moderations|answers)$ { @@ -22,24 +21,3 @@ location /nim_llm { access_log /var/log/nginx/access.log no_cache_log; } } - -location /nim_embed { - location ~* ^\/nim_embed\/v1\/(?:embeddings)$ { - rewrite ^\/nim_embed(\/.*)$ $1 break; - proxy_pass $upstream_nim_embed; - proxy_set_header Connection ''; - proxy_set_header Authorization $nim_http_authorization; - proxy_cache llm_cache; - proxy_cache_methods GET POST; - proxy_cache_key "$request_method|$request_uri|$request_body"; - add_header X-Cache-Status $upstream_cache_status; - client_body_buffer_size 4m; - } - - location /nim_embed/v1 { - rewrite ^\/nim_embed(\/.*)$ $1 break; - proxy_pass $upstream_nim_embed; - access_log /dev/stdout no_cache_log; - access_log /var/log/nginx/access.log no_cache_log; - } -} diff --git a/nginx/templates/routes/nvidia.conf.template b/deploy/nginx/templates/routes/nvidia.conf.template similarity index 100% rename from nginx/templates/routes/nvidia.conf.template rename to deploy/nginx/templates/routes/nvidia.conf.template diff --git a/nginx/templates/routes/openai.conf.template b/deploy/nginx/templates/routes/openai.conf.template similarity index 100% rename from nginx/templates/routes/openai.conf.template rename to deploy/nginx/templates/routes/openai.conf.template diff --git a/nginx/templates/variables/template-variables.conf.template b/deploy/nginx/templates/variables/template-variables.conf.template similarity index 79% rename from nginx/templates/variables/template-variables.conf.template rename to deploy/nginx/templates/variables/template-variables.conf.template index e2f122c52..a66dd06ea 100644 --- a/nginx/templates/variables/template-variables.conf.template +++ b/deploy/nginx/templates/variables/template-variables.conf.template @@ -39,16 +39,3 @@ map $http_authorization $openai_http_authorization { 'Bearer "CYBER_DEV_DAY"' 'Bearer ${OPENAI_API_KEY}'; default $http_authorization; } - -map $http_authorization $ghsa_http_authorization { - 'Bearer AGENT_MORPHEUS' 'Bearer ${GHSA_API_KEY}'; - 'Bearer "AGENT_MORPHEUS"' 'Bearer ${GHSA_API_KEY}'; - 'Bearer CYBER_DEV_DAY' 'Bearer ${GHSA_API_KEY}'; - 'Bearer "CYBER_DEV_DAY"' 'Bearer ${GHSA_API_KEY}'; - default $http_authorization; -} - -map $http_apikey $nvd_http_api_key { - 'AGENT_MORPHEUS' '${NVD_API_KEY}'; - default $http_apikey; -} diff --git a/images/pipeline_orchestrator.png b/images/pipeline_orchestrator.png new file mode 100644 index 000000000..938d414c2 --- /dev/null +++ b/images/pipeline_orchestrator.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b917698bbdff5bd424bf316e759ab720dae8dec9e5801b5d3c0af7ce21019258 +size 1356352 diff --git a/pyproject.toml b/pyproject.toml index 610ff8903..489348e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,37 +10,36 @@ name = "vuln_analysis" dynamic = ["version"] dependencies = [ "aiofiles", - "aiohttp-client-cache==0.11", "aiolimiter~=1.2", - "aioresponses==0.7.6", - "arize-phoenix", # Install phoenix server, which is not included in nvidia-nat phoenix telemetry requirements - "brotli", - "esprima", - "faiss-cpu==1.9.0", + "aiq-agent", + "arize-phoenix", + "bokeh>=3.1.0", + "deepagents>=0.6.4,<0.7", "gitpython", - "google-search-results==2.4", - "json5", - "langchain>=0.3,<1.0", - "langchain-core>=0.3.80,<0.4.0", - "mcp>=1.0,<1.20", # Avoid CVE-2025-45768: pyjwt added in 1.20.0. Remove when pyjwt CVE fix is released. - "nbformat", - "nemollm", - "nvidia-nat[async_endpoints,langchain,phoenix,profiling]~=1.3.0", - "openinference-instrumentation-langchain~=0.1.31", - "ordered_set", + "httpx>=0.27", + "langchain", + "langchain-core", + # >=1.4.1: 1.4.0's ChatNVIDIA leaks the `verify_ssl` client option into the + # request body (it pops verify_ssl only after super().__init__, so it lands in + # model_kwargs). Under `nat serve` the config round-trip marks verify_ssl as + # set, so it gets forwarded and the inference gateway rejects it. Fixed in 1.4.1. + "langchain-nvidia-ai-endpoints>=1.4.1,<2.0.0", + "langgraph>=1.0.5,<2.0.0", + "filelock", + "nvidia-nat[async_endpoints,langchain,phoenix,profiler,eval]==1.7.0", + "openinference-instrumentation-langchain", + "packaging>=24.0", "pydpkg==1.9.4", - "rank_bm25==0.2.2", - "tantivy==0.22.2", - "tree-sitter==0.21.3", - "tree-sitter-languages==1.10.2", - "univers==30.12" + "tavily-python>=0.5.0", + "univers==30.12", ] -requires-python = ">=3.11,<3.13" +requires-python = ">=3.13,<3.14" description = "NVIDIA AI Blueprint: Vulnerability Analysis for Container Security" classifiers = ["Programming Language :: Python"] -[project.entry-points.'nat.components'] +[project.entry-points."nat.plugins"] vuln_analysis = "vuln_analysis.register" +vuln_analysis_cve_researcher = "vuln_analysis.plugins.cve_researcher.register" [dependency-groups] # Dependency groups are only for developers to aid in managing dependencies local to a dev machine. @@ -784,3 +783,7 @@ reportInvalidTypeForm = "warning" [tool.uv] managed = true + +[tool.uv.sources] +aiq-agent = { path = "aiq", editable = true } +knowledge-layer = { path = "aiq/sources/knowledge_layer", editable = true } diff --git a/quick_start/quick_start_guide.ipynb b/quick_start/quick_start_guide.ipynb index fc9963c0e..7d125813c 100644 --- a/quick_start/quick_start_guide.ipynb +++ b/quick_start/quick_start_guide.ipynb @@ -1,4830 +1,668 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Overview\n", - "This notebook contains a quick start guide for the NIM Agent Blueprint: Vulnerability Analysis for Container Security. By the end of the notebook, you will be able to run the blueprint on an example input, and view the results.\n", - "\n", - "For full instructions on running and customizing the blueprint, please see the [README](../README.md).\n", - "\n", - "## Prerequisites\n", - "* [Build and launch Vulnerability Analysis container](../README.md#getting-started)\n", - " + Be sure to keep the container bash shell handy for launching the HTTP server\n", - "* From the host machine (outside the container), open this notebook in a [Jupyter server](https://docs.jupyter.org/en/latest/running.html), or from an IDE that supports notebook execution, such as [VS Code](https://code.visualstudio.com/docs/datascience/jupyter-notebooks).\n", - " + A running Jupyter server is included with the container by default. Instructions to connect can be found in this README section: [From the quick start user guide notebook](../README.md#from-the-quick-start-user-guide-notebook).\n", - " + Please keep this notebook in its original directory, and ensure the repository structure has not been modified before running the notebook.\n", - "* [Install Syft](https://github.com/anchore/syft) (optional prerequisite for cell 4, Generating a custom SBOM)\n", - "\n", - "## Other notes\n", - "* All files and directories in the notebook are relative to the repository root.\n", - "\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import os\n", - "import json\n", - "import pandas as pd\n", - "import re\n", - "import requests\n", - "import yaml" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Change the notebook's working directory to the repository root. All the directory and file paths below will now be relative to `REPO_ROOT`." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "REPO_ROOT = !echo $(git rev-parse --show-toplevel)\n", - "os.chdir(REPO_ROOT[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Preparing the workflow" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Select container and obtain SBOM" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using pre-generated SBOM" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sbom_file = \"data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Generating a custom SBOM\n", - "\n", - "Be aware that depending on the size of the container, the sbom generation process can take several minutes to complete." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# import os\n", - "# import subprocess\n", - "\n", - "# image_url = \"nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime\"\n", - "# sbom_file = \".tmp/morpheus231101.sbom\"\n", - "# if not os.path.exists(sbom_file):\n", - "# cmd = ['syft', image_url, '-o', 'syft-table='+sbom_file]\n", - "# sp = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=False)\n", - "# (out, err) = sp.communicate()\n", - "# print(out)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Load up example workflow config" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "tags": [] - }, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "general:\n", - " use_uvloop: true\n", - "functions:\n", - " cve_generate_vdbs:\n", - " _type: cve_generate_vdbs\n", - " agent_name: cve_agent_executor\n", - " embedder_name: nim_embedder\n", - " base_git_dir: .cache/am_cache/git\n", - " base_vdb_dir: .cache/am_cache/vdb\n", - " base_code_index_dir: .cache/am_cache/code_index\n", - " cve_fetch_intel:\n", - " _type: cve_fetch_intel\n", - " cve_process_sbom:\n", - " _type: cve_process_sbom\n", - " cve_check_vuln_deps:\n", - " _type: cve_check_vuln_deps\n", - " cve_checklist:\n", - " _type: cve_checklist\n", - " llm_name: checklist_llm\n", - " Container Image Code QA System:\n", - " _type: local_vdb_retriever\n", - " embedder_name: nim_embedder\n", - " llm_name: code_vdb_retriever_llm\n", - " vdb_type: code\n", - " return_source_documents: false\n", - " Container Image Developer Guide QA System:\n", - " _type: local_vdb_retriever\n", - " embedder_name: nim_embedder\n", - " llm_name: doc_vdb_retriever_llm\n", - " vdb_type: doc\n", - " return_source_documents: false\n", - " Lexical Search Container Image Code QA System:\n", - " _type: lexical_code_search\n", - " top_k: 5\n", - " Internet Search:\n", - " _type: serp_wrapper\n", - " max_retries: 5\n", - " cve_agent_executor:\n", - " _type: cve_agent_executor\n", - " llm_name: cve_agent_executor_llm\n", - " tool_names:\n", - " - Container Image Code QA System\n", - " - Container Image Developer Guide QA System\n", - " - Internet Search\n", - " max_concurrency: null\n", - " max_iterations: 10\n", - " prompt_examples: false\n", - " replace_exceptions: true\n", - " replace_exceptions_value: I do not have a definitive answer for this checklist\n", - " item.\n", - " return_intermediate_steps: false\n", - " verbose: false\n", - " cve_summarize:\n", - " _type: cve_summarize\n", - " llm_name: summarize_llm\n", - " cve_justify:\n", - " _type: cve_justify\n", - " llm_name: justify_llm\n", - " cve_file_output:\n", - " _type: cve_file_output\n", - " file_path: .tmp/output.json\n", - " markdown_dir: .tmp/vulnerability_markdown_reports\n", - " overwrite: true\n", - "llms:\n", - " checklist_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${CHECKLIST_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 2000\n", - " top_p: 0.01\n", - " code_vdb_retriever_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${CODE_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 2000\n", - " top_p: 0.01\n", - " doc_vdb_retriever_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${DOC_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 2000\n", - " top_p: 0.01\n", - " cve_agent_executor_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${CVE_AGENT_EXECUTOR_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 2000\n", - " top_p: 0.01\n", - " summarize_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${SUMMARIZE_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 1024\n", - " top_p: 0.01\n", - " justify_llm:\n", - " _type: nim\n", - " base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${JUSTIFY_MODEL_NAME:-meta/llama-3.1-70b-instruct}\n", - " temperature: 0.0\n", - " max_tokens: 1024\n", - " top_p: 0.01\n", - "embedders:\n", - " nim_embedder:\n", - " _type: nim\n", - " base_url: ${NIM_EMBED_BASE_URL:-https://integrate.api.nvidia.com/v1}\n", - " model_name: ${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5}\n", - " truncate: END\n", - " max_batch_size: 128\n", - "workflow:\n", - " _type: cve_agent\n", - " cve_generate_vdbs_name: cve_generate_vdbs\n", - " cve_fetch_intel_name: cve_fetch_intel\n", - " cve_process_sbom_name: cve_process_sbom\n", - " cve_check_vuln_deps_name: cve_check_vuln_deps\n", - " cve_checklist_name: cve_checklist\n", - " cve_agent_executor_name: cve_agent_executor\n", - " cve_summarize_name: cve_summarize\n", - " cve_justify_name: cve_justify\n", - " cve_output_config_name: cve_file_output\n", - "eval:\n", - " general:\n", - " output_dir: ./.tmp/eval/cve_agent\n", - " dataset:\n", - " _type: json\n", - " file_path: data/eval_datasets/eval_dataset.json\n", - " profiler:\n", - " token_uniqueness_forecast: true\n", - " workflow_runtime_forecast: true\n", - " compute_llm_metrics: true\n", - " csv_exclude_io_text: true\n", - " prompt_caching_prefixes:\n", - " enable: true\n", - " min_frequency: 0.1\n", - " bottleneck_analysis:\n", - " enable_nested_stack: true\n", - " concurrency_spike_analysis:\n", - " enable: true\n", - " spike_threshold: 7\n", - "\n" - ] - } - ], - "source": [ - "with open('configs/config.yml', 'r') as config_in:\n", - " workflow_config = yaml.safe_load(config_in)\n", - "print(yaml.dump(workflow_config, indent=2, sort_keys=False))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuring the workflow output" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "output_file_path = \".tmp/quick_start_results/output.json\"\n", - "markdown_dir = \".tmp/quick_start_results/vulnerability_markdown_reports\"\n", - "\n", - "workflow_config['functions']['cve_file_output']['file_path'] = output_file_path\n", - "workflow_config['functions']['cve_file_output']['markdown_dir'] = markdown_dir\n", - "\n", - "# Save the updated workflow configuration file to the .tmp/ directory\n", - "config_path = \".tmp/config.yml\"\n", - "os.makedirs(os.path.dirname(config_path), exist_ok=True)\n", - "with open(config_path, \"w\") as config_out:\n", - " yaml.dump(workflow_config, config_out, indent=2, sort_keys=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Starting the HTTP server" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Pull up the bash shell for the container and run the following command:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```bash\n", - "nat serve --config_file=.tmp/config.yml --host 0.0.0.0 --port 26466\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "Once you see the following, HTTP requests can be sent to the server.\n", - "```\n", - "INFO: Application startup complete.\n", - "INFO: Uvicorn running on http://0.0.0.0:26466 (Press CTRL+C to quit)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Sending Workflow Requests" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup input message for HTTP server" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "tags": [] - }, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quick Start Guide v3\n", + "\n", + "This notebook runs the current vulnerability analysis workflow from the repository README: the NeMo Agent Toolkit `cve_pipeline` graph. The v3 flow analyzes one or more CVE or GHSA identifiers against one container image, then writes per-agent Markdown reports and final VEX JSON classifications.\n", + "\n", + "The main change from the older quick start is the input model. The pipeline now consumes a `PipelineInput` JSON document and no longer needs an SBOM. The recommended path is to provide `vulns`, `image_name`, and `image_tag`; when `filesystem_path` is omitted, the orchestrator pulls `image_name:image_tag`, extracts a flattened rootfs via `crane`, and reuses the image cache on later runs. You can still provide a pre-extracted `filesystem_path` when you want to manage extraction yourself.\n", + "\n", + "For deeper usage details, see the [README](../README.md).\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"image\": {\n", - " \"name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", - " \"tag\": \"23.11-runtime\",\n", - " \"source_info\": [\n", - " {\n", - " \"type\": \"code\",\n", - " \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\",\n", - " \"ref\": \"v23.11.01\",\n", - " \"include\": [\n", - " \"**/*.cpp\",\n", - " \"**/*.cu\",\n", - " \"**/*.cuh\",\n", - " \"**/*.h\",\n", - " \"**/*.hpp\",\n", - " \"**/*.ipynb\",\n", - " \"**/*.py\",\n", - " \"**/*Dockerfile\"\n", - " ],\n", - " \"exclude\": [\n", - " \"tests/**/*\"\n", - " ]\n", - " },\n", - " {\n", - " \"type\": \"doc\",\n", - " \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\",\n", - " \"ref\": \"v23.11.01\",\n", - " \"include\": [\n", - " \"**/*.md\",\n", - " \"docs/**/*.rst\"\n", - " ]\n", - " }\n", - " ],\n", - " \"sbom_info\": {\n", - " \"_type\": \"file\",\n", - " \"file_path\": \"data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom\"\n", - " }\n", - " },\n", - " \"scan\": {\n", - " \"vulns\": [\n", - " {\n", - " \"vuln_id\": \"GHSA-3f63-hfp8-52jq\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"CVE-2023-50782\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"CVE-2023-36632\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"CVE-2023-43804\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-cxfr-5q3r-2rc2\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-554w-xh4j-8w64\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-3ww4-gg4f-jr7f\"\n", - " }\n", - " ]\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "with open('data/input_messages/morpheus:23.11-runtime.json', 'r') as input_message_in:\n", - " input_message = json.load(input_message_in)\n", - "print(json.dumps(input_message, indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Set up source code and documentation for vector database" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "source_code_repo = \"https://github.com/nv-morpheus/Morpheus.git\"\n", - "source_code_tag = \"v23.11.01\"\n", - "doc_repo = \"https://github.com/nv-morpheus/Morpheus.git\"\n", - "doc_tag = \"v23.11.01\"\n", - "\n", - "input_message[\"image\"][\"source_info\"] = [\n", - " {\n", - " \"type\": \"code\",\n", - " \"git_repo\": source_code_repo,\n", - " \"ref\": source_code_tag,\n", - " \"include\": [\"**/*.cpp\", \"**/*.cu\", \"**/*.cuh\", \"**/*.h\", \"**/*.hpp\", \"**/*.ipynb\", \"**/*.py\", \"**/*Dockerfile\"],\n", - " \"exclude\": [\"tests/**/*\"]\n", - " },\n", - " {\n", - " \"type\": \"doc\", \"git_repo\": doc_repo, \"ref\": doc_tag, \"include\": [\"**/*.md\", \"docs/**/*.rst\"]\n", - " },\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Set up SBOM and list of vulnerabilities to analyze" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "input_message[\"image\"][\"sbom_info\"][\"file_path\"] = sbom_file\n", - "\n", - "vuln_list = [\n", - " \"CVE-2024-21762\", # irrelevant to the Morpheus container\n", - " \"GHSA-hh8p-p8mp-gqhm\", # vulnerable package in use\n", - " \"GHSA-3f63-hfp8-52jq\", # vulnerable code included in the environment but not executed during runtime\n", - "]\n", - "vuln_entry = [{\"vuln_id\": vuln} for vuln in vuln_list]\n", - "input_message[\"scan\"][\"vulns\"] = vuln_entry" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "tags": [] - }, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Before running the workflow cells, make sure the project is installed and configured:\n", + "\n", + "- Python 3.13 and project dependencies installed with `uv sync`\n", + "- `NVIDIA_API_KEY` exported for the NAT `nim` LLM configuration\n", + "- `TAVILY_API_KEY` exported for web search\n", + "- `crane` installed if you use automatic image extraction, the default path\n", + "- Optional registry auth in `~/.docker/config.json`, using `crane auth login ` when needed\n", + "\n", + "Set `image_name`, `image_tag`, and `vuln_ids` in the cells below for the container and vulnerabilities you want to analyze.\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"image\": {\n", - " \"name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", - " \"tag\": \"23.11-runtime\",\n", - " \"source_info\": [\n", - " {\n", - " \"type\": \"code\",\n", - " \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\",\n", - " \"ref\": \"v23.11.01\",\n", - " \"include\": [\n", - " \"**/*.cpp\",\n", - " \"**/*.cu\",\n", - " \"**/*.cuh\",\n", - " \"**/*.h\",\n", - " \"**/*.hpp\",\n", - " \"**/*.ipynb\",\n", - " \"**/*.py\",\n", - " \"**/*Dockerfile\"\n", - " ],\n", - " \"exclude\": [\n", - " \"tests/**/*\"\n", - " ]\n", - " },\n", - " {\n", - " \"type\": \"doc\",\n", - " \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\",\n", - " \"ref\": \"v23.11.01\",\n", - " \"include\": [\n", - " \"**/*.md\",\n", - " \"docs/**/*.rst\"\n", - " ]\n", - " }\n", - " ],\n", - " \"sbom_info\": {\n", - " \"_type\": \"file\",\n", - " \"file_path\": \"data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom\"\n", - " }\n", - " },\n", - " \"scan\": {\n", - " \"vulns\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2024-21762\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-hh8p-p8mp-gqhm\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-3f63-hfp8-52jq\"\n", - " }\n", - " ]\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "print(json.dumps(input_message, indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This command sends a request with the input message to the running HTTP server. You should be able to see the logs from the workflow in the console." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "tags": [] - }, - "outputs": [ + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import json\n", + "import os\n", + "import re\n", + "import shlex\n", + "import shutil\n", + "import subprocess\n", + "from pathlib import Path\n", + "\n", + "import pandas as pd\n", + "from IPython.display import Markdown, display\n" + ] + }, { - "data": { - "text/plain": [ - "" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set Repository Root\n", + "\n", + "Change the notebook working directory to the repository root. All relative paths below are resolved from `REPO_ROOT`.\n" ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "requests.post(f\"http://localhost:26466/generate\", json=input_message)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An overview of each provided CVE's affected status and vulnerability label will be printed to the logs upon completion of the workflow, for example:\n", - "\n", - "```\n", - "Vulnerability 'CVE-2024-21762' affected status: FALSE. Label: code_not_present\n", - "Vulnerability 'GHSA-hh8p-p8mp-gqhm' affected status: TRUE. Label: vulnerable\n", - "Vulnerability 'GHSA-3f63-hfp8-52jq' affected status: FALSE. Label: code_not_reachable\n", - "```\n", - "\n", - "
\n", - " Note: The output you receive from the workflow may not be identical as the output in the example above. The output may vary due to the non-deterministic nature of the LLM models.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Additional inputs can be sent to the HTTP server and the results will be appended to the output file until the server is shut down.\n", - "\n", - "
\n", - " Note: Please wait until the workflow execution is finished before proceeding to the next section.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Processing Results\n", - "## Output schema\n", - "### 1. `input`\n", - "This section captures the details about the target of the vulnerability investigation. It includes subsections for the security scan and information on the scanned container image. It is structured as follows:\n", - "\n", - "- `scan`: Details about the security scan and the vulnerabilities discovered for the system to investigate. Many of the fields are null in illustrative example input. These fields can be populated with security scan metadata when integrating scan results as the HTTP input source.\n", - " - `id`: ID for the scan.\n", - " - `type`: Scan type.\n", - " - `started_at` and `completed_at`: Timestamps indicating scan start and end.\n", - " - `vulns`: Array of detected vulnerabilities, each described with details including `vuln_id`, severity, description, and package-specific data.\n", - "\n", - "- `image`: Specifies the container image analyzed in this scan. This information is directly parsed from the input message we posted.\n", - "\n", - "### 2. `info`\n", - "This section provides metadata and intel on vulnerabilities gathered from threat databases and advisories.\n", - "\n", - "- `vdb`: Paths to vulnerability vector databases used for code (`code_vdb_path`) and documentation (`doc_vdb_path`) relevant to the container.\n", - "\n", - "- `intel`: Vulnerability intel that groups vulnerabilities by their IDs.\n", - " - `vuln_id`: Identifier for each vulnerability.\n", - " - `ghsa`, `nvd`, `rhsa`, `ubuntu`: Subsections providing detailed advisories from different vulnerability databases:\n", - " - Details like `cve_id`, `severity`, `summary`, `description`, `cwes` explain the nature of the vulnerability and provide information on affected versions, available patches, publication dates, severity, and other relevant details.\n", - " - Depending on the intel source, information about specific configurations or environments where the vulnerabilities may occur could also be included.\n", - "\n", - "- `sbom`(Software Bill of Materials): Lists packages used in the container.\n", - " - Each package contains fields like `name`, `version`, `path`, and `system`.\n", - "\n", - "- `vulnerable_dependencies`: Lists dependencies in the container that match identified vulnerabilities.\n", - " - For each vulnerability (`vuln_id`), this section lists vulnerable SBOM packages, identifying any affected packages and their versions.\n", - "\n", - "### 3. `output`\n", - "The output section captures the results of the vulnerability investigation from the agentic system.\n", - "\n", - "- `vuln_id`: Identifier for each vulnerability.\n", - "- `checklist`: Detailed investigative steps and results for investigating each vulnerability's exploitability.\n", - " - `input`: The checklist item for the agent to investigate.\n", - " - `response`: The agent's finding and conclusion for the checklist item.\n", - " - `intermediate_steps`: (Optional) Additional information on the intermediate actions taken during the investigation by the agent. Useful for debugging and explainability.\n", - "- `summary`: A summary of the investigation, indicating whether the vulnerability is exploitable or not, based on checklist findings.\n", - "- `justification`: A detailed conclusion on the vulnerability's exploitability status, including:\n", - " - `label`: A VEX justification label for the vulnerability.\n", - " - `reason`: Explanation for the decision of the classification.\n", - " - `status`: Final boolean decision on whether the vulnerability is currently exploitable.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Load results into the notebook" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "#Load first result in case multiple requests were sent\n", - "with open(output_file_path, 'r') as result_in:\n", - " full_input = result_in.read()\n", - " offsets = [res.start() for res in re.finditer('{\"input\":{\"scan\":', full_input)]\n", - " offsets.append(len(full_input))\n", - " first_result = json.loads(full_input[offsets[0]:offsets[1]])" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "tags": [] - }, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"input\": {\n", - " \"scan\": {\n", - " \"id\": \"4fff5361-9706-41ad-8f0a-15730cc1d300\",\n", - " \"type\": null,\n", - " \"started_at\": \"2025-06-07T19:37:22.118544\",\n", - " \"completed_at\": \"2025-06-07T19:37:27.757216\",\n", - " \"vulns\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2024-21762\",\n", - " \"description\": null,\n", - " \"score\": null,\n", - " \"severity\": null,\n", - " \"published_date\": null,\n", - " \"last_modified_date\": null,\n", - " \"url\": null,\n", - " \"feed_group\": null,\n", - " \"package\": null,\n", - " \"package_version\": null,\n", - " \"package_name\": null,\n", - " \"package_type\": null\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-hh8p-p8mp-gqhm\",\n", - " \"description\": null,\n", - " \"score\": null,\n", - " \"severity\": null,\n", - " \"published_date\": null,\n", - " \"last_modified_date\": null,\n", - " \"url\": null,\n", - " \"feed_group\": null,\n", - " \"package\": null,\n", - " \"package_version\": null,\n", - " \"package_name\": null,\n", - " \"package_type\": null\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-3f63-hfp8-52jq\",\n", - " \"description\": null,\n", - " \"score\": null,\n", - " \"severity\": null,\n", - " \"published_date\": null,\n", - " \"last_modified_date\": null,\n", - " \"url\": null,\n", - " \"feed_group\": null,\n", - " \"package\": null,\n", - " \"package_version\": null,\n", - " \"package_name\": null,\n", - " \"package_type\": null\n", - " }\n", - " ]\n", - " },\n", - " \"image\": {\n", - " \"name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", - " \"tag\": \"23.11-runtime\",\n", - " \"digest\": null,\n", - " \"platform\": null,\n", - " \"feed_group\": null,\n", - " \"source_info\": [\n", - " {\n", - " \"type\": \"code\",\n", - " \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\",\n", - " \"ref\": \"eeebf55da604c16e6de3b060cbae6ad3172fdd99\",\n", - " \"include\": [\n", - " \"**/*.cpp\",\n", - " \"**/*.cu\",\n", - " \"**/*.cuh\",\n", - " \"**/*.h\",\n", - " \"**/*.hpp\",\n", - " \"**/*.ipynb\",\n", - " \"**/*.py\",\n", - " \"**/*Dockerfile\"\n", - " ],\n", - " \"exclude\": [\n", - " \"tests/**/*\"\n", - " ]\n", - " },\n", - " {\n", - " \"type\": \"doc\",\n", - " \"git_repo\": \"https://github.com/nv-morpheus/Morpheus.git\",\n", - " \"ref\": \"eeebf55da604c16e6de3b060cbae6ad3172fdd99\",\n", - " \"include\": [\n", - " \"**/*.md\",\n", - " \"docs/**/*.rst\"\n", - " ],\n", - " \"exclude\": []\n", - " }\n", - " ],\n", - " \"sbom_info\": {\n", - " \"_type\": \"file\",\n", - " \"file_path\": \"data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom\"\n", - " }\n", - " }\n", - " },\n", - " \"info\": {\n", - " \"vdb\": {\n", - " \"code_vdb_path\": \".cache/am_cache/vdb/code/ccf658f72228c497\",\n", - " \"doc_vdb_path\": \".cache/am_cache/vdb/doc/c89fe6d707036daa\",\n", - " \"code_index_path\": null\n", - " },\n", - " \"intel\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2024-21762\",\n", - " \"ghsa\": {\n", - " \"ghsa_id\": \"GHSA-v4hq-m4wr-6pmj\",\n", - " \"cve_id\": \"CVE-2024-21762\",\n", - " \"summary\": \"A out-of-bounds write in Fortinet FortiOS versions 7.4.0 through 7.4.2, 7.2.0 through 7.2.6, 7.0...\",\n", - " \"description\": \"A out-of-bounds write in Fortinet FortiOS versions 7.4.0 through 7.4.2, 7.2.0 through 7.2.6, 7.0.0 through 7.0.13, 6.4.0 through 6.4.14, 6.2.0 through 6.2.15, 6.0.0 through 6.0.17, FortiProxy versions 7.4.0 through 7.4.2, 7.2.0 through 7.2.8, 7.0.0 through 7.0.14, 2.0.0 through 2.0.13, 1.2.0 through 1.2.13, 1.1.0 through 1.1.6, 1.0.0 through 1.0.7 allows attacker to execute unauthorized code or commands via specifically crafted requests\",\n", - " \"severity\": \"critical\",\n", - " \"vulnerabilities\": [],\n", - " \"cvss\": {\n", - " \"score\": 9.8,\n", - " \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\"\n", - " },\n", - " \"cwes\": [\n", - " {\n", - " \"cwe_id\": \"CWE-787\",\n", - " \"name\": \"Out-of-bounds Write\"\n", - " }\n", - " ],\n", - " \"published_at\": \"2024-02-09T09:31:31Z\",\n", - " \"updated_at\": \"2024-02-09T09:31:40Z\",\n", - " \"url\": \"https://api.github.com/advisories/GHSA-v4hq-m4wr-6pmj\",\n", - " \"html_url\": \"https://github.com/advisories/GHSA-v4hq-m4wr-6pmj\",\n", - " \"type\": \"unreviewed\",\n", - " \"repository_advisory_url\": null,\n", - " \"source_code_location\": \"\",\n", - " \"identifiers\": [\n", - " {\n", - " \"value\": \"GHSA-v4hq-m4wr-6pmj\",\n", - " \"type\": \"GHSA\"\n", - " },\n", - " {\n", - " \"value\": \"CVE-2024-21762\",\n", - " \"type\": \"CVE\"\n", - " }\n", - " ],\n", - " \"references\": [\n", - " \"https://nvd.nist.gov/vuln/detail/CVE-2024-21762\",\n", - " \"https://fortiguard.com/psirt/FG-IR-24-015\",\n", - " \"https://github.com/advisories/GHSA-v4hq-m4wr-6pmj\"\n", - " ],\n", - " \"github_reviewed_at\": null,\n", - " \"nvd_published_at\": \"2024-02-09T09:15:08Z\",\n", - " \"withdrawn_at\": null,\n", - " \"cvss_severities\": {\n", - " \"cvss_v3\": {\n", - " \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"score\": 9.8\n", - " },\n", - " \"cvss_v4\": {\n", - " \"vector_string\": null,\n", - " \"score\": 0.0\n", - " }\n", - " },\n", - " \"credits\": [],\n", - " \"epss\": {\n", - " \"percentage\": 0.92448,\n", - " \"percentile\": 0.99718\n", - " }\n", - " },\n", - " \"nvd\": {\n", - " \"cve_id\": \"CVE-2024-21762\",\n", - " \"cve_description\": \"A out-of-bounds write in Fortinet FortiOS versions 7.4.0 through 7.4.2, 7.2.0 through 7.2.6, 7.0.0 through 7.0.13, 6.4.0 through 6.4.14, 6.2.0 through 6.2.15, 6.0.0 through 6.0.17, FortiProxy versions 7.4.0 through 7.4.2, 7.2.0 through 7.2.8, 7.0.0 through 7.0.14, 2.0.0 through 2.0.13, 1.2.0 through 1.2.13, 1.1.0 through 1.1.6, 1.0.0 through 1.0.7 allows attacker to execute unauthorized code or commands via specifically crafted requests\",\n", - " \"cvss_vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"cvss_base_score\": 9.8,\n", - " \"cvss_severity\": \"CRITICAL\",\n", - " \"cwe_name\": \"CWE-787: Out-of-bounds Write (4.17)\",\n", - " \"cwe_description\": \"The product writes data past the end, or before the beginning, of the intended buffer.\",\n", - " \"cwe_extended_description\": null,\n", - " \"configurations\": [\n", - " {\n", - " \"package\": \"fortiproxy\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"2.0.14\",\n", - " \"versionStartIncluding\": \"1.0.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortiproxy\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"7.0.15\",\n", - " \"versionStartIncluding\": \"7.0.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortiproxy\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"7.2.9\",\n", - " \"versionStartIncluding\": \"7.2.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortiproxy\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"7.4.3\",\n", - " \"versionStartIncluding\": \"7.4.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortios\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"6.0.18\",\n", - " \"versionStartIncluding\": \"6.0.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortios\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"6.2.16\",\n", - " \"versionStartIncluding\": \"6.2.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortios\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"6.4.15\",\n", - " \"versionStartIncluding\": \"6.4.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortios\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"7.0.14\",\n", - " \"versionStartIncluding\": \"7.0.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortios\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"7.2.7\",\n", - " \"versionStartIncluding\": \"7.2.0\",\n", - " \"versionEndIncluding\": null\n", - " },\n", - " {\n", - " \"package\": \"fortios\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"7.4.3\",\n", - " \"versionStartIncluding\": \"7.4.0\",\n", - " \"versionEndIncluding\": null\n", - " }\n", - " ],\n", - " \"vendor_names\": [\n", - " \"Fortinet\"\n", - " ],\n", - " \"references\": [\n", - " \"https://fortiguard.com/psirt/FG-IR-24-015\",\n", - " \"https://fortiguard.com/psirt/FG-IR-24-015\"\n", - " ],\n", - " \"disputed\": false,\n", - " \"published_at\": \"2024-02-09T09:15:08.087\",\n", - " \"updated_at\": \"2024-11-29T15:23:32.167\"\n", - " },\n", - " \"rhsa\": {\n", - " \"bugzilla\": {\n", - " \"description\": null,\n", - " \"id\": null,\n", - " \"url\": null\n", - " },\n", - " \"details\": null,\n", - " \"statement\": null,\n", - " \"package_state\": null,\n", - " \"upstream_fix\": null,\n", - " \"cvss3\": null\n", - " },\n", - " \"ubuntu\": {\n", - " \"description\": null,\n", - " \"notes\": null,\n", - " \"notices\": null,\n", - " \"priority\": null,\n", - " \"ubuntu_description\": null,\n", - " \"impact\": null\n", - " },\n", - " \"epss\": {\n", - " \"epss\": 0.92448,\n", - " \"percentile\": 0.99719,\n", - " \"date\": \"2025-06-07\",\n", - " \"cve\": \"CVE-2024-21762\"\n", - " },\n", - " \"has_sufficient_intel_for_agent\": true\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-hh8p-p8mp-gqhm\",\n", - " \"ghsa\": {\n", - " \"ghsa_id\": \"GHSA-hh8p-p8mp-gqhm\",\n", - " \"cve_id\": \"CVE-2023-6975\",\n", - " \"summary\": \"MLFlow Path Traversal Vulnerability\",\n", - " \"description\": \"A malicious user could use this issue to get command execution on the vulnerable machine and get access to data & models information.\",\n", - " \"severity\": \"critical\",\n", - " \"vulnerabilities\": [\n", - " {\n", - " \"package\": {\n", - " \"ecosystem\": \"pip\",\n", - " \"name\": \"mlflow\"\n", - " },\n", - " \"vulnerable_version_range\": \"< 2.9.2\",\n", - " \"first_patched_version\": \"2.9.2\",\n", - " \"vulnerable_functions\": []\n", - " }\n", - " ],\n", - " \"cvss\": {\n", - " \"score\": 9.8,\n", - " \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\"\n", - " },\n", - " \"cwes\": [\n", - " {\n", - " \"cwe_id\": \"CWE-29\",\n", - " \"name\": \"Path Traversal: '..filename'\"\n", - " }\n", - " ],\n", - " \"published_at\": \"2023-12-20T06:30:25Z\",\n", - " \"updated_at\": \"2024-01-02T15:14:39Z\",\n", - " \"url\": \"https://api.github.com/advisories/GHSA-hh8p-p8mp-gqhm\",\n", - " \"html_url\": \"https://github.com/advisories/GHSA-hh8p-p8mp-gqhm\",\n", - " \"type\": \"reviewed\",\n", - " \"repository_advisory_url\": null,\n", - " \"source_code_location\": \"https://github.com/mlflow/mlflow\",\n", - " \"identifiers\": [\n", - " {\n", - " \"value\": \"GHSA-hh8p-p8mp-gqhm\",\n", - " \"type\": \"GHSA\"\n", - " },\n", - " {\n", - " \"value\": \"CVE-2023-6975\",\n", - " \"type\": \"CVE\"\n", - " }\n", - " ],\n", - " \"references\": [\n", - " \"https://nvd.nist.gov/vuln/detail/CVE-2023-6975\",\n", - " \"https://github.com/mlflow/mlflow/commit/b9ab9ed77e1deda9697fe472fb1079fd428149ee\",\n", - " \"https://huntr.com/bounties/029a3824-cee3-4cf1-b260-7138aa539b85\",\n", - " \"https://github.com/advisories/GHSA-hh8p-p8mp-gqhm\"\n", - " ],\n", - " \"github_reviewed_at\": \"2023-12-20T20:32:39Z\",\n", - " \"nvd_published_at\": \"2023-12-20T06:15:45Z\",\n", - " \"withdrawn_at\": null,\n", - " \"cvss_severities\": {\n", - " \"cvss_v3\": {\n", - " \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"score\": 9.8\n", - " },\n", - " \"cvss_v4\": {\n", - " \"vector_string\": null,\n", - " \"score\": 0.0\n", - " }\n", - " },\n", - " \"credits\": [],\n", - " \"epss\": {\n", - " \"percentage\": 0.01542,\n", - " \"percentile\": 0.80461\n", - " }\n", - " },\n", - " \"nvd\": {\n", - " \"cve_id\": \"CVE-2023-6975\",\n", - " \"cve_description\": \"A malicious user could use this issue to get command execution on the vulnerable machine and get access to data & models information.\",\n", - " \"cvss_vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"cvss_base_score\": 9.8,\n", - " \"cvss_severity\": \"CRITICAL\",\n", - " \"cwe_name\": \"CWE-29: Path Traversal: '\\\\..\\\\filename' (4.17)\",\n", - " \"cwe_description\": \"The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize '\\\\..\\\\filename' (leading backslash dot dot) sequences that can resolve to a location that is outside of that directory.\",\n", - " \"cwe_extended_description\": \"This allows attackers to traverse the file system to access files or directories that are outside of the restricted directory.\\nThis is similar to CWE-25, except using \\\"\\\\\\\" instead of \\\"/\\\". Sometimes a program checks for \\\"..\\\\\\\" at the beginning of the input, so a \\\"\\\\..\\\\\\\" can bypass that check. It is also useful for bypassing path traversal protection schemes that only assume that the \\\"/\\\" separator is valid.\",\n", - " \"configurations\": [\n", - " {\n", - " \"package\": \"mlflow\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": \"2.9.2\",\n", - " \"versionStartIncluding\": null,\n", - " \"versionEndIncluding\": null\n", - " }\n", - " ],\n", - " \"vendor_names\": [\n", - " \"Lfprojects\"\n", - " ],\n", - " \"references\": [\n", - " \"https://github.com/mlflow/mlflow/commit/b9ab9ed77e1deda9697fe472fb1079fd428149ee\",\n", - " \"https://huntr.com/bounties/029a3824-cee3-4cf1-b260-7138aa539b85\",\n", - " \"https://github.com/mlflow/mlflow/commit/b9ab9ed77e1deda9697fe472fb1079fd428149ee\",\n", - " \"https://huntr.com/bounties/029a3824-cee3-4cf1-b260-7138aa539b85\"\n", - " ],\n", - " \"disputed\": false,\n", - " \"published_at\": \"2023-12-20T06:15:45.553\",\n", - " \"updated_at\": \"2024-11-21T08:44:57.463\"\n", - " },\n", - " \"rhsa\": {\n", - " \"bugzilla\": {\n", - " \"description\": null,\n", - " \"id\": null,\n", - " \"url\": null\n", - " },\n", - " \"details\": null,\n", - " \"statement\": null,\n", - " \"package_state\": null,\n", - " \"upstream_fix\": null,\n", - " \"cvss3\": null\n", - " },\n", - " \"ubuntu\": {\n", - " \"description\": null,\n", - " \"notes\": null,\n", - " \"notices\": null,\n", - " \"priority\": null,\n", - " \"ubuntu_description\": null,\n", - " \"impact\": null\n", - " },\n", - " \"epss\": {\n", - " \"epss\": 0.01542,\n", - " \"percentile\": 0.80467,\n", - " \"date\": \"2025-06-07\",\n", - " \"cve\": \"CVE-2023-6975\"\n", - " },\n", - " \"has_sufficient_intel_for_agent\": true\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-3f63-hfp8-52jq\",\n", - " \"ghsa\": {\n", - " \"ghsa_id\": \"GHSA-3f63-hfp8-52jq\",\n", - " \"cve_id\": \"CVE-2023-50447\",\n", - " \"summary\": \"Arbitrary Code Execution in Pillow\",\n", - " \"description\": \"Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter).\",\n", - " \"severity\": \"critical\",\n", - " \"vulnerabilities\": [\n", - " {\n", - " \"package\": {\n", - " \"ecosystem\": \"pip\",\n", - " \"name\": \"Pillow\"\n", - " },\n", - " \"vulnerable_version_range\": \"< 10.2.0\",\n", - " \"first_patched_version\": \"10.2.0\",\n", - " \"vulnerable_functions\": []\n", - " }\n", - " ],\n", - " \"cvss\": {\n", - " \"score\": 8.1,\n", - " \"vector_string\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\"\n", - " },\n", - " \"cwes\": [\n", - " {\n", - " \"cwe_id\": \"CWE-94\",\n", - " \"name\": \"Improper Control of Generation of Code ('Code Injection')\"\n", - " },\n", - " {\n", - " \"cwe_id\": \"CWE-95\",\n", - " \"name\": \"Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')\"\n", - " }\n", - " ],\n", - " \"published_at\": \"2024-01-19T21:30:35Z\",\n", - " \"updated_at\": \"2024-11-18T16:26:35Z\",\n", - " \"url\": \"https://api.github.com/advisories/GHSA-3f63-hfp8-52jq\",\n", - " \"html_url\": \"https://github.com/advisories/GHSA-3f63-hfp8-52jq\",\n", - " \"type\": \"reviewed\",\n", - " \"repository_advisory_url\": null,\n", - " \"source_code_location\": \"https://github.com/python-pillow/Pillow\",\n", - " \"identifiers\": [\n", - " {\n", - " \"value\": \"GHSA-3f63-hfp8-52jq\",\n", - " \"type\": \"GHSA\"\n", - " },\n", - " {\n", - " \"value\": \"CVE-2023-50447\",\n", - " \"type\": \"CVE\"\n", - " }\n", - " ],\n", - " \"references\": [\n", - " \"https://nvd.nist.gov/vuln/detail/CVE-2023-50447\",\n", - " \"https://github.com/python-pillow/Pillow/commit/45c726fd4daa63236a8f3653530f297dc87b160a\",\n", - " \"https://pillow.readthedocs.io/en/stable/releasenotes/10.2.0.html#security\",\n", - " \"http://www.openwall.com/lists/oss-security/2024/01/20/1\",\n", - " \"https://github.com/python-pillow/Pillow/releases\",\n", - " \"https://lists.debian.org/debian-lts-announce/2024/01/msg00019.html\",\n", - " \"https://devhub.checkmarx.com/cve-details/CVE-2023-50447\",\n", - " \"https://duartecsantos.github.io/2023-01-02-CVE-2023-50447\",\n", - " \"https://duartecsantos.github.io/2024-01-02-CVE-2023-50447\",\n", - " \"https://github.com/advisories/GHSA-3f63-hfp8-52jq\"\n", - " ],\n", - " \"github_reviewed_at\": \"2024-01-22T21:28:18Z\",\n", - " \"nvd_published_at\": \"2024-01-19T20:15:11Z\",\n", - " \"withdrawn_at\": null,\n", - " \"cvss_severities\": {\n", - " \"cvss_v3\": {\n", - " \"vector_string\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"score\": 8.1\n", - " },\n", - " \"cvss_v4\": {\n", - " \"vector_string\": \"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N\",\n", - " \"score\": 9.3\n", - " }\n", - " },\n", - " \"credits\": [],\n", - " \"epss\": {\n", - " \"percentage\": 0.00408,\n", - " \"percentile\": 0.60132\n", - " }\n", - " },\n", - " \"nvd\": {\n", - " \"cve_id\": \"CVE-2023-50447\",\n", - " \"cve_description\": \"Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter).\",\n", - " \"cvss_vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"cvss_base_score\": 8.1,\n", - " \"cvss_severity\": \"HIGH\",\n", - " \"cwe_name\": \"CWE-94: Improper Control of Generation of Code ('Code Injection') (4.17)\",\n", - " \"cwe_description\": \"The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.\",\n", - " \"cwe_extended_description\": null,\n", - " \"configurations\": [\n", - " {\n", - " \"package\": \"pillow\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": null,\n", - " \"versionStartIncluding\": null,\n", - " \"versionEndIncluding\": \"10.1.0\"\n", - " },\n", - " {\n", - " \"package\": \"debian_linux\",\n", - " \"system\": null,\n", - " \"versionStartExcluding\": null,\n", - " \"versionEndExcluding\": null,\n", - " \"versionStartIncluding\": \"10.0\",\n", - " \"versionEndIncluding\": \"10.0\"\n", - " }\n", - " ],\n", - " \"vendor_names\": [\n", - " \"Debian\",\n", - " \"Python\"\n", - " ],\n", - " \"references\": [\n", - " \"http://www.openwall.com/lists/oss-security/2024/01/20/1\",\n", - " \"https://devhub.checkmarx.com/cve-details/CVE-2023-50447/\",\n", - " \"https://duartecsantos.github.io/2024-01-02-CVE-2023-50447/\",\n", - " \"https://github.com/python-pillow/Pillow/releases\",\n", - " \"https://lists.debian.org/debian-lts-announce/2024/01/msg00019.html\",\n", - " \"http://www.openwall.com/lists/oss-security/2024/01/20/1\",\n", - " \"https://devhub.checkmarx.com/cve-details/CVE-2023-50447/\",\n", - " \"https://duartecsantos.github.io/2024-01-02-CVE-2023-50447/\",\n", - " \"https://github.com/python-pillow/Pillow/releases\",\n", - " \"https://lists.debian.org/debian-lts-announce/2024/01/msg00019.html\"\n", - " ],\n", - " \"disputed\": false,\n", - " \"published_at\": \"2024-01-19T20:15:11.870\",\n", - " \"updated_at\": \"2024-11-21T08:37:00.967\"\n", - " },\n", - " \"rhsa\": {\n", - " \"bugzilla\": {\n", - " \"description\": \"pillow: Arbitrary Code Execution via the environment parameter\",\n", - " \"id\": \"2259479\",\n", - " \"url\": \"https://bugzilla.redhat.com/show_bug.cgi?id=2259479\"\n", - " },\n", - " \"details\": [\n", - " \"Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter).\",\n", - " \"A vulnerability was found in Pillow, a popular Python imaging library. The flaw identified in the PIL.ImageMath.eval function enables arbitrary code execution by manipulating the environment parameter.\"\n", - " ],\n", - " \"statement\": \"The vulnerability in Pillow's PIL.ImageMath.eval function poses a significant threat due to its potential for arbitrary code execution. Pillow's widespread use in diverse domains makes this flaw particularly impactful, as it could lead to unauthorized access, data breaches, and compromise of entire systems. The complex exploitation method involving Python's dunder methods adds sophistication to potential attacks.\",\n", - " \"package_state\": null,\n", - " \"upstream_fix\": null,\n", - " \"cvss3\": {\n", - " \"cvss3_base_score\": 8.1,\n", - " \"cvss3_scoring_vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"status\": \"verified\"\n", - " },\n", - " \"threat_severity\": \"Important\",\n", - " \"public_date\": \"2024-01-19T00:00:00Z\",\n", - " \"cwe\": \"CWE-77\",\n", - " \"affected_release\": [\n", - " {\n", - " \"product_name\": \"Red Hat Ansible Automation Platform 2.4 for RHEL 8\",\n", - " \"release_date\": \"2024-06-10T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:3781\",\n", - " \"cpe\": \"cpe:/a:redhat:ansible_automation_platform:2.4::el8\",\n", - " \"package\": \"python3x-pillow-0:10.3.0-1.el8ap\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Ansible Automation Platform 2.4 for RHEL 9\",\n", - " \"release_date\": \"2024-06-10T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:3781\",\n", - " \"cpe\": \"cpe:/a:redhat:ansible_automation_platform:2.4::el9\",\n", - " \"package\": \"python-pillow-0:10.3.0-1.el9ap\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 7\",\n", - " \"release_date\": \"2024-02-19T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:0857\",\n", - " \"cpe\": \"cpe:/o:redhat:enterprise_linux:7\",\n", - " \"package\": \"python-pillow-0:2.0.0-25.gitd1c6db8.el7_9\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8\",\n", - " \"release_date\": \"2024-02-20T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:0893\",\n", - " \"cpe\": \"cpe:/a:redhat:enterprise_linux:8\",\n", - " \"package\": \"python-pillow-0:5.1.1-18.el8_9.1\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8.2 Advanced Update Support\",\n", - " \"release_date\": \"2024-02-29T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:1059\",\n", - " \"cpe\": \"cpe:/a:redhat:rhel_aus:8.2\",\n", - " \"package\": \"python-pillow-0:5.1.1-15.el8_2\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8.4 Advanced Mission Critical Update Support\",\n", - " \"release_date\": \"2024-02-29T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:1060\",\n", - " \"cpe\": \"cpe:/a:redhat:rhel_aus:8.4\",\n", - " \"package\": \"python-pillow-0:5.1.1-15.el8_4\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8.4 Telecommunications Update Service\",\n", - " \"release_date\": \"2024-02-29T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:1060\",\n", - " \"cpe\": \"cpe:/a:redhat:rhel_tus:8.4\",\n", - " \"package\": \"python-pillow-0:5.1.1-15.el8_4\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8.4 Update Services for SAP Solutions\",\n", - " \"release_date\": \"2024-02-29T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:1060\",\n", - " \"cpe\": \"cpe:/a:redhat:rhel_e4s:8.4\",\n", - " \"package\": \"python-pillow-0:5.1.1-15.el8_4\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8.6 Extended Update Support\",\n", - " \"release_date\": \"2024-02-29T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:1058\",\n", - " \"cpe\": \"cpe:/a:redhat:rhel_eus:8.6\",\n", - " \"package\": \"python-pillow-0:5.1.1-19.el8_6\"\n", - " },\n", - " {\n", - " \"product_name\": \"Red Hat Enterprise Linux 8.8 Extended Update Support\",\n", - " \"release_date\": \"2024-02-08T00:00:00Z\",\n", - " \"advisory\": \"RHSA-2024:0754\",\n", - " \"cpe\": \"cpe:/a:redhat:rhel_eus:8.8\",\n", - " \"package\": \"python-pillow-0:5.1.1-19.el8_8\"\n", - " }\n", - " ],\n", - " \"references\": [\n", - " \"https://www.cve.org/CVERecord?id=CVE-2023-50447\\nhttps://nvd.nist.gov/vuln/detail/CVE-2023-50447\\nhttp://www.openwall.com/lists/oss-security/2024/01/20/1\\nhttps://devhub.checkmarx.com/cve-details/CVE-2023-50447/\\nhttps://duartecsantos.github.io/2023-01-02-CVE-2023-50447/\\nhttps://github.com/python-pillow/Pillow/releases\"\n", - " ],\n", - " \"name\": \"CVE-2023-50447\",\n", - " \"mitigation\": {\n", - " \"value\": \"Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base or stability.\",\n", - " \"lang\": \"en:us\"\n", - " },\n", - " \"csaw\": false\n", - " },\n", - " \"ubuntu\": {\n", - " \"description\": \"\\nPillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution\\nvia the environment parameter, a different vulnerability than\\nCVE-2022-22817 (which was about the expression parameter).\",\n", - " \"notes\": [],\n", - " \"notices\": [\n", - " {\n", - " \"cves_ids\": [\n", - " \"CVE-2023-44271\",\n", - " \"CVE-2023-50447\"\n", - " ],\n", - " \"description\": \"It was discovered that Pillow incorrectly handled certain long text\\narguments. An attacker could possibly use this issue to cause Pillow to\\nconsume resources, leading to a denial of service. This issue only affected\\nUbuntu 20.04 LTS, and Ubuntu 22.04 LTS. (CVE-2023-44271)\\n\\nDuarte Santos discovered that Pillow incorrectly handled the environment\\nparameter to PIL.ImageMath.eval. An attacker could possibly use this issue\\nto execute arbitrary code. (CVE-2023-50447)\\n\",\n", - " \"id\": \"USN-6618-1\",\n", - " \"instructions\": \"In general, a standard system update will make all the necessary changes.\\n\",\n", - " \"is_hidden\": false,\n", - " \"published\": \"2024-01-30T15:17:54.445907\",\n", - " \"references\": [],\n", - " \"release_packages\": {\n", - " \"focal\": [\n", - " {\n", - " \"description\": \"Python Imaging Library\",\n", - " \"is_source\": true,\n", - " \"name\": \"pillow\",\n", - " \"version\": \"7.0.0-4ubuntu0.8\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": false,\n", - " \"name\": \"python-pil-doc\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"7.0.0-4ubuntu0.8\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/7.0.0-4ubuntu0.8\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": true,\n", - " \"name\": \"python3-pil\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"7.0.0-4ubuntu0.8\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/7.0.0-4ubuntu0.8\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": false,\n", - " \"name\": \"python3-pil.imagetk\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"7.0.0-4ubuntu0.8\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/7.0.0-4ubuntu0.8\"\n", - " }\n", - " ],\n", - " \"jammy\": [\n", - " {\n", - " \"description\": \"Python Imaging Library\",\n", - " \"is_source\": true,\n", - " \"name\": \"pillow\",\n", - " \"version\": \"9.0.1-1ubuntu0.2\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": false,\n", - " \"name\": \"python-pil-doc\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"9.0.1-1ubuntu0.2\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/9.0.1-1ubuntu0.2\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": true,\n", - " \"name\": \"python3-pil\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"9.0.1-1ubuntu0.2\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/9.0.1-1ubuntu0.2\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": false,\n", - " \"name\": \"python3-pil.imagetk\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"9.0.1-1ubuntu0.2\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/9.0.1-1ubuntu0.2\"\n", - " }\n", - " ],\n", - " \"mantic\": [\n", - " {\n", - " \"description\": \"Python Imaging Library\",\n", - " \"is_source\": true,\n", - " \"name\": \"pillow\",\n", - " \"version\": \"10.0.0-1ubuntu0.1\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": false,\n", - " \"name\": \"python-pil-doc\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"10.0.0-1ubuntu0.1\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/10.0.0-1ubuntu0.1\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": true,\n", - " \"name\": \"python3-pil\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"10.0.0-1ubuntu0.1\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/10.0.0-1ubuntu0.1\"\n", - " },\n", - " {\n", - " \"is_source\": false,\n", - " \"is_visible\": false,\n", - " \"name\": \"python3-pil.imagetk\",\n", - " \"pocket\": \"security\",\n", - " \"source_link\": \"https://launchpad.net/ubuntu/+source/pillow\",\n", - " \"version\": \"10.0.0-1ubuntu0.1\",\n", - " \"version_link\": \"https://launchpad.net/ubuntu/+source/pillow/10.0.0-1ubuntu0.1\"\n", - " }\n", - " ]\n", - " },\n", - " \"summary\": \"Several security issues were fixed in Pillow.\\n\",\n", - " \"title\": \"Pillow vulnerabilities\",\n", - " \"type\": \"USN\"\n", - " }\n", - " ],\n", - " \"priority\": \"medium\",\n", - " \"ubuntu_description\": \"\",\n", - " \"impact\": {\n", - " \"baseMetricV3\": {\n", - " \"cvssV3\": {\n", - " \"attackComplexity\": \"HIGH\",\n", - " \"attackVector\": \"NETWORK\",\n", - " \"availabilityImpact\": \"HIGH\",\n", - " \"baseScore\": 8.1,\n", - " \"baseSeverity\": \"HIGH\",\n", - " \"confidentialityImpact\": \"HIGH\",\n", - " \"integrityImpact\": \"HIGH\",\n", - " \"privilegesRequired\": \"NONE\",\n", - " \"scope\": \"UNCHANGED\",\n", - " \"userInteraction\": \"NONE\",\n", - " \"vectorString\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n", - " \"version\": \"3.1\"\n", - " },\n", - " \"exploitabilityScore\": 2.2,\n", - " \"impactScore\": 5.9\n", - " }\n", - " },\n", - " \"bugs\": [\n", - " \"http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1061172\"\n", - " ],\n", - " \"codename\": null,\n", - " \"cvss3\": 8.1,\n", - " \"id\": \"CVE-2023-50447\",\n", - " \"mitigation\": \"\",\n", - " \"notices_ids\": [\n", - " \"USN-6618-1\"\n", - " ],\n", - " \"packages\": [\n", - " {\n", - " \"debian\": \"https://tracker.debian.org/pkg/pillow\",\n", - " \"name\": \"pillow\",\n", - " \"source\": \"https://ubuntu.com/security/cve?package=pillow\",\n", - " \"statuses\": [\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"bionic\",\n", - " \"status\": \"needs-triage\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"7.0.0-4ubuntu0.8\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"focal\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"9.0.1-1ubuntu0.2\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"jammy\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"end of life, was needed\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"lunar\",\n", - " \"status\": \"ignored\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"10.0.0-1ubuntu0.1\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"mantic\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"10.2.0-1\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"noble\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"10.2.0-1\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"oracular\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"10.2.0-1\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"plucky\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"trusty\",\n", - " \"status\": \"needs-triage\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"10.2.0-1\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"upstream\",\n", - " \"status\": \"released\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"xenial\",\n", - " \"status\": \"needs-triage\"\n", - " }\n", - " ],\n", - " \"ubuntu\": \"https://packages.ubuntu.com/search?suite=all§ion=all&arch=any&searchon=sourcenames&keywords=pillow\"\n", - " },\n", - " {\n", - " \"debian\": \"https://tracker.debian.org/pkg/pillow-python2\",\n", - " \"name\": \"pillow-python2\",\n", - " \"source\": \"https://ubuntu.com/security/cve?package=pillow-python2\",\n", - " \"statuses\": [\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"bionic\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"focal\",\n", - " \"status\": \"needs-triage\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"jammy\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"lunar\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"mantic\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"noble\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"oracular\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"plucky\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"trusty\",\n", - " \"status\": \"DNE\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"upstream\",\n", - " \"status\": \"needs-triage\"\n", - " },\n", - " {\n", - " \"component\": null,\n", - " \"description\": \"\",\n", - " \"pocket\": \"security\",\n", - " \"release_codename\": \"xenial\",\n", - " \"status\": \"DNE\"\n", - " }\n", - " ],\n", - " \"ubuntu\": \"https://packages.ubuntu.com/search?suite=all§ion=all&arch=any&searchon=sourcenames&keywords=pillow-python2\"\n", - " }\n", - " ],\n", - " \"patches\": {\n", - " \"pillow\": [\n", - " \"upstream: https://github.com/python-pillow/Pillow/commit/45c726fd4daa63236a8f3653530f297dc87b160a\",\n", - " \"upstream: https://github.com/python-pillow/Pillow/commit/0ca3c33c59927e1c7e0c14dbc1eea1dfb2431a80\",\n", - " \"upstream: https://github.com/python-pillow/Pillow/commit/557ba59d13de919d04b3fd4cdef8634f7d4b3348\"\n", - " ],\n", - " \"pillow-python2\": []\n", - " },\n", - " \"published\": \"2024-01-19T20:15:00\",\n", - " \"references\": [\n", - " \"https://duartecsantos.github.io/2023-01-02-CVE-2023-50447/\",\n", - " \"https://pillow.readthedocs.io/en/stable/releasenotes/10.2.0.html#imagemath-eval-restricted-environment-keys\",\n", - " \"https://devhub.checkmarx.com/cve-details/CVE-2023-50447/\",\n", - " \"http://www.openwall.com/lists/oss-security/2024/01/20/1\",\n", - " \"https://ubuntu.com/security/notices/USN-6618-1\",\n", - " \"https://www.cve.org/CVERecord?id=CVE-2023-50447\"\n", - " ],\n", - " \"status\": \"active\",\n", - " \"tags\": {},\n", - " \"updated_at\": \"2024-07-24T15:57:39.284958+00:00\"\n", - " },\n", - " \"epss\": {\n", - " \"epss\": 0.00408,\n", - " \"percentile\": 0.60242,\n", - " \"date\": \"2025-05-30\",\n", - " \"cve\": \"CVE-2023-50447\"\n", - " },\n", - " \"has_sufficient_intel_for_agent\": true\n", - " }\n", - " ],\n", - " \"sbom\": {\n", - " \"packages\": [\n", - " {\n", - " \"name\": \"Babel\",\n", - " \"version\": \"2.13.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Brotli\",\n", - " \"version\": \"1.0.9\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Brotli\",\n", - " \"version\": \"1.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"ConfigArgParse\",\n", - " \"version\": \"1.5.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Flask\",\n", - " \"version\": \"3.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"GitPython\",\n", - " \"version\": \"3.1.40\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Jinja2\",\n", - " \"version\": \"3.1.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Mako\",\n", - " \"version\": \"1.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Markdown\",\n", - " \"version\": \"3.5.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"MarkupSafe\",\n", - " \"version\": \"2.1.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Pillow\",\n", - " \"version\": \"10.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"PyJWT\",\n", - " \"version\": \"2.8.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"PySocks\",\n", - " \"version\": \"1.7.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"PyYAML\",\n", - " \"version\": \"6.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Pygments\",\n", - " \"version\": \"2.17.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"SQLAlchemy\",\n", - " \"version\": \"2.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"SciPy\",\n", - " \"version\": \"1.11.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Simple\",\n", - " \"version\": \"Launcher\",\n", - " \"path\": null,\n", - " \"system\": \"1.1.0.14\"\n", - " },\n", - " {\n", - " \"name\": \"Sphinx\",\n", - " \"version\": \"7.2.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"Werkzeug\",\n", - " \"version\": \"3.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"absl-py\",\n", - " \"version\": \"1.4.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"adduser\",\n", - " \"version\": \"3.118ubuntu5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"alabaster\",\n", - " \"version\": \"0.7.13\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"alembic\",\n", - " \"version\": \"1.13.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"anyio\",\n", - " \"version\": \"3.7.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"appdirs\",\n", - " \"version\": \"1.4.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"apt\",\n", - " \"version\": \"2.4.11\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"asn1crypto\",\n", - " \"version\": \"1.5.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"attrs\",\n", - " \"version\": \"23.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"base-files\",\n", - " \"version\": \"12ubuntu4.4\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"base-passwd\",\n", - " \"version\": \"3.5.52build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"bash\",\n", - " \"version\": \"5.1-6ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"bc\",\n", - " \"version\": \"1.07.1-3build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"beautifulsoup4\",\n", - " \"version\": \"4.12.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"betterproto\",\n", - " \"version\": \"1.2.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"binutils\",\n", - " \"version\": \"2.38-4ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"binutils-common\",\n", - " \"version\": \"2.38-4ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"binutils-x86-64-linux-gnu\",\n", - " \"version\": \"2.38-4ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"blinker\",\n", - " \"version\": \"1.7.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"boa\",\n", - " \"version\": \"0.16.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"bokeh\",\n", - " \"version\": \"2.4.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"boltons\",\n", - " \"version\": \"23.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"bsdutils\",\n", - " \"version\": \"1:2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"build-essential\",\n", - " \"version\": \"12.9ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"bzip2\",\n", - " \"version\": \"1.0.8-5build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"ca-certificates\",\n", - " \"version\": \"20230311ubuntu0.22.04.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"ca-certificates-java\",\n", - " \"version\": \"java-archive\",\n", - " \"path\": null,\n", - " \"system\": \"\\u001b[38;2;119;119;119m\\u001b[0m\"\n", - " },\n", - " {\n", - " \"name\": \"ca-certificates-java\",\n", - " \"version\": \"20190909ubuntu1.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cachetools\",\n", - " \"version\": \"5.3.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cattrs\",\n", - " \"version\": \"23.2.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"certifi\",\n", - " \"version\": \"2023.11.17\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cffi\",\n", - " \"version\": \"1.15.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cffi\",\n", - " \"version\": \"1.16.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"chardet\",\n", - " \"version\": \"5.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"charset-normalizer\",\n", - " \"version\": \"3.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"charset-normalizer\",\n", - " \"version\": \"3.3.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"click\",\n", - " \"version\": \"8.1.7\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cloudpickle\",\n", - " \"version\": \"3.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"colorama\",\n", - " \"version\": \"0.4.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"conda\",\n", - " \"version\": \"23.3.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"conda-build\",\n", - " \"version\": \"3.28.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"conda-libmamba-solver\",\n", - " \"version\": \"23.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"conda-package-handling\",\n", - " \"version\": \"2.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"conda_index\",\n", - " \"version\": \"0.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"conda_package_streaming\",\n", - " \"version\": \"0.9.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"configparser\",\n", - " \"version\": \"5.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"confluent-kafka\",\n", - " \"version\": \"1.9.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"contourpy\",\n", - " \"version\": \"1.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"coreutils\",\n", - " \"version\": \"8.32-4.1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cpp\",\n", - " \"version\": \"4:11.2.0-1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cpp-11\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cryptography\",\n", - " \"version\": \"41.0.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cryptography\",\n", - " \"version\": \"41.0.7\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cubinlinker\",\n", - " \"version\": \"0.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-cccl-11-8\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-compat-11-8\",\n", - " \"version\": \"520.61.05-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-compiler-11-8\",\n", - " \"version\": \"11.8.0-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-cudart-11-8\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-cudart-dev-11-8\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-cuobjdump-11-8\",\n", - " \"version\": \"11.8.86-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-cupti-11-8\",\n", - " \"version\": \"11.8.87-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-cuxxfilt-11-8\",\n", - " \"version\": \"11.8.86-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-driver-dev-11-8\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-gdb-11-8\",\n", - " \"version\": \"11.8.86-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-keyring\",\n", - " \"version\": \"1.1-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-nvcc-11-8\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-nvdisasm-11-8\",\n", - " \"version\": \"11.8.86-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-nvprune-11-8\",\n", - " \"version\": \"11.8.86-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-python\",\n", - " \"version\": \"11.8.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-toolkit-11-8-config-common\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-toolkit-11-config-common\",\n", - " \"version\": \"11.8.89-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cuda-toolkit-config-common\",\n", - " \"version\": \"12.3.101-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cudf\",\n", - " \"version\": \"23.6.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cudf-kafka\",\n", - " \"version\": \"23.6.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cupy\",\n", - " \"version\": \"12.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"curl\",\n", - " \"version\": \"7.81.0-1ubuntu1.15\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"cycler\",\n", - " \"version\": \"0.12.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"cytoolz\",\n", - " \"version\": \"0.12.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"dash\",\n", - " \"version\": \"0.5.11+git20210903+057cd650a4ed-3build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"dask\",\n", - " \"version\": \"2023.3.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"dask-cuda\",\n", - " \"version\": \"23.6.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"dask-cudf\",\n", - " \"version\": \"23.6.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"databricks-cli\",\n", - " \"version\": \"0.18.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"dataclasses\",\n", - " \"version\": \"0.8\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"datacompy\",\n", - " \"version\": \"0.8.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"debconf\",\n", - " \"version\": \"1.5.79ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"debianutils\",\n", - " \"version\": \"5.5-1ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"diffutils\",\n", - " \"version\": \"1:3.8-0ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"dill\",\n", - " \"version\": \"0.3.7\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"dirmngr\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"distributed\",\n", - " \"version\": \"2023.3.2.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"docker\",\n", - " \"version\": \"5.0.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"docker-pycreds\",\n", - " \"version\": \"0.4.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"docutils\",\n", - " \"version\": \"0.20.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"dpkg\",\n", - " \"version\": \"1.21.1ubuntu2.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"dpkg-dev\",\n", - " \"version\": \"1.21.1ubuntu2.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"e2fsprogs\",\n", - " \"version\": \"1.46.5-2ubuntu1.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"entrypoints\",\n", - " \"version\": \"0.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"exceptiongroup\",\n", - " \"version\": \"1.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"fastrlock\",\n", - " \"version\": \"0.8.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"filelock\",\n", - " \"version\": \"3.13.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"findutils\",\n", - " \"version\": \"4.8.0-1ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"fontconfig-config\",\n", - " \"version\": \"2.13.1-4.2ubuntu5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"fonts-dejavu-core\",\n", - " \"version\": \"2.37-2build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"fonttools\",\n", - " \"version\": \"4.46.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"fsspec\",\n", - " \"version\": \"2023.12.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"g++\",\n", - " \"version\": \"4:11.2.0-1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"g++-11\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gcc\",\n", - " \"version\": \"4:11.2.0-1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gcc-11\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gcc-11-base\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gcc-12-base\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gitdb\",\n", - " \"version\": \"4.0.11\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"gmpy2\",\n", - " \"version\": \"2.1.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"gnupg\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gnupg-l10n\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gnupg-utils\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gnupg2\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"googleapis-common-protos\",\n", - " \"version\": \"1.61.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"gpg\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gpg-agent\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gpg-wks-client\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gpg-wks-server\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gpgconf\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gpgsm\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"gpgv\",\n", - " \"version\": \"2.2.27-3ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"greenlet\",\n", - " \"version\": \"3.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"grep\",\n", - " \"version\": \"3.7-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"grpcio\",\n", - " \"version\": \"1.54.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"grpclib\",\n", - " \"version\": \"0.4.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"gunicorn\",\n", - " \"version\": \"21.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"gzip\",\n", - " \"version\": \"1.10-4ubuntu4.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"h2\",\n", - " \"version\": \"4.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"hostname\",\n", - " \"version\": \"3.23ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"hpack\",\n", - " \"version\": \"4.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"hyperframe\",\n", - " \"version\": \"6.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"idna\",\n", - " \"version\": \"3.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"idna\",\n", - " \"version\": \"3.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"imagesize\",\n", - " \"version\": \"1.4.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"importlib-metadata\",\n", - " \"version\": \"7.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"importlib-resources\",\n", - " \"version\": \"6.1.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"init-system-helpers\",\n", - " \"version\": \"1.62\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"itsdangerous\",\n", - " \"version\": \"2.1.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"java-common\",\n", - " \"version\": \"0.72build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"joblib\",\n", - " \"version\": \"1.3.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jq\",\n", - " \"version\": \"1.6-2.1ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"jrt-fs\",\n", - " \"version\": \"11.0.21\",\n", - " \"path\": null,\n", - " \"system\": \"java-archive\"\n", - " },\n", - " {\n", - " \"name\": \"js\",\n", - " \"version\": \"1.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"npm\"\n", - " },\n", - " {\n", - " \"name\": \"json5\",\n", - " \"version\": \"0.9.14\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jsonpatch\",\n", - " \"version\": \"1.32\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jsonpointer\",\n", - " \"version\": \"2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jsonschema\",\n", - " \"version\": \"4.20.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jsonschema-specifications\",\n", - " \"version\": \"2023.11.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jupyter_client\",\n", - " \"version\": \"8.6.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"jupyter_core\",\n", - " \"version\": \"5.5.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"kiwisolver\",\n", - " \"version\": \"1.4.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"libacl1\",\n", - " \"version\": \"2.3.1-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libapt-pkg6.0\",\n", - " \"version\": \"2.4.11\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libarchive-c\",\n", - " \"version\": \"5.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"libasan6\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libasound2\",\n", - " \"version\": \"1.2.6.1-1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libasound2-data\",\n", - " \"version\": \"1.2.6.1-1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libassuan0\",\n", - " \"version\": \"2.5.5-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libatomic1\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libattr1\",\n", - " \"version\": \"1:2.5.1-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libaudit-common\",\n", - " \"version\": \"1:3.0.7-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libaudit1\",\n", - " \"version\": \"1:3.0.7-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libavahi-client3\",\n", - " \"version\": \"0.8-5ubuntu5.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libavahi-common-data\",\n", - " \"version\": \"0.8-5ubuntu5.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libavahi-common3\",\n", - " \"version\": \"0.8-5ubuntu5.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libbinutils\",\n", - " \"version\": \"2.38-4ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libblkid1\",\n", - " \"version\": \"2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libbrotli1\",\n", - " \"version\": \"1.0.9-2build6\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libbsd0\",\n", - " \"version\": \"0.11.5-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libbz2-1.0\",\n", - " \"version\": \"1.0.8-5build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libc-bin\",\n", - " \"version\": \"2.35-0ubuntu3.5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libc-dev-bin\",\n", - " \"version\": \"2.35-0ubuntu3.5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libc6\",\n", - " \"version\": \"2.35-0ubuntu3.5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libc6-dev\",\n", - " \"version\": \"2.35-0ubuntu3.5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcap-ng0\",\n", - " \"version\": \"0.7.9-2.2build3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcap2\",\n", - " \"version\": \"1:2.44-1ubuntu0.22.04.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcbor0.8\",\n", - " \"version\": \"0.8.0-2ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcc1-0\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcom-err2\",\n", - " \"version\": \"1.46.5-2ubuntu1.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcrypt-dev\",\n", - " \"version\": \"1:4.4.27-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcrypt1\",\n", - " \"version\": \"1:4.4.27-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libctf-nobfd0\",\n", - " \"version\": \"2.38-4ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libctf0\",\n", - " \"version\": \"2.38-4ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcublas-11-8\",\n", - " \"version\": \"11.11.3.6-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcufft-11-8\",\n", - " \"version\": \"10.9.0.58-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcups2\",\n", - " \"version\": \"2.4.1op1-1ubuntu4.7\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcurand-11-8\",\n", - " \"version\": \"10.3.0.86-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcurl4\",\n", - " \"version\": \"7.81.0-1ubuntu1.15\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libcusolver-11-8\",\n", - " \"version\": \"11.4.1.48-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libdb5.3\",\n", - " \"version\": \"5.3.28+dfsg1-0.8ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libdbus-1-3\",\n", - " \"version\": \"1.12.20-2ubuntu4.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libdebconfclient0\",\n", - " \"version\": \"0.261ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libdpkg-perl\",\n", - " \"version\": \"1.21.1ubuntu2.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libedit2\",\n", - " \"version\": \"3.1-20210910-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libexpat1\",\n", - " \"version\": \"2.4.7-1ubuntu0.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libext2fs2\",\n", - " \"version\": \"1.46.5-2ubuntu1.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libffi8\",\n", - " \"version\": \"3.4.2-4\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libfido2-1\",\n", - " \"version\": \"1.10.0-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libfontconfig1\",\n", - " \"version\": \"2.13.1-4.2ubuntu5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libfreetype6\",\n", - " \"version\": \"2.11.1+dfsg-1ubuntu0.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgcc-11-dev\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgcc-s1\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgcrypt20\",\n", - " \"version\": \"1.9.4-3ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgdbm-compat4\",\n", - " \"version\": \"1.23-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgdbm6\",\n", - " \"version\": \"1.23-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libglib2.0-0\",\n", - " \"version\": \"2.72.4-0ubuntu2.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgmp10\",\n", - " \"version\": \"2:6.2.1+dfsg-3ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgnutls30\",\n", - " \"version\": \"3.7.3-4ubuntu1.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgomp1\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgpg-error0\",\n", - " \"version\": \"1.43-3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgraphite2-3\",\n", - " \"version\": \"1.3.14-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libgssapi-krb5-2\",\n", - " \"version\": \"1.19.2-2ubuntu0.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libharfbuzz0b\",\n", - " \"version\": \"2.7.4-1ubuntu3.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libhogweed6\",\n", - " \"version\": \"3.7.3-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libidn2-0\",\n", - " \"version\": \"2.3.2-2build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libisl23\",\n", - " \"version\": \"0.24-2build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libitm1\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libjpeg-turbo8\",\n", - " \"version\": \"2.1.2-0ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libjpeg8\",\n", - " \"version\": \"8c-2ubuntu10\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libjq1\",\n", - " \"version\": \"1.6-2.1ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libk5crypto3\",\n", - " \"version\": \"1.19.2-2ubuntu0.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libkeyutils1\",\n", - " \"version\": \"1.6.1-2ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libkrb5-3\",\n", - " \"version\": \"1.19.2-2ubuntu0.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libkrb5support0\",\n", - " \"version\": \"1.19.2-2ubuntu0.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libksba8\",\n", - " \"version\": \"1.6.0-2ubuntu0.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"liblcms2-2\",\n", - " \"version\": \"2.12~rc1-2build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libldap-2.5-0\",\n", - " \"version\": \"2.5.16+dfsg-0ubuntu0.22.04.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"liblsan0\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"liblz4-1\",\n", - " \"version\": \"1.9.3-2build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"liblzma5\",\n", - " \"version\": \"5.2.5-2ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libmambapy\",\n", - " \"version\": \"1.5.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"libmd0\",\n", - " \"version\": \"1.0.4-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libmount1\",\n", - " \"version\": \"2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libmpc3\",\n", - " \"version\": \"1.2.1-2build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libmpfr6\",\n", - " \"version\": \"4.1.0-3build3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libncurses6\",\n", - " \"version\": \"6.3-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libncursesw6\",\n", - " \"version\": \"6.3-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnettle8\",\n", - " \"version\": \"3.7.3-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnghttp2-14\",\n", - " \"version\": \"1.43.0-1ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnpth0\",\n", - " \"version\": \"1.6-3build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnsl-dev\",\n", - " \"version\": \"1.3.0-2build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnsl2\",\n", - " \"version\": \"1.3.0-2build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnspr4\",\n", - " \"version\": \"2:4.32-3build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnss3\",\n", - " \"version\": \"2:3.68.2-0ubuntu1.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libnuma1\",\n", - " \"version\": \"2.0.14-3ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libonig5\",\n", - " \"version\": \"6.9.7.1-2build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libp11-kit0\",\n", - " \"version\": \"0.24.0-6build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpam-modules\",\n", - " \"version\": \"1.4.0-11ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpam-modules-bin\",\n", - " \"version\": \"1.4.0-11ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpam-runtime\",\n", - " \"version\": \"1.4.0-11ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpam0g\",\n", - " \"version\": \"1.4.0-11ubuntu2.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpcre2-8-0\",\n", - " \"version\": \"10.39-3ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpcre3\",\n", - " \"version\": \"2:8.39-13ubuntu0.22.04.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpcsclite1\",\n", - " \"version\": \"1.9.5-3ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libperl5.34\",\n", - " \"version\": \"5.34.0-3ubuntu1.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpng16-16\",\n", - " \"version\": \"1.6.37-3build5\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libprocps8\",\n", - " \"version\": \"2:3.3.17-6ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libpsl5\",\n", - " \"version\": \"0.21.0-1.2build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libquadmath0\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libreadline8\",\n", - " \"version\": \"8.1.2-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"librtmp1\",\n", - " \"version\": \"2.4+20151223.gitfa8646d.1-2build4\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsasl2-2\",\n", - " \"version\": \"2.1.27+dfsg2-3ubuntu1.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsasl2-modules-db\",\n", - " \"version\": \"2.1.27+dfsg2-3ubuntu1.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libseccomp2\",\n", - " \"version\": \"2.5.3-2ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libselinux1\",\n", - " \"version\": \"3.3-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsemanage-common\",\n", - " \"version\": \"3.3-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsemanage2\",\n", - " \"version\": \"3.3-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsepol2\",\n", - " \"version\": \"3.3-1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsmartcols1\",\n", - " \"version\": \"2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsqlite3-0\",\n", - " \"version\": \"3.37.2-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libss2\",\n", - " \"version\": \"1.46.5-2ubuntu1.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libssh-4\",\n", - " \"version\": \"0.9.6-2ubuntu0.22.04.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libssl3\",\n", - " \"version\": \"3.0.2-0ubuntu1.12\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libstdc++-11-dev\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libstdc++6\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libsystemd0\",\n", - " \"version\": \"249.11-0ubuntu3.11\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libtasn1-6\",\n", - " \"version\": \"4.18.0-4build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libtinfo6\",\n", - " \"version\": \"6.3-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libtirpc-common\",\n", - " \"version\": \"1.3.2-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libtirpc-dev\",\n", - " \"version\": \"1.3.2-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libtirpc3\",\n", - " \"version\": \"1.3.2-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libtsan0\",\n", - " \"version\": \"11.4.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libubsan1\",\n", - " \"version\": \"12.3.0-1ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libudev1\",\n", - " \"version\": \"249.11-0ubuntu3.11\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libunistring2\",\n", - " \"version\": \"1.0-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libuuid1\",\n", - " \"version\": \"2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libxxhash0\",\n", - " \"version\": \"0.8.1-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"libzstd1\",\n", - " \"version\": \"1.4.8+dfsg-3build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"linux-libc-dev\",\n", - " \"version\": \"5.15.0-89.99\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"llvmlite\",\n", - " \"version\": \"0.41.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"locket\",\n", - " \"version\": \"1.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"login\",\n", - " \"version\": \"1:4.8.1-2ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"logsave\",\n", - " \"version\": \"1.46.5-2ubuntu1.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"lsb-base\",\n", - " \"version\": \"11.1.0ubuntu4\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"lto-disabled-list\",\n", - " \"version\": \"24\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"lz4\",\n", - " \"version\": \"4.3.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"make\",\n", - " \"version\": \"4.3-4.1build1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"mamba\",\n", - " \"version\": \"1.5.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"markdown-it-py\",\n", - " \"version\": \"3.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"matplotlib\",\n", - " \"version\": \"3.8.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"mawk\",\n", - " \"version\": \"1.3.4.20200120-3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"mdurl\",\n", - " \"version\": \"0.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"menuinst\",\n", - " \"version\": \"2.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"merlin-core\",\n", - " \"version\": \"23.6.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"merlin-dataloader\",\n", - " \"version\": \"23.6.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"mlflow\",\n", - " \"version\": \"2.9.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"more-itertools\",\n", - " \"version\": \"10.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"morpheus\",\n", - " \"version\": \"23.11.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"mount\",\n", - " \"version\": \"2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"mpmath\",\n", - " \"version\": \"1.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"mrc\",\n", - " \"version\": \"23.11.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"msgpack\",\n", - " \"version\": \"1.0.7\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"multidict\",\n", - " \"version\": \"6.0.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"munkres\",\n", - " \"version\": \"1.1.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"nb-conda-kernels\",\n", - " \"version\": \"2.3.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"ncurses-base\",\n", - " \"version\": \"6.3-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"ncurses-bin\",\n", - " \"version\": \"6.3-2ubuntu0.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"networkx\",\n", - " \"version\": \"3.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"npy-append-array\",\n", - " \"version\": \"0.9.16\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"numba\",\n", - " \"version\": \"0.58.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"numpy\",\n", - " \"version\": \"1.26.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"numpydoc\",\n", - " \"version\": \"1.4.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"nvtabular\",\n", - " \"version\": \"23.6.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"nvtx\",\n", - " \"version\": \"0.2.8\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"oauthlib\",\n", - " \"version\": \"3.2.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"openjdk-11-jre-headless\",\n", - " \"version\": \"11.0.21+9-0ubuntu1~22.04\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"openssh-client\",\n", - " \"version\": \"1:8.9p1-3ubuntu0.4\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"openssl\",\n", - " \"version\": \"3.0.2-0ubuntu1.12\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"openssl\",\n", - " \"version\": \"3.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"binary\"\n", - " },\n", - " {\n", - " \"name\": \"ordered-set\",\n", - " \"version\": \"4.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"packaging\",\n", - " \"version\": \"23.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"packaging\",\n", - " \"version\": \"23.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pandas\",\n", - " \"version\": \"1.3.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"partd\",\n", - " \"version\": \"1.4.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"passwd\",\n", - " \"version\": \"1:4.8.1-2ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"patch\",\n", - " \"version\": \"2.7.6-7build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"perl\",\n", - " \"version\": \"5.34.0-3ubuntu1.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"perl-base\",\n", - " \"version\": \"5.34.0-3ubuntu1.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"perl-modules-5.34\",\n", - " \"version\": \"5.34.0-3ubuntu1.3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"pinentry-curses\",\n", - " \"version\": \"1.1.1-1build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"pip\",\n", - " \"version\": \"23.3.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pkg-config\",\n", - " \"version\": \"0.29.2-1ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"pkginfo\",\n", - " \"version\": \"1.9.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pkgutil_resolve_name\",\n", - " \"version\": \"1.3.10\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"platformdirs\",\n", - " \"version\": \"4.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pluggy\",\n", - " \"version\": \"1.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pluggy\",\n", - " \"version\": \"1.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"procps\",\n", - " \"version\": \"2:3.3.17-6ubuntu2.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"prometheus-client\",\n", - " \"version\": \"0.19.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"prometheus-flask-exporter\",\n", - " \"version\": \"0.23.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"prompt-toolkit\",\n", - " \"version\": \"3.0.41\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"protobuf\",\n", - " \"version\": \"4.21.12\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"psutil\",\n", - " \"version\": \"5.9.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"ptxcompiler\",\n", - " \"version\": \"0.8.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pyOpenSSL\",\n", - " \"version\": \"23.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pyOpenSSL\",\n", - " \"version\": \"23.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pyarrow\",\n", - " \"version\": \"11.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pyarrow-hotfix\",\n", - " \"version\": \"0.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pycosat\",\n", - " \"version\": \"0.6.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pycparser\",\n", - " \"version\": \"2.21\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pynvml\",\n", - " \"version\": \"11.4.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pyparsing\",\n", - " \"version\": \"3.1.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"python\",\n", - " \"version\": \"3.10.12\",\n", - " \"path\": null,\n", - " \"system\": \"binary\"\n", - " },\n", - " {\n", - " \"name\": \"python\",\n", - " \"version\": \"3.10.13\",\n", - " \"path\": null,\n", - " \"system\": \"binary\"\n", - " },\n", - " {\n", - " \"name\": \"python-dateutil\",\n", - " \"version\": \"2.8.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"python-rapidjson\",\n", - " \"version\": \"1.13\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pytz\",\n", - " \"version\": \"2023.3.post1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"pyzmq\",\n", - " \"version\": \"25.1.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"querystring-parser\",\n", - " \"version\": \"1.2.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"readline-common\",\n", - " \"version\": \"8.1.2-1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"referencing\",\n", - " \"version\": \"0.31.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"requests\",\n", - " \"version\": \"2.31.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"requests-cache\",\n", - " \"version\": \"1.1.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"rich\",\n", - " \"version\": \"13.7.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"rmm\",\n", - " \"version\": \"23.6.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"rpcsvc-proto\",\n", - " \"version\": \"1.4.2-0ubuntu6\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"rpds-py\",\n", - " \"version\": \"0.13.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"ruamel.yaml\",\n", - " \"version\": \"0.17.32\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"ruamel.yaml.clib\",\n", - " \"version\": \"0.2.7\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"scikit-learn\",\n", - " \"version\": \"1.2.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sed\",\n", - " \"version\": \"4.8-1ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"sensible-utils\",\n", - " \"version\": \"0.0.17\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"setuptools\",\n", - " \"version\": \"59.8.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"setuptools\",\n", - " \"version\": \"68.1.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"six\",\n", - " \"version\": \"1.16.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"smmap\",\n", - " \"version\": \"5.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sniffio\",\n", - " \"version\": \"1.3.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"snowballstemmer\",\n", - " \"version\": \"2.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sortedcontainers\",\n", - " \"version\": \"2.4.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"soupsieve\",\n", - " \"version\": \"2.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sphinxcontrib-applehelp\",\n", - " \"version\": \"1.0.7\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sphinxcontrib-devhelp\",\n", - " \"version\": \"1.0.5\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sphinxcontrib-htmlhelp\",\n", - " \"version\": \"2.0.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sphinxcontrib-jsmath\",\n", - " \"version\": \"1.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sphinxcontrib-qthelp\",\n", - " \"version\": \"1.0.6\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sphinxcontrib-serializinghtml\",\n", - " \"version\": \"1.1.9\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sqlparse\",\n", - " \"version\": \"0.4.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"stringcase\",\n", - " \"version\": \"1.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sympy\",\n", - " \"version\": \"1.12\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"sysvinit-utils\",\n", - " \"version\": \"3.01-1ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"tabulate\",\n", - " \"version\": \"0.9.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"tar\",\n", - " \"version\": \"1.34+dfsg-1ubuntu0.1.22.04.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"tblib\",\n", - " \"version\": \"2.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"tensorflow-metadata\",\n", - " \"version\": \"1.13.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"threadpoolctl\",\n", - " \"version\": \"3.2.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"tomli\",\n", - " \"version\": \"2.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"toolz\",\n", - " \"version\": \"0.12.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"torch\",\n", - " \"version\": \"2.0.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"tornado\",\n", - " \"version\": \"6.3.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"tqdm\",\n", - " \"version\": \"4.66.1\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"traitlets\",\n", - " \"version\": \"5.14.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"triton\",\n", - " \"version\": \"2.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"tritonclient\",\n", - " \"version\": \"2.26.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"typing-utils\",\n", - " \"version\": \"0.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"typing_extensions\",\n", - " \"version\": \"4.8.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"ubuntu-keyring\",\n", - " \"version\": \"2021.03.26\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"ucf\",\n", - " \"version\": \"3.0043\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"ujson\",\n", - " \"version\": \"5.8.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"unicodedata2\",\n", - " \"version\": \"15.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"unzip\",\n", - " \"version\": \"6.0-26ubuntu3.1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"url-normalize\",\n", - " \"version\": \"1.4.3\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"urllib3\",\n", - " \"version\": \"2.0.4\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"urllib3\",\n", - " \"version\": \"2.1.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"usrmerge\",\n", - " \"version\": \"25ubuntu2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"util-linux\",\n", - " \"version\": \"2.37.2-4ubuntu3\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"watchdog\",\n", - " \"version\": \"2.1.9\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"watchgod\",\n", - " \"version\": \"0.8.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"wcwidth\",\n", - " \"version\": \"0.2.12\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"websocket-client\",\n", - " \"version\": \"1.7.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"websockets\",\n", - " \"version\": \"12.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"wget\",\n", - " \"version\": \"1.21.2-2ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"wheel\",\n", - " \"version\": \"0.41.2\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"wheel\",\n", - " \"version\": \"0.42.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"xz-utils\",\n", - " \"version\": \"5.2.5-2ubuntu1\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"zict\",\n", - " \"version\": \"3.0.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"zip\",\n", - " \"version\": \"3.0-12build2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"zipp\",\n", - " \"version\": \"3.17.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " },\n", - " {\n", - " \"name\": \"zlib1g\",\n", - " \"version\": \"1:1.2.11.dfsg-2ubuntu9.2\",\n", - " \"path\": null,\n", - " \"system\": \"deb\"\n", - " },\n", - " {\n", - " \"name\": \"zstandard\",\n", - " \"version\": \"0.19.0\",\n", - " \"path\": null,\n", - " \"system\": \"python\"\n", - " }\n", - " ]\n", - " },\n", - " \"vulnerable_dependencies\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2024-21762\",\n", - " \"vuln_package_intel_sources\": [\n", - " \"nvd\"\n", - " ],\n", - " \"vulnerable_sbom_packages\": []\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-hh8p-p8mp-gqhm\",\n", - " \"vuln_package_intel_sources\": [\n", - " \"ghsa\",\n", - " \"nvd\"\n", - " ],\n", - " \"vulnerable_sbom_packages\": [\n", - " {\n", - " \"name\": \"mlflow\",\n", - " \"version\": \"2.9.1\",\n", - " \"vulnerable_dependency_package\": {\n", - " \"system\": \"PYPI\",\n", - " \"name\": \"mlflow\",\n", - " \"version\": \"2.9.1\",\n", - " \"relation\": \"SELF\"\n", - " }\n", - " },\n", - " {\n", - " \"name\": \"mlflow\",\n", - " \"version\": \"2.9.1\",\n", - " \"vulnerable_dependency_package\": {\n", - " \"system\": \"PYPI\",\n", - " \"name\": \"mlflow\",\n", - " \"version\": \"2.9.1\",\n", - " \"relation\": \"SELF\"\n", - " }\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-3f63-hfp8-52jq\",\n", - " \"vuln_package_intel_sources\": [\n", - " \"ghsa\",\n", - " \"nvd\",\n", - " \"ubuntu\",\n", - " \"rhsa\"\n", - " ],\n", - " \"vulnerable_sbom_packages\": [\n", - " {\n", - " \"name\": \"pillow\",\n", - " \"version\": \"10.1.0\",\n", - " \"vulnerable_dependency_package\": {\n", - " \"system\": \"PYPI\",\n", - " \"name\": \"pillow\",\n", - " \"version\": \"10.1.0\",\n", - " \"relation\": \"SELF\"\n", - " }\n", - " },\n", - " {\n", - " \"name\": \"pillow\",\n", - " \"version\": \"10.1.0\",\n", - " \"vulnerable_dependency_package\": {\n", - " \"system\": \"PYPI\",\n", - " \"name\": \"pillow\",\n", - " \"version\": \"10.1.0\",\n", - " \"relation\": \"SELF\"\n", - " }\n", - " }\n", - " ]\n", - " }\n", - " ]\n", - " },\n", - " \"output\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2024-21762\",\n", - " \"checklist\": [\n", - " {\n", - " \"input\": \"Agent bypassed: no vulnerable packages detected. Checklist not generated.\",\n", - " \"response\": \"The VulnerableDependencyChecker did not find any vulnerable packages or dependencies in the SBOM and so the agent was bypassed.\",\n", - " \"intermediate_steps\": null\n", - " }\n", - " ],\n", - " \"summary\": \"The VulnerableDependencyChecker did not find any vulnerable packages or dependencies in the SBOM.\",\n", - " \"justification\": {\n", - " \"label\": \"false_positive\",\n", - " \"reason\": \"No vulnerable packages or dependencies were detected in the SBOM.\",\n", - " \"status\": \"FALSE\"\n", - " }\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-hh8p-p8mp-gqhm\",\n", - " \"checklist\": [\n", - " {\n", - " \"input\": \"Verify Usage of Vulnerable Path Construction: Review the application code within the container image to check if it constructs pathnames using external input. Specifically, look for instances where user input is used to build file paths, which could be exploited to traverse the file system.\",\n", - " \"response\": \"The application code within the container image does construct pathnames using external input, which could potentially be exploited to traverse the file system. Specifically, the code patterns found in the `path.join()` and string concatenation functions may be vulnerable to path traversal attacks. However, without further information about the container image and its configuration, it is difficult to determine the exact risk and potential impact of these vulnerabilities.\",\n", - " \"intermediate_steps\": null\n", - " },\n", - " {\n", - " \"input\": \"Assess Input Validation and Sanitization: Evaluate the robustness of input validation and sanitization practices within the application, particularly for inputs that are used in constructing file paths. Check if the application properly neutralizes '\\\\..\\\\filename' sequences that could resolve to locations outside the intended directory.\",\n", - " \"response\": \"The application does not properly neutralize '\\\\..\\\\filename' sequences that could resolve to locations outside the intended directory. The custom class `MorpheusRelativePath` and the function `get_package_relative_file` do not validate and sanitize file paths constructed from user input, making the application vulnerable to directory traversal attacks. To fix this, the code should use `os.path.normpath` or `os.path.abspath` to normalize the path and remove any directory traversal sequences, and also validate the resulting path to ensure it's within the intended directory.\",\n", - " \"intermediate_steps\": null\n", - " },\n", - " {\n", - " \"input\": \"Inspect File System Access Controls: Review the file system access controls and permissions within the container to determine if an attacker could exploit the vulnerability to access sensitive files or directories. Consider the potential impact of an attacker gaining access to data and models information.\",\n", - " \"response\": \"The container image likely has restricted access controls and permissions to prevent unauthorized access to sensitive files and directories, but the exact specifics couldn't be determined.\",\n", - " \"intermediate_steps\": null\n", - " }\n", - " ],\n", - " \"summary\": \"Based on the provided Checklist and Findings, the CVE is exploitable. Specifically, the application code constructs pathnames using external input, which could be exploited to traverse the file system (Checklist Item 1). Moreover, the application does not properly neutralize directory traversal sequences, making it vulnerable to directory traversal attacks (Checklist Item 2). These findings indicate that the CVE is exploitable, allowing an attacker to potentially access sensitive files or directories.\",\n", - " \"justification\": {\n", - " \"label\": \"vulnerable\",\n", - " \"reason\": \"The analysis indicates that the CVE is exploitable due to the application's construction of pathnames using external input and failure to neutralize directory traversal sequences, making it vulnerable to directory traversal attacks.\",\n", - " \"status\": \"TRUE\"\n", - " }\n", - " },\n", - " {\n", - " \"vuln_id\": \"GHSA-3f63-hfp8-52jq\",\n", - " \"checklist\": [\n", - " {\n", - " \"input\": \"Verify Usage of PIL.ImageMath.eval: Check if the application code within the container image uses the PIL.ImageMath.eval function, specifically with the environment parameter. This function is the target of the vulnerability, and its usage with untrusted input could lead to arbitrary code execution.\",\n", - " \"response\": \"Based on the observations from the Container Image Code QA System and the Container Image Developer Guide QA System, it appears that the application code within the container image does not use the PIL.ImageMath.eval function with the environment parameter, and there is no indication of any dependencies or libraries that might be using it. Therefore, the usage of PIL.ImageMath.eval with untrusted input, which could lead to arbitrary code execution, is unlikely to be a concern for this container image.\",\n", - " \"intermediate_steps\": null\n", - " },\n", - " {\n", - " \"input\": \"Assess Input Handling for PIL.ImageMath.eval: If PIL.ImageMath.eval is used, evaluate how the application handles input data passed to this function, especially focusing on the environment parameter. Ensure that inputs are properly sanitized and validated to prevent potential code injection attacks.\",\n", - " \"response\": \"The container image does not use PIL.ImageMath.eval, but it uses other libraries such as CuPy and Morpheus for mathematical operations. While the provided context does not provide enough information to determine if the inputs are properly sanitized and validated, it is essential to handle input data carefully to prevent potential security risks, such as information leakage across user boundaries. Therefore, it is recommended to implement proper input data sanitization and validation mechanisms when using these libraries.\",\n", - " \"intermediate_steps\": null\n", - " },\n", - " {\n", - " \"input\": \"Review Environment Variable Management: Since the vulnerability involves manipulation of the environment parameter, assess how environment variables are managed within the application and the container. Ensure that sensitive data is not exposed through environment variables and that variables are properly secured to prevent unauthorized access or manipulation.\",\n", - " \"response\": \"The container image's management of environment variables is not clearly documented, and it is unclear if sensitive data is exposed through environment variables or if variables are properly secured to prevent unauthorized access or manipulation. Further investigation is needed to fully assess the security of environment variable management within the container image.\",\n", - " \"intermediate_steps\": null\n", - " }\n", - " ],\n", - " \"summary\": \"Based on the provided Checklist and Findings, the CVE is not exploitable. The investigation found that the application code within the container image does not use the PIL.ImageMath.eval function with the environment parameter, which is the target of the vulnerability. This suggests that the risk of arbitrary code execution through this specific vulnerability is unlikely.\",\n", - " \"justification\": {\n", - " \"label\": \"code_not_reachable\",\n", - " \"reason\": \"The vulnerable code is not executed during runtime because the application code within the container image does not use the PIL.ImageMath.eval function with the environment parameter, which is the target of the vulnerability.\",\n", - " \"status\": \"FALSE\"\n", - " }\n", - " }\n", - " ]\n", - "}\n" - ] - } - ], - "source": [ - "print(json.dumps(first_result, indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Process and display relevant results" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "def process_results(json_file):\n", - " with open(json_file, 'r') as result_in:\n", - " full_input = result_in.read()\n", - " offsets = [res.start() for res in re.finditer('{\"input\":{\"scan\":', full_input)]\n", - " offsets.append(len(full_input))\n", - " outputs = [json.loads(full_input[offsets[i]:offsets[i+1]])['output'] for i in range(len(offsets)-1)]\n", - " flattened = (vuln_line for output in outputs for vuln_line in output)\n", - " df = pd.DataFrame(flattened)\n", - " df[['label', 'reason', 'affected']] = pd.json_normalize(df['justification'])\n", - " df.drop('justification', axis='columns', inplace=True)\n", - " return df" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "tags": [] - }, - "outputs": [ + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def find_repo_root(start=None) -> Path:\n", + " start = (start or Path.cwd()).resolve()\n", + " for candidate in (start, *start.parents):\n", + " if (candidate / 'pyproject.toml').exists() and (candidate / 'README.md').exists():\n", + " return candidate\n", + " raise RuntimeError('Could not find the vulnerability-analysis repository root.')\n", + "\n", + "\n", + "REPO_ROOT = find_repo_root()\n", + "os.chdir(REPO_ROOT)\n", + "print(f'REPO_ROOT = {REPO_ROOT}')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check Local Setup\n", + "\n", + "This cell only checks local files, commands, and environment variables. It does not pull images or call any LLMs.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def resolve_nat_command() -> list:\n", + " if shutil.which('nat'):\n", + " return ['nat']\n", + " if shutil.which('uv'):\n", + " return ['uv', 'run', 'nat']\n", + " raise RuntimeError('Could not find `nat` or `uv` on PATH.')\n", + "\n", + "\n", + "NAT_CMD = resolve_nat_command()\n", + "print('NAT command:', shlex.join(NAT_CMD))\n", + "\n", + "setup_rows = [\n", + " {'check': 'configs/config.yml', 'ok': Path('configs/config.yml').exists(), 'required': True},\n", + " {'check': 'data/examples/pipeline_input.json', 'ok': Path('data/examples/pipeline_input.json').exists(), 'required': False},\n", + " {'check': 'NVIDIA_API_KEY', 'ok': bool(os.getenv('NVIDIA_API_KEY')), 'required': True},\n", + " {'check': 'TAVILY_API_KEY', 'ok': bool(os.getenv('TAVILY_API_KEY')), 'required': True},\n", + " {'check': 'crane', 'ok': bool(shutil.which('crane')), 'required': 'automatic extraction'},\n", + "]\n", + "setup_df = pd.DataFrame(setup_rows)\n", + "display(setup_df)\n", + "\n", + "missing_required = [row['check'] for row in setup_rows if row['required'] is True and not row['ok']]\n", + "if missing_required:\n", + " print('Set or install these before running the full pipeline:', ', '.join(missing_required))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Choose Container Input Mode\n", + "\n", + "`PipelineInput` always includes these fields:\n", + "\n", + "- `vulns`: a list of vulnerability entries, each with `vuln_id`\n", + "- `image_name`: the image repository/name without the tag\n", + "- `image_tag`: the image tag\n", + "\n", + "For the default automatic path, omit `filesystem_path`. The orchestrator pulls `image_name:image_tag` with `crane`, extracts the rootfs into `${VULN_ANALYSIS_IMAGE_CACHE:-.cache/vuln_analysis/images}//rootfs`, and fills in `filesystem_path`, `image_config_path`, and `image_digest` internally. If you already have a merged rootfs, set `use_pre_extracted_filesystem = True` and provide `filesystem_path` instead.\n" + ] + }, { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
vuln_idchecklistsummarylabelreasonaffected
0CVE-2024-21762[{'input': 'Agent bypassed: no vulnerable pack...The VulnerableDependencyChecker did not find a...false_positiveNo vulnerable packages or dependencies were de...FALSE
1GHSA-hh8p-p8mp-gqhm[{'input': 'Verify Usage of Vulnerable Path Co...Based on the provided Checklist and Findings, ...vulnerableThe analysis indicates that the CVE is exploit...TRUE
2GHSA-3f63-hfp8-52jq[{'input': 'Verify Usage of PIL.ImageMath.eval...Based on the provided Checklist and Findings, ...code_not_reachableThe vulnerable code is not executed during run...FALSE
\n", - "
" - ], - "text/plain": [ - " vuln_id checklist \\\n", - "0 CVE-2024-21762 [{'input': 'Agent bypassed: no vulnerable pack... \n", - "1 GHSA-hh8p-p8mp-gqhm [{'input': 'Verify Usage of Vulnerable Path Co... \n", - "2 GHSA-3f63-hfp8-52jq [{'input': 'Verify Usage of PIL.ImageMath.eval... \n", - "\n", - " summary label \\\n", - "0 The VulnerableDependencyChecker did not find a... false_positive \n", - "1 Based on the provided Checklist and Findings, ... vulnerable \n", - "2 Based on the provided Checklist and Findings, ... code_not_reachable \n", - "\n", - " reason affected \n", - "0 No vulnerable packages or dependencies were de... FALSE \n", - "1 The analysis indicates that the CVE is exploit... TRUE \n", - "2 The vulnerable code is not executed during run... FALSE " + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "use_pre_extracted_filesystem = False\n", + "\n", + "image_name = 'registry.example.com/namespace/container'\n", + "image_tag = 'tag'\n", + "vuln_ids = ['CVE-YYYY-NNNN']\n", + "\n", + "if image_name == 'registry.example.com/namespace/container':\n", + " print('Replace image_name with the container image repository/name you want to analyze.')\n", + "if image_tag == 'tag':\n", + " print('Replace image_tag with the container image tag you want to analyze.')\n", + "if vuln_ids == ['CVE-YYYY-NNNN']:\n", + " print('Replace vuln_ids with the CVE or GHSA identifiers you want to analyze.')\n", + "\n", + "# Only used when use_pre_extracted_filesystem = True.\n", + "filesystem_path = Path('/path/to/user-defined-container/filesystem')\n", + "image_config_path = Path('/path/to/user-defined-container/image_config.json')\n", + "\n", + "quick_start_dir = REPO_ROOT / '.tmp' / 'quick_start_v3'\n", + "output_dir = quick_start_dir / 'outputs'\n", + "input_file = quick_start_dir / 'pipeline_input.json'\n", + "run_log_path = quick_start_dir / 'nat_run.log'\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional: Pre-extract an Image\n", + "\n", + "You can skip this section when `use_pre_extracted_filesystem = False`. To warm the cache or debug image extraction separately, run the standalone helper from a terminal:\n", + "\n", + "```bash\n", + "uv run python scripts/extract_image.py /tmp/img-cache\n", + "```\n", + "\n", + "After setting `image_name` and `image_tag`, the next cell prints the matching command. If you run the helper manually, set `use_pre_extracted_filesystem = True`, `filesystem_path` to the printed `rootfs_path`, and `image_config_path` to the printed `config_path`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "container_ref = f'{image_name}:{image_tag}'\n", + "\n", + "extract_cmd = [\n", + " 'uv', 'run', 'python', 'scripts/extract_image.py',\n", + " container_ref,\n", + " str(REPO_ROOT / '.cache' / 'vuln_analysis' / 'images'),\n", + "]\n", + "print('$ ' + shlex.join(extract_cmd))\n", + "print('This is shown for reference. It uses image_name and image_tag from the previous cell.')\n", + "print('Run it manually if you want to pre-extract the user-defined container image.')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Build `PipelineInput`\n", + "\n", + "A `PipelineInput` represents one or more CVE or GHSA identifiers against one container image. Each entry goes in `vulns` as `{vuln_id: ...}`; CVE and GHSA identifiers use the same field.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def make_pipeline_input(\n", + " vuln_ids: str | list[str] | tuple[str, ...],\n", + " *,\n", + " image_name: str,\n", + " image_tag: str,\n", + " use_pre_extracted_filesystem: bool,\n", + " filesystem_path: Path | None,\n", + " image_config_path: Path | None,\n", + " output_dir: Path,\n", + " cache: bool = True,\n", + " skip_web_research: bool = False,\n", + " package_hint=None,\n", + " version_hint=None,\n", + ") -> dict:\n", + " if isinstance(vuln_ids, str):\n", + " vuln_ids = [vuln_ids]\n", + "\n", + " vulns = []\n", + " for vuln_id in vuln_ids:\n", + " entry = {'vuln_id': vuln_id}\n", + " if package_hint:\n", + " entry['package_hint'] = package_hint\n", + " if version_hint:\n", + " entry['version_hint'] = version_hint\n", + " vulns.append(entry)\n", + "\n", + " payload = {\n", + " 'vulns': vulns,\n", + " 'image_name': image_name,\n", + " 'image_tag': image_tag,\n", + " 'output_dir': str(output_dir),\n", + " 'cache': cache,\n", + " 'skip_web_research': skip_web_research,\n", + " }\n", + "\n", + " if use_pre_extracted_filesystem:\n", + " if filesystem_path is None:\n", + " raise ValueError('filesystem_path is required when use_pre_extracted_filesystem is True.')\n", + " payload['filesystem_path'] = str(filesystem_path)\n", + " if image_config_path:\n", + " payload['image_config_path'] = str(image_config_path)\n", + "\n", + " return payload\n", + "\n", + "\n", + "pipeline_input = make_pipeline_input(\n", + " vuln_ids,\n", + " image_name=image_name,\n", + " image_tag=image_tag,\n", + " use_pre_extracted_filesystem=use_pre_extracted_filesystem,\n", + " filesystem_path=filesystem_path,\n", + " image_config_path=image_config_path,\n", + " output_dir=output_dir,\n", + " cache=True,\n", + ")\n", + "\n", + "quick_start_dir.mkdir(parents=True, exist_ok=True)\n", + "input_file.write_text(json.dumps(pipeline_input, indent=2) + '\\n')\n", + "\n", + "print(f'Wrote {input_file.relative_to(REPO_ROOT)}')\n", + "print(json.dumps(pipeline_input, indent=2))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Validate Input Shape\n", + "\n", + "This mirrors the `PipelineInput` rule from the code: provide a non-empty `vulns` list, `image_name` without a tag suffix, and `image_tag`. If `filesystem_path` is omitted, the orchestrator extracts `image_name:image_tag` automatically. If you use a pre-extracted filesystem, the directory must exist before running NAT.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if not pipeline_input.get('vulns'):\n", + " raise ValueError('Provide at least one vulnerability in vulns.')\n", + "\n", + "for entry in pipeline_input['vulns']:\n", + " if not entry.get('vuln_id'):\n", + " raise ValueError('Each vulns entry must include vuln_id.')\n", + "\n", + "if not pipeline_input.get('image_name'):\n", + " raise ValueError('image_name is required.')\n", + "if ':' in pipeline_input['image_name'].rsplit('/', 1)[-1]:\n", + " raise ValueError('image_name must not include a tag; put the tag in image_tag instead.')\n", + "if not pipeline_input.get('image_tag'):\n", + " raise ValueError('image_tag is required.')\n", + "\n", + "has_filesystem_path = bool(pipeline_input.get('filesystem_path'))\n", + "\n", + "if not has_filesystem_path and not shutil.which('crane'):\n", + " print('Warning: `crane` is not on PATH. Install it before running the automatic extraction workflow.')\n", + "\n", + "if has_filesystem_path:\n", + " rootfs = Path(pipeline_input['filesystem_path'])\n", + " if not rootfs.exists():\n", + " raise FileNotFoundError(f'filesystem_path does not exist: {rootfs}')\n", + " if pipeline_input.get('image_config_path'):\n", + " config_path = Path(pipeline_input['image_config_path'])\n", + " if not config_path.exists():\n", + " raise FileNotFoundError(f'image_config_path does not exist: {config_path}')\n", + "\n", + "print('PipelineInput is ready.')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Run the Full Pipeline\n", + "\n", + "This invokes the registered NAT `cve_pipeline` workflow with the generated input file. First runs can take several minutes because they may pull the image, perform web research, and call LLMs. Later runs can reuse the image cache and per-stage report cache.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config_file = Path('configs/config.yml')\n", + "cmd = NAT_CMD + [\n", + " 'run',\n", + " f'--config_file={config_file}',\n", + " f'--input_file={input_file}',\n", + "]\n", + "\n", + "print('$ ' + shlex.join(str(part) for part in cmd))\n", + "completed = subprocess.run(\n", + " [str(part) for part in cmd],\n", + " cwd=REPO_ROOT,\n", + " text=True,\n", + " stdout=subprocess.PIPE,\n", + " stderr=subprocess.STDOUT,\n", + ")\n", + "nat_stdout = completed.stdout\n", + "run_log_path.write_text(nat_stdout)\n", + "print(nat_stdout)\n", + "print(f'Run log: {run_log_path.relative_to(REPO_ROOT)}')\n", + "completed.check_returncode()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parse the NAT Result\n", + "\n", + "`nat run` prints the serialized `PipelineResult`. This helper scans stdout and returns the last JSON object that looks like a pipeline result.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def extract_pipeline_result(text: str) -> dict:\n", + " decoder = json.JSONDecoder()\n", + " matches = []\n", + " for match in re.finditer(r'\\{', text):\n", + " try:\n", + " parsed, _ = decoder.raw_decode(text[match.start():])\n", + " except json.JSONDecodeError:\n", + " continue\n", + " if isinstance(parsed, dict) and {'image_name', 'image_tag', 'results'} <= set(parsed):\n", + " matches.append(parsed)\n", + " if not matches:\n", + " raise ValueError('Could not find a PipelineResult JSON object in NAT stdout.')\n", + " return matches[-1]\n", + "\n", + "\n", + "pipeline_result = extract_pipeline_result(nat_stdout)\n", + "print(json.dumps(pipeline_result, indent=2))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipeline Summary\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "summary = {\n", + " 'image_name': pipeline_result.get('image_name'),\n", + " 'image_tag': pipeline_result.get('image_tag'),\n", + " 'success': pipeline_result.get('success'),\n", + " 'elapsed_seconds': round(pipeline_result.get('elapsed_seconds', 0), 1),\n", + " 'vulnerability_count': len(pipeline_result.get('results', [])),\n", + "}\n", + "display(pd.DataFrame([summary]))\n", + "\n", + "vuln_rows = []\n", + "for result in pipeline_result.get('results', []):\n", + " vuln_rows.append({\n", + " 'vuln_id': result.get('vuln_id'),\n", + " 'success': result.get('success'),\n", + " 'vex_status': result.get('vex_status'),\n", + " 'vex_justification': result.get('vex_justification'),\n", + " 'error': result.get('error'),\n", + " })\n", + "display(pd.DataFrame(vuln_rows))\n", + "\n", + "stage_rows = []\n", + "container_stage = pipeline_result.get('container_analyzer') or {}\n", + "stage_rows.append({\n", + " 'vuln_id': None,\n", + " 'stage': 'container_analyzer',\n", + " 'success': container_stage.get('success'),\n", + " 'report_path': container_stage.get('report_path'),\n", + " 'tool_calls': container_stage.get('tool_call_count'),\n", + " 'llm_calls': container_stage.get('llm_call_count'),\n", + " 'error': container_stage.get('error'),\n", + "})\n", + "\n", + "for result in pipeline_result.get('results', []):\n", + " for name in ['cve_researcher', 'exploitability_analyzer', 'vex_categorizer']:\n", + " stage = result.get(name) or {}\n", + " stage_rows.append({\n", + " 'vuln_id': result.get('vuln_id'),\n", + " 'stage': name,\n", + " 'success': stage.get('success'),\n", + " 'report_path': stage.get('report_path'),\n", + " 'tool_calls': stage.get('tool_call_count'),\n", + " 'llm_calls': stage.get('llm_call_count'),\n", + " 'error': stage.get('error'),\n", + " })\n", + "\n", + "display(pd.DataFrame(stage_rows))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Inspect Outputs\n", + "\n", + "The orchestrator writes outputs under the `output_dir` from `PipelineInput`:\n", + "\n", + "- `vulnerability_reports/`\n", + "- `container_reports/`\n", + "- `exploitability_reports/`\n", + "- `vex_results/`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "resolved_output_dir = Path(pipeline_input['output_dir'])\n", + "if not resolved_output_dir.is_absolute():\n", + " resolved_output_dir = REPO_ROOT / resolved_output_dir\n", + "\n", + "if not resolved_output_dir.exists():\n", + " raise FileNotFoundError(f'Output directory not found: {resolved_output_dir}')\n", + "\n", + "output_files = sorted(path for path in resolved_output_dir.rglob('*') if path.is_file())\n", + "for path in output_files:\n", + " print(path.relative_to(REPO_ROOT))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Final VEX JSON\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vex_json_path = None\n", + "first_vuln_result = pipeline_result['results'][0]\n", + "vex_stage = first_vuln_result.get('vex_categorizer') or {}\n", + "json_path = (vex_stage.get('extra') or {}).get('json_path')\n", + "if json_path:\n", + " candidate = Path(json_path)\n", + " vex_json_path = candidate if candidate.is_absolute() else REPO_ROOT / candidate\n", + "\n", + "if not vex_json_path or not vex_json_path.exists():\n", + " vex_json_files = sorted(\n", + " (resolved_output_dir / 'vex_results').glob('*.json'),\n", + " key=lambda path: path.stat().st_mtime,\n", + " reverse=True,\n", + " )\n", + " if not vex_json_files:\n", + " raise FileNotFoundError(f\"No VEX JSON files found under {resolved_output_dir / 'vex_results'}\")\n", + " vex_json_path = vex_json_files[0]\n", + "\n", + "vex_result = json.loads(vex_json_path.read_text())\n", + "print(f'Loaded {vex_json_path.relative_to(REPO_ROOT)}')\n", + "\n", + "display(pd.DataFrame([{\n", + " 'vuln_id': vex_result.get('vuln_id') or vex_result.get('cve_id') or vex_result.get('ghsa_id'),\n", + " 'container': vex_result.get('container'),\n", + " 'status': vex_result.get('status'),\n", + " 'justification': vex_result.get('justification'),\n", + " 'exploitability_verdict': vex_result.get('exploitability_verdict'),\n", + " 'confidence': vex_result.get('confidence'),\n", + " 'risk_level': vex_result.get('risk_level'),\n", + " 'container_type': vex_result.get('container_type'),\n", + "}]))\n", + "\n", + "print(json.dumps(vex_result, indent=2))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## View VEX Markdown Report\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "report_path = Path(vex_result['report_path'])\n", + "if not report_path.is_absolute():\n", + " report_path = REPO_ROOT / report_path\n", + "\n", + "print(report_path)\n", + "display(Markdown(report_path.read_text()))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Run More Vulnerabilities\n", + "\n", + "A `PipelineInput` can analyze multiple CVE or GHSA identifiers against the same container image. The cell below prepares a multi-vulnerability input without launching it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "more_vuln_ids = [\n", + " 'CVE-YYYY-NNNN',\n", + " 'GHSA-xxxx-xxxx-xxxx',\n", + "]\n", + "\n", + "multi_vuln_input = make_pipeline_input(\n", + " more_vuln_ids,\n", + " image_name=image_name,\n", + " image_tag=image_tag,\n", + " use_pre_extracted_filesystem=use_pre_extracted_filesystem,\n", + " filesystem_path=filesystem_path,\n", + " image_config_path=image_config_path,\n", + " output_dir=output_dir,\n", + " cache=True,\n", + ")\n", + "\n", + "print(json.dumps(multi_vuln_input, indent=2))\n", + "pd.DataFrame(multi_vuln_input['vulns'])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Optional: Individual Agents\n", + "\n", + "The README also exposes each agent as an independent NAT workflow for targeted debugging:\n", + "\n", + "```bash\n", + "nat run --config_file=configs/config-cve-researcher.yml --input_file=data/examples/cve_researcher_input.json\n", + "nat run --config_file=configs/config-container-analyzer.yml --input_file=data/examples/container_analyzer_input.json\n", + "nat run --config_file=configs/config-exploitability-analyzer.yml --input_file=data/examples/exploitability_analyzer_input.json\n", + "nat run --config_file=configs/config-vex-categorizer.yml --input_file=data/examples/vex_categorizer_input.json\n", + "```\n", + "\n", + "For the Exploitability Analyzer and VEX Categorizer inputs, update the report paths to files produced by a previous full-pipeline run.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Optional: HTTP Server\n", + "\n", + "To use the client-server style, start the NAT server from a separate terminal:\n", + "\n", + "```bash\n", + "nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466\n", + "```\n", + "\n", + "Send a JSON body matching `PipelineInput` to the endpoint exposed by your NAT API server version. The `pipeline_input` dictionary created above is the request body.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example only. Confirm the route for your NAT API server version before running.\n", + "# import requests\n", + "# response = requests.post('http://localhost:26466/generate', json=pipeline_input, timeout=3600)\n", + "# response.raise_for_status()\n", + "# response.json()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cleanup Notes\n", + "\n", + "This notebook writes quick start artifacts under `.tmp/quick_start_v3/`. Image extraction caches live under `${VULN_ANALYSIS_IMAGE_CACHE:-.cache/vuln_analysis/images}` when `filesystem_path` is omitted and automatic extraction is used. Per-agent outputs are regular Markdown and JSON files, so you can inspect, archive, diff, or delete them directly. Set `cache` to `False` in `PipelineInput` when you want fresh agent reports.\n" ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" } - ], - "source": [ - "process_results(output_file_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "## View Markdown Reports\n", - "\n", - "In addition to the raw JSON output object, you can also view a Markdown-formatted report for each CVE in the `.tmp/quick_start_results/vulnerability_markdown_reports` directory. For example, the report for `GHSA-3f63-hfp8-52jq` can be viewed at `.tmp/quick_start_results/vulnerability_markdown_reports/vulnerability_reports_morpheus_23_11-runtime/GHSA-3f63-hfp8-52jq.md`." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.10" - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/scripts/extract_image.py b/scripts/extract_image.py new file mode 100755 index 000000000..a09dff69a --- /dev/null +++ b/scripts/extract_image.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Run :func:`vuln_analysis.utils.image_extraction.materialize_image` standalone. + +Pulls a container image via ``crane`` and extracts a squashed rootfs into a +cache directory keyed by image digest. Useful for warming the cache or +debugging extraction outside of the full pipeline workflow. + +Only ``image_ref`` and ``target_dir`` are required. ``--crane-bin`` and +``--tar-bin`` default to ``crane`` and ``tar`` on ``PATH`` and only need to be +set when pointing at a non-PATH binary. + +Run from the repo root via uv (resolves the project venv automatically):: + + uv run python scripts/extract_image.py +""" + +from __future__ import annotations + +import argparse +import logging +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_SRC = _REPO_ROOT / "src" +if _SRC.is_dir() and str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +from vuln_analysis.utils.image_extraction import ImageMaterializationError # noqa: E402 +from vuln_analysis.utils.image_extraction import materialize_image # noqa: E402 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("image_ref", help="Container image reference (e.g. nvcr.io/foo/bar:tag)") + parser.add_argument( + "target_dir", + type=Path, + help="Cache root directory; rootfs is extracted under //rootfs", + ) + parser.add_argument( + "--no-cache", + action="store_true", + help="Force re-extraction even if the digest is already materialized", + ) + parser.add_argument("--crane-bin", default="crane", help="Path to the crane binary") + parser.add_argument("--tar-bin", default="tar", help="Path to the tar binary") + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable debug-level logging" + ) + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + + try: + result = materialize_image( + args.image_ref, + args.target_dir, + use_cache=not args.no_cache, + crane_bin=args.crane_bin, + tar_bin=args.tar_bin, + ) + except ImageMaterializationError as e: + print(f"error: {e}", file=sys.stderr) + return 1 + + print(f"image_ref: {result.pull_ref}") + print(f"digest: {result.digest}") + print(f"rootfs_path: {result.rootfs_path}") + print(f"config_path: {result.config_path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/vuln_analysis/configs/config-container-analyzer.yml b/src/vuln_analysis/configs/config-container-analyzer.yml new file mode 100644 index 000000000..2f7cf0fa7 --- /dev/null +++ b/src/vuln_analysis/configs/config-container-analyzer.yml @@ -0,0 +1,56 @@ +general: + use_uvloop: true + +llms: + container_phase1_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 4096 + container_phase2_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + +functions: + web_search: + _type: web_search + search_depth: advanced + max_results: 5 + web_fetch: + _type: web_fetch + timeout: 30 + max_content_length: 50000 + read_file: + _type: read_file + + list_directory: + _type: list_directory + + grep_search: + _type: grep_search + + glob_find: + _type: glob_find + +workflow: + _type: container_analyzer + phase1_llm_name: container_phase1_llm + phase2_llm_name: container_phase2_llm + web_tool_names: [web_search, web_fetch] + fs_tool_names: [read_file, list_directory, grep_search, glob_find] + phase1_max_iterations: 8 + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + public_registry_prefixes: + - "nvcr.io/" + - "docker.io/" + - "gcr.io/" + - "ghcr.io/" + - "registry.hub.docker.com/" + output_subdir: container_reports diff --git a/src/vuln_analysis/configs/config-cve-report-eval.yml b/src/vuln_analysis/configs/config-cve-report-eval.yml new file mode 100644 index 000000000..31cb56bfb --- /dev/null +++ b/src/vuln_analysis/configs/config-cve-report-eval.yml @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Evaluation config for comparing generated CVE researcher reports against +# gold-standard report markdown files with an LLM-as-judge. +# +# Usage: +# nat eval --config_file src/vuln_analysis/configs/config-cve-report-eval.yml +# +# The dataset file_path is a directory, not a CSV/JSON file. + +general: + use_uvloop: true + +llms: + cve_researcher_llm: + _type: nim + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + cve_report_judge_llm: + _type: nim + model_name: ${CVE_REPORT_JUDGE_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.0 + max_tokens: 4096 + +functions: + web_search: + _type: web_search + search_depth: advanced + max_results: 5 + web_fetch: + _type: web_fetch + timeout: 30 + max_content_length: 50000 + osv_lookup: + _type: osv_lookup + +workflow: + _type: cve_researcher + llm_name: cve_researcher_llm + tool_names: [web_search, web_fetch, osv_lookup] + max_iterations: 2 + data_sources: null + include_middleware: true + verbose: true + output_subdir: vulnerability_reports + +eval: + general: + output_dir: .tmp/outputs + max_concurrency: 2 + dataset: + _type: custom + file_path: data/eval_datasets/cve_report_benchmark + function: vuln_analysis.eval.parse_cve_report_directory.parse_input + + evaluators: + cve_report_judge: + _type: cve_report_judge + llm_name: cve_report_judge_llm + max_retries: 3 + save_generated_reports: true diff --git a/src/vuln_analysis/configs/config-cve-researcher.yml b/src/vuln_analysis/configs/config-cve-researcher.yml new file mode 100644 index 000000000..4e69f2f0a --- /dev/null +++ b/src/vuln_analysis/configs/config-cve-researcher.yml @@ -0,0 +1,32 @@ +general: + use_uvloop: true + +llms: + cve_researcher_llm: + _type: nim + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + +functions: + web_search: + _type: web_search + search_depth: advanced + max_results: 5 + web_fetch: + _type: web_fetch + timeout: 30 + max_content_length: 50000 + osv_lookup: + _type: osv_lookup + +workflow: + _type: cve_researcher + llm_name: cve_researcher_llm + tool_names: [web_search, web_fetch, osv_lookup] + max_iterations: 2 + data_sources: null + include_middleware: true + verbose: true + output_subdir: vulnerability_reports diff --git a/src/vuln_analysis/configs/config-eval.yml b/src/vuln_analysis/configs/config-eval.yml index c3aedf564..510149fbe 100644 --- a/src/vuln_analysis/configs/config-eval.yml +++ b/src/vuln_analysis/configs/config-eval.yml @@ -13,205 +13,229 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Evaluation config for the CVE pipeline benchmark. +# +# Usage: +# nat eval --config_file configs/config-eval.yml +# +# Override dataset: +# nat eval --config_file configs/config-eval.yml \ +# --override eval.general.dataset.file_path data/eval_datasets/morpheus_benchmark_2506.csv + general: use_uvloop: true -functions: - cve_generate_vdbs: - _type: cve_generate_vdbs - agent_name: cve_agent_executor # Used to determine which tools are enabled - embedder_name: nim_embedder - base_git_dir: .cache/am_cache/git - base_vdb_dir: .cache/am_cache/vdb - base_code_index_dir: .cache/am_cache/code_index - cve_fetch_intel: - _type: cve_fetch_intel - retry_on_client_errors: true - max_retries: 5 - request_timeout: 30 - intel_source_timeout: null - cve_process_sbom: - _type: cve_process_sbom - cve_check_vuln_deps : - _type: cve_check_vuln_deps - skip: false - cve_checklist: - _type: cve_checklist - llm_name: default_llm - Container Image Code QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: code - return_source_documents: false - Container Image Developer Guide QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: doc - return_source_documents: false - Lexical Search Container Image Code QA System: - _type: lexical_code_search - top_k: 5 - Internet Search: - _type: serp_wrapper - max_retries: 5 - cve_agent_executor: - _type: cve_agent_executor - llm_name: default_llm - tool_names: - - Container Image Code QA System - - Container Image Developer Guide QA System - # - Lexical Search Container Image Code QA System # Uncomment to enable lexical search - - Internet Search - max_iterations: 10 - prompt_examples: false - replace_exceptions: true - replace_exceptions_value: "I do not have a definitive answer for this checklist item." - return_intermediate_steps: false - verbose: false - cve_summarize: - _type: cve_summarize - llm_name: default_llm - cve_justify: - _type: cve_justify - llm_name: default_llm - cve_file_output: - _type: cve_file_output - file_path: .tmp/output.json - markdown_dir: .tmp/vulnerability_markdown_reports - overwrite: true - llms: - default_llm: + cve_researcher_llm: _type: nim - base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - model_name: ${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct} - temperature: 0.0 - max_tokens: 2000 - top_p: 0.01 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # To use a different LLM configuration for a given function, uncomment the relevant configuration below - # and update the `llm_name` in the functions section to use the desired LLM. - # checklist_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CHECKLIST_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # code_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CODE_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # doc_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${DOC_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # cve_agent_executor_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CVE_AGENT_EXECUTOR_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # summarize_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${SUMMARIZE_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # justify_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${JUSTIFY_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - -embedders: - nim_embedder: + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + container_phase1_llm: _type: nim - base_url: ${NIM_EMBED_BASE_URL:-https://integrate.api.nvidia.com/v1} - model_name: ${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} - truncate: END - max_batch_size: 128 - do_auto_retry: True - max_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 4096 + + container_phase2_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + exploitability_llm: + _type: nim + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 16384 + + vex_llm: + _type: nim + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.0 + max_tokens: 4096 + +functions: + web_search: + _type: web_search + search_depth: advanced + max_results: 5 + + web_fetch: + _type: web_fetch + timeout: 30 + max_content_length: 50000 + + osv_lookup: + _type: osv_lookup + + read_file: + _type: read_file + + list_directory: + _type: list_directory + + grep_search: + _type: grep_search + + glob_find: + _type: glob_find + + check_binary_dependencies: + _type: check_binary_dependencies + + search_binary_content: + _type: search_binary_content + + check_distro_patches: + _type: check_distro_patches + + get_binary_linked_libraries: + _type: get_binary_linked_libraries + + query_dpkg_database: + _type: query_dpkg_database + + search_dpkg_file_lists: + _type: search_dpkg_file_lists + + query_rpm_database: + _type: query_rpm_database + + list_python_packages: + _type: list_python_packages + + find_shared_libraries: + _type: find_shared_libraries + + find_go_binaries: + _type: find_go_binaries + + check_security_controls: + _type: check_security_controls + + query_npm_packages: + _type: query_npm_packages + + find_java_packages: + _type: find_java_packages + + list_installed_packages: + _type: list_installed_packages + + cve_researcher: + _type: cve_researcher + llm_name: cve_researcher_llm + tool_names: [web_search, web_fetch, osv_lookup] + max_iterations: 2 + data_sources: null + include_middleware: true + verbose: true + output_subdir: vulnerability_reports + + container_analyzer: + _type: container_analyzer + phase1_llm_name: container_phase1_llm + phase2_llm_name: container_phase2_llm + web_tool_names: [web_search, web_fetch] + fs_tool_names: [read_file, list_directory, grep_search, glob_find] + phase1_max_iterations: 8 + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + public_registry_prefixes: + - "nvcr.io/" + - "docker.io/" + - "gcr.io/" + - "ghcr.io/" + - "registry.hub.docker.com/" + output_subdir: container_reports + + exploitability_analyzer: + _type: exploitability_analyzer + llm_name: exploitability_llm + tool_names: + - read_file + - list_directory + - grep_search + - glob_find + - check_binary_dependencies + - search_binary_content + - check_distro_patches + - get_binary_linked_libraries + - query_dpkg_database + - search_dpkg_file_lists + - query_rpm_database + - list_python_packages + - find_shared_libraries + - find_go_binaries + - check_security_controls + - query_npm_packages + - find_java_packages + - list_installed_packages + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + discovery_probe_paths: + - "opt/conda/envs" + - "opt/conda" + - "usr/lib" + - "usr/local/lib" + - "usr/local/bin" + - "workspace" + - "app" + - "opt" + - "srv" + output_subdir: exploitability_reports + + vex_categorizer: + _type: vex_categorizer + llm_name: vex_llm + max_iterations: 10 + loop_detection_threshold: 2 + output_subdir: vex_results workflow: - _type: cve_agent - cve_generate_vdbs_name: cve_generate_vdbs - cve_fetch_intel_name: cve_fetch_intel - cve_process_sbom_name: cve_process_sbom - cve_check_vuln_deps_name: cve_check_vuln_deps - cve_checklist_name: cve_checklist - cve_agent_executor_name: cve_agent_executor - cve_summarize_name: cve_summarize - cve_justify_name: cve_justify - cve_output_config_name: cve_file_output - llm_max_rate: 5 # Global max rate (requests/second) for all functions; individual function settings override this + _type: cve_pipeline + cve_researcher_name: cve_researcher + container_analyzer_name: container_analyzer + exploitability_analyzer_name: exploitability_analyzer + vex_categorizer_name: vex_categorizer + stage_timeout: 600 + stage_max_retries: 1 + # Max parallel agent tasks per stage. null (or 0) = unlimited. + # Set to a positive integer to cap concurrent agent executions per pipeline stage. + max_concurrency: null + # Global LLM rate limit (requests/second) shared across all agent functions; per-function + # settings override this. null = no limit. Set a number only if the LLM endpoint enforces + # rate limiting (the upstream blueprint used 5 for that reason). + llm_max_rate: null + output_dir: outputs eval: general: - output_dir: ./.tmp/evaluators + output_dir: .tmp/evaluators + max_concurrency: 2 dataset: - _type: custom - file_path: data/eval_datasets/eval_dataset.json - function: vuln_analysis.eval.parse_eval_input.parse_input - max_concurrency: 1 + _type: custom + file_path: data/eval_datasets/morpheus_benchmark_2506.csv + function: vuln_analysis.eval.parse_eval_input.parse_input evaluators: - status_accuracy: + accuracy_exclude: + _type: accuracy + uncertain_mode: exclude + accuracy_wrong: _type: accuracy - field: status - duplicates_policy: drop_all # Options: drop_all, keep_first, keep_all - label_accuracy: + uncertain_mode: wrong + accuracy_vulnerable: _type: accuracy - field: label - duplicates_policy: drop_all - - # Uncomment to enable consistency evaluator - # NOTE: Only use this evaluator if evaluation is being run with multiple repetitions - # status_consistency: - # _type: consistency - # field: status - # label_consistency: - # _type: consistency - # field: label + uncertain_mode: vulnerable diff --git a/src/vuln_analysis/configs/config-exploitability-analyzer.yml b/src/vuln_analysis/configs/config-exploitability-analyzer.yml new file mode 100644 index 000000000..9f67714d9 --- /dev/null +++ b/src/vuln_analysis/configs/config-exploitability-analyzer.yml @@ -0,0 +1,102 @@ +general: + use_uvloop: true + +llms: + exploitability_llm: + _type: nim + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 16384 + +functions: + read_file: + _type: read_file + + list_directory: + _type: list_directory + + grep_search: + _type: grep_search + + glob_find: + _type: glob_find + + check_binary_dependencies: + _type: check_binary_dependencies + + search_binary_content: + _type: search_binary_content + + check_distro_patches: + _type: check_distro_patches + + get_binary_linked_libraries: + _type: get_binary_linked_libraries + + query_dpkg_database: + _type: query_dpkg_database + + search_dpkg_file_lists: + _type: search_dpkg_file_lists + + query_rpm_database: + _type: query_rpm_database + + list_python_packages: + _type: list_python_packages + + find_shared_libraries: + _type: find_shared_libraries + + find_go_binaries: + _type: find_go_binaries + + check_security_controls: + _type: check_security_controls + + query_npm_packages: + _type: query_npm_packages + + find_java_packages: + _type: find_java_packages + + list_installed_packages: + _type: list_installed_packages + +workflow: + _type: exploitability_analyzer + llm_name: exploitability_llm + tool_names: + - read_file + - list_directory + - grep_search + - glob_find + - check_binary_dependencies + - search_binary_content + - check_distro_patches + - get_binary_linked_libraries + - query_dpkg_database + - search_dpkg_file_lists + - query_rpm_database + - list_python_packages + - find_shared_libraries + - find_go_binaries + - check_security_controls + - query_npm_packages + - find_java_packages + - list_installed_packages + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + discovery_probe_paths: + - "opt/conda/envs" + - "opt/conda" + - "usr/lib" + - "usr/local/lib" + - "usr/local/bin" + - "workspace" + - "app" + - "opt" + - "srv" + output_subdir: exploitability_reports diff --git a/src/vuln_analysis/configs/config-relaxed-vex.yml b/src/vuln_analysis/configs/config-relaxed-vex.yml new file mode 100644 index 000000000..be05f6721 --- /dev/null +++ b/src/vuln_analysis/configs/config-relaxed-vex.yml @@ -0,0 +1,191 @@ +general: + use_uvloop: true + +llms: + cve_researcher_llm: + _type: nim + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + container_phase1_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 4096 + + container_phase2_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + exploitability_llm: + _type: nim + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 16384 + + vex_llm: + _type: nim + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.0 + max_tokens: 4096 + +functions: + web_search: + _type: web_search + search_depth: advanced + max_results: 5 + + web_fetch: + _type: web_fetch + timeout: 30 + max_content_length: 50000 + + osv_lookup: + _type: osv_lookup + + read_file: + _type: read_file + + list_directory: + _type: list_directory + + grep_search: + _type: grep_search + + glob_find: + _type: glob_find + + check_binary_dependencies: + _type: check_binary_dependencies + + search_binary_content: + _type: search_binary_content + + check_distro_patches: + _type: check_distro_patches + + get_binary_linked_libraries: + _type: get_binary_linked_libraries + + query_dpkg_database: + _type: query_dpkg_database + + search_dpkg_file_lists: + _type: search_dpkg_file_lists + + query_rpm_database: + _type: query_rpm_database + + list_python_packages: + _type: list_python_packages + + find_shared_libraries: + _type: find_shared_libraries + + find_go_binaries: + _type: find_go_binaries + + check_security_controls: + _type: check_security_controls + + query_npm_packages: + _type: query_npm_packages + + find_java_packages: + _type: find_java_packages + + list_installed_packages: + _type: list_installed_packages + + cve_researcher: + _type: cve_researcher + llm_name: cve_researcher_llm + tool_names: [web_search, web_fetch, osv_lookup] + max_iterations: 30 + data_sources: null + include_middleware: true + verbose: true + output_subdir: vulnerability_reports + + container_analyzer: + _type: container_analyzer + phase1_llm_name: container_phase1_llm + phase2_llm_name: container_phase2_llm + web_tool_names: [web_search, web_fetch] + fs_tool_names: [read_file, list_directory, grep_search, glob_find] + phase1_max_iterations: 8 + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + public_registry_prefixes: + - "nvcr.io/" + - "docker.io/" + - "gcr.io/" + - "ghcr.io/" + - "registry.hub.docker.com/" + output_subdir: container_reports + + exploitability_analyzer: + _type: exploitability_analyzer + llm_name: exploitability_llm + tool_names: + - read_file + - list_directory + - grep_search + - glob_find + - check_binary_dependencies + - search_binary_content + - check_distro_patches + - get_binary_linked_libraries + - query_dpkg_database + - search_dpkg_file_lists + - query_rpm_database + - list_python_packages + - find_shared_libraries + - find_go_binaries + - check_security_controls + - query_npm_packages + - find_java_packages + - list_installed_packages + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + discovery_probe_paths: + - "opt/conda/envs" + - "opt/conda" + - "usr/lib" + - "usr/local/lib" + - "usr/local/bin" + - "workspace" + - "app" + - "opt" + - "srv" + output_subdir: exploitability_reports + + vex_categorizer: + _type: custom_vex_categorizer + llm_name: vex_llm + prompt: src/vuln_analysis/prompts/relaxed_vex_categorizer.prompt.md + max_iterations: 10 + loop_detection_threshold: 2 + output_subdir: vex_results + +workflow: + _type: cve_pipeline + cve_researcher_name: cve_researcher + container_analyzer_name: container_analyzer + exploitability_analyzer_name: exploitability_analyzer + vex_categorizer_name: vex_categorizer + stage_timeout: 600 + stage_max_retries: 1 + output_dir: .tmp/outputs diff --git a/src/vuln_analysis/configs/config-tracing.yml b/src/vuln_analysis/configs/config-tracing.yml deleted file mode 100644 index 7940c107a..000000000 --- a/src/vuln_analysis/configs/config-tracing.yml +++ /dev/null @@ -1,223 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -general: - use_uvloop: true - telemetry: - logging: - console: - _type: console - level: WARN - tracing: - phoenix: - _type: phoenix - endpoint: http://localhost:6006/v1/traces - project: cve_agent - -functions: - cve_generate_vdbs: - _type: cve_generate_vdbs - agent_name: cve_agent_executor # Used to determine which tools are enabled - embedder_name: nim_embedder - base_git_dir: .cache/am_cache/git - base_vdb_dir: .cache/am_cache/vdb - base_code_index_dir: .cache/am_cache/code_index - cve_fetch_intel: - _type: cve_fetch_intel - retry_on_client_errors: true - max_retries: 5 - request_timeout: 30 - intel_source_timeout: null - cve_process_sbom: - _type: cve_process_sbom - cve_check_vuln_deps : - _type: cve_check_vuln_deps - skip: false - cve_checklist: - _type: cve_checklist - llm_name: default_llm - Container Image Code QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: code - return_source_documents: false - Container Image Developer Guide QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: doc - return_source_documents: false - Lexical Search Container Image Code QA System: - _type: lexical_code_search - top_k: 5 - Internet Search: - _type: serp_wrapper - max_retries: 5 - cve_agent_executor: - _type: cve_agent_executor - llm_name: default_llm - tool_names: - - Container Image Code QA System - - Container Image Developer Guide QA System - # - Lexical Search Container Image Code QA System # Uncomment to enable lexical search - - Internet Search - max_iterations: 10 - prompt_examples: false - replace_exceptions: true - replace_exceptions_value: "I do not have a definitive answer for this checklist item." - return_intermediate_steps: false - verbose: false - cve_summarize: - _type: cve_summarize - llm_name: default_llm - cve_justify: - _type: cve_justify - llm_name: default_llm - cve_file_output: - _type: cve_file_output - file_path: .tmp/output.json - markdown_dir: .tmp/vulnerability_markdown_reports - overwrite: true - -llms: - default_llm: - _type: nim - base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - model_name: ${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct} - temperature: 0.0 - max_tokens: 2000 - top_p: 0.01 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # To use a different LLM configuration for a given function, uncomment the relevant configuration below - # and update the `llm_name` in the functions section to use the desired LLM. - # checklist_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CHECKLIST_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # code_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CODE_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # doc_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${DOC_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # cve_agent_executor_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CVE_AGENT_EXECUTOR_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # summarize_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${SUMMARIZE_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # justify_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${JUSTIFY_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - -embedders: - nim_embedder: - _type: nim - base_url: ${NIM_EMBED_BASE_URL:-https://integrate.api.nvidia.com/v1} - model_name: ${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} - truncate: END - max_batch_size: 128 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - - -workflow: - _type: cve_agent - cve_generate_vdbs_name: cve_generate_vdbs - cve_fetch_intel_name: cve_fetch_intel - cve_process_sbom_name: cve_process_sbom - cve_check_vuln_deps_name: cve_check_vuln_deps - cve_checklist_name: cve_checklist - cve_agent_executor_name: cve_agent_executor - cve_summarize_name: cve_summarize - cve_justify_name: cve_justify - cve_output_config_name: cve_file_output - missing_source_action: continue_with_warning - llm_max_rate: 5 # Global max rate (requests/second) for all functions; individual function settings override this - -eval: - general: - output_dir: ./.tmp/profiler - dataset: - _type: json - file_path: data/profiler_datasets/profiler_dataset.json - - profiler: - token_uniqueness_forecast: true - workflow_runtime_forecast: true - compute_llm_metrics: true - csv_exclude_io_text: true - prompt_caching_prefixes: - enable: true - min_frequency: 0.1 - bottleneck_analysis: - # Can also be simple_stack - enable_nested_stack: true - concurrency_spike_analysis: - enable: true - spike_threshold: 7 diff --git a/src/vuln_analysis/configs/config-ttc.yml b/src/vuln_analysis/configs/config-ttc.yml deleted file mode 100644 index 392b4bde9..000000000 --- a/src/vuln_analysis/configs/config-ttc.yml +++ /dev/null @@ -1,203 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -general: - use_uvloop: true - -functions: - cve_generate_vdbs: - _type: cve_generate_vdbs - agent_name: cve_agent_executor # Used to determine which tools are enabled - embedder_name: nim_embedder - base_git_dir: .cache/am_cache/git - base_vdb_dir: .cache/am_cache/vdb - base_code_index_dir: .cache/am_cache/code_index - cve_fetch_intel: - _type: cve_fetch_intel - retry_on_client_errors: true - max_retries: 5 - request_timeout: 30 - intel_source_timeout: null - cve_process_sbom: - _type: cve_process_sbom - cve_check_vuln_deps : - _type: cve_check_vuln_deps - skip: false - cve_checklist: - _type: cve_checklist - llm_name: default_llm - Container Image Code QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: code - return_source_documents: false - Container Image Developer Guide QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: doc - return_source_documents: false - Lexical Search Container Image Code QA System: - _type: lexical_code_search - top_k: 5 - Internet Search: - _type: serp_wrapper - max_retries: 5 - cve_agent_executor: - _type: cve_agent_executor - llm_name: default_llm - tool_names: - - Container Image Code QA System - - Container Image Developer Guide QA System - # - Lexical Search Container Image Code QA System # Uncomment to enable lexical search - - Internet Search - max_iterations: 10 - prompt_examples: false - replace_exceptions: true - replace_exceptions_value: "I do not have a definitive answer for this checklist item." - return_intermediate_steps: false - verbose: false - cve_summarize: - _type: cve_summarize - llm_name: default_llm - cve_justify: - _type: cve_justify - llm_name: default_llm - cve_file_output: - _type: cve_file_output - file_path: .tmp/output.json - markdown_dir: .tmp/vulnerability_markdown_reports - overwrite: true - cve_agent_workflow: - _type: cve_agent - cve_generate_vdbs_name: cve_generate_vdbs - cve_fetch_intel_name: cve_fetch_intel - cve_process_sbom_name: cve_process_sbom - cve_check_vuln_deps_name: cve_check_vuln_deps - cve_checklist_name: cve_checklist - cve_agent_executor_name: cve_agent_executor - cve_summarize_name: cve_summarize - cve_justify_name: cve_justify - cve_output_config_name: null # Disable base pipeline output when using TTC - missing_source_action: continue_with_warning - llm_max_rate: 5 # Global max rate (requests/second) for workflow functions; individual function settings override this - -llms: - default_llm: - _type: nim - base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - model_name: ${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct} - temperature: 0.0 - max_tokens: 2000 - top_p: 0.01 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # To use a different LLM configuration for a given function, uncomment the relevant configuration below - # and update the `llm_name` in the functions section to use the desired LLM. - # checklist_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CHECKLIST_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # code_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CODE_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # doc_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${DOC_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # cve_agent_executor_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CVE_AGENT_EXECUTOR_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # summarize_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${SUMMARIZE_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # justify_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${JUSTIFY_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - -embedders: - nim_embedder: - _type: nim - base_url: ${NIM_EMBED_BASE_URL:-https://integrate.api.nvidia.com/v1} - model_name: ${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} - truncate: END - max_batch_size: 128 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - -ttc_strategies: - selection_strategy_majority_voting: - _type: majority_voting_selection - selection_mode: decisive - -workflow: - _type: execute_then_select_function - selector: selection_strategy_majority_voting - augmented_fn: cve_agent_workflow - output_fn: cve_file_output # Output results post-TTC - num_executions: 3 - early_stop_threshold: false - max_concurrency: 1 diff --git a/src/vuln_analysis/configs/config-vex-categorizer.yml b/src/vuln_analysis/configs/config-vex-categorizer.yml new file mode 100644 index 000000000..34b1c7b28 --- /dev/null +++ b/src/vuln_analysis/configs/config-vex-categorizer.yml @@ -0,0 +1,16 @@ +general: + use_uvloop: true + +llms: + vex_llm: + _type: nim + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.0 + max_tokens: 4096 + +workflow: + _type: vex_categorizer + llm_name: vex_llm + max_iterations: 10 + output_subdir: vex_results diff --git a/src/vuln_analysis/configs/config.yml b/src/vuln_analysis/configs/config.yml index 66d0bd3f5..084c783c1 100644 --- a/src/vuln_analysis/configs/config.yml +++ b/src/vuln_analysis/configs/config.yml @@ -1,213 +1,197 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - general: use_uvloop: true -functions: - cve_generate_vdbs: - _type: cve_generate_vdbs - agent_name: cve_agent_executor # Used to determine which tools are enabled - embedder_name: nim_embedder - base_git_dir: .cache/am_cache/git - base_vdb_dir: .cache/am_cache/vdb - base_code_index_dir: .cache/am_cache/code_index - cve_fetch_intel: - _type: cve_fetch_intel - retry_on_client_errors: true - max_retries: 5 - request_timeout: 30 - intel_source_timeout: null - cve_process_sbom: - _type: cve_process_sbom - cve_check_vuln_deps : - _type: cve_check_vuln_deps - skip: false - cve_checklist: - _type: cve_checklist - llm_name: default_llm - Container Image Code QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: code - return_source_documents: false - Container Image Developer Guide QA System: - _type: local_vdb_retriever - embedder_name: nim_embedder - llm_name: default_llm - vdb_type: doc - return_source_documents: false - Lexical Search Container Image Code QA System: - _type: lexical_code_search - top_k: 5 - Internet Search: - _type: serp_wrapper - max_retries: 5 - cve_agent_executor: - _type: cve_agent_executor - llm_name: default_llm - tool_names: - - Container Image Code QA System - - Container Image Developer Guide QA System - # - Lexical Search Container Image Code QA System # Uncomment to enable lexical search - - Internet Search - max_iterations: 10 - prompt_examples: false - replace_exceptions: true - replace_exceptions_value: "I do not have a definitive answer for this checklist item." - return_intermediate_steps: false - verbose: false - cve_summarize: - _type: cve_summarize - llm_name: default_llm - cve_justify: - _type: cve_justify - llm_name: default_llm - cve_file_output: - _type: cve_file_output - file_path: .tmp/output.json - markdown_dir: .tmp/vulnerability_markdown_reports - overwrite: true - llms: - default_llm: + cve_researcher_llm: _type: nim - base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - model_name: ${DEFAULT_MODEL_NAME:-meta/llama-3.1-70b-instruct} - temperature: 0.0 - max_tokens: 2000 - top_p: 0.01 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # To use a different LLM configuration for a given function, uncomment the relevant configuration below - # and update the `llm_name` in the functions section to use the desired LLM. - # checklist_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CHECKLIST_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # code_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CODE_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # doc_vdb_retriever_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${DOC_VDB_RETRIEVER_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # cve_agent_executor_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${CVE_AGENT_EXECUTOR_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 2000 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # summarize_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${SUMMARIZE_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - # justify_llm: - # _type: nim - # base_url: ${NVIDIA_API_BASE:-https://integrate.api.nvidia.com/v1} - # model_name: ${JUSTIFY_MODEL_NAME:-meta/llama-3.1-70b-instruct} - # temperature: 0.0 - # max_tokens: 1024 - # top_p: 0.01 - # do_auto_retry: True - # num_retries: 5 - # retry_on_status_codes: [429, 500, 502, 503, 504] - # retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] - -embedders: - nim_embedder: + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + container_phase1_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 4096 + + container_phase2_llm: + _type: nim + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.15 + max_tokens: 16384 + + exploitability_llm: _type: nim - base_url: ${NIM_EMBED_BASE_URL:-https://integrate.api.nvidia.com/v1} - model_name: ${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} - truncate: END - max_batch_size: 128 - do_auto_retry: True - num_retries: 5 - retry_on_status_codes: [429, 500, 502, 503, 504] - retry_on_errors: ["Too Many Requests", "Internal Server Error", "Bad Gateway", "Service Unavailable", "Gateway Time-out", "Gateway Timeout"] + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.1 + max_tokens: 16384 + + vex_llm: + _type: nim + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} + temperature: 0.0 + max_tokens: 4096 + +functions: + web_search: + _type: web_search + search_depth: advanced + max_results: 5 + + web_fetch: + _type: web_fetch + timeout: 30 + max_content_length: 50000 + + osv_lookup: + _type: osv_lookup + + read_file: + _type: read_file + + list_directory: + _type: list_directory + + grep_search: + _type: grep_search + glob_find: + _type: glob_find + + check_binary_dependencies: + _type: check_binary_dependencies + + search_binary_content: + _type: search_binary_content + + check_distro_patches: + _type: check_distro_patches + + get_binary_linked_libraries: + _type: get_binary_linked_libraries + + query_dpkg_database: + _type: query_dpkg_database + + search_dpkg_file_lists: + _type: search_dpkg_file_lists + + query_rpm_database: + _type: query_rpm_database + + list_python_packages: + _type: list_python_packages + + find_shared_libraries: + _type: find_shared_libraries + + find_go_binaries: + _type: find_go_binaries + + check_security_controls: + _type: check_security_controls + + query_npm_packages: + _type: query_npm_packages + + find_java_packages: + _type: find_java_packages + + list_installed_packages: + _type: list_installed_packages + + cve_researcher: + _type: cve_researcher + llm_name: cve_researcher_llm + tool_names: [web_search, web_fetch, osv_lookup] + max_iterations: 2 + data_sources: null + include_middleware: true + verbose: true + output_subdir: vulnerability_reports + + container_analyzer: + _type: container_analyzer + phase1_llm_name: container_phase1_llm + phase2_llm_name: container_phase2_llm + web_tool_names: [web_search, web_fetch] + fs_tool_names: [read_file, list_directory, grep_search, glob_find] + phase1_max_iterations: 8 + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + public_registry_prefixes: + - "nvcr.io/" + - "docker.io/" + - "gcr.io/" + - "ghcr.io/" + - "registry.hub.docker.com/" + output_subdir: container_reports + + exploitability_analyzer: + _type: exploitability_analyzer + llm_name: exploitability_llm + tool_names: + - read_file + - list_directory + - grep_search + - glob_find + - check_binary_dependencies + - search_binary_content + - check_distro_patches + - get_binary_linked_libraries + - query_dpkg_database + - search_dpkg_file_lists + - query_rpm_database + - list_python_packages + - find_shared_libraries + - find_go_binaries + - check_security_controls + - query_npm_packages + - find_java_packages + - list_installed_packages + phase2_max_iterations: 40 + context_window_tokens: 262144 + wrap_up_fraction: 0.80 + loop_detection_threshold: 2 + discovery_probe_paths: + - "opt/conda/envs" + - "opt/conda" + - "usr/lib" + - "usr/local/lib" + - "usr/local/bin" + - "workspace" + - "app" + - "opt" + - "srv" + output_subdir: exploitability_reports + + vex_categorizer: + _type: vex_categorizer + llm_name: vex_llm + max_iterations: 10 + loop_detection_threshold: 2 + output_subdir: vex_results workflow: - _type: cve_agent - cve_generate_vdbs_name: cve_generate_vdbs - cve_fetch_intel_name: cve_fetch_intel - cve_process_sbom_name: cve_process_sbom - cve_check_vuln_deps_name: cve_check_vuln_deps - cve_checklist_name: cve_checklist - cve_agent_executor_name: cve_agent_executor - cve_summarize_name: cve_summarize - cve_justify_name: cve_justify - cve_output_config_name: cve_file_output - missing_source_action: continue_with_warning - llm_max_rate: 5 # Global max rate (requests/second) for all functions; individual function settings override this - -eval: - general: - output_dir: ./.tmp/profiler - dataset: - _type: json - file_path: data/profiler_datasets/profiler_dataset.json - - profiler: - token_uniqueness_forecast: true - workflow_runtime_forecast: true - compute_llm_metrics: true - csv_exclude_io_text: true - prompt_caching_prefixes: - enable: true - min_frequency: 0.1 - bottleneck_analysis: - # Can also be simple_stack - enable_nested_stack: true - concurrency_spike_analysis: - enable: true - spike_threshold: 7 + _type: cve_pipeline + cve_researcher_name: cve_researcher + container_analyzer_name: container_analyzer + exploitability_analyzer_name: exploitability_analyzer + vex_categorizer_name: vex_categorizer + stage_timeout: 600 + stage_max_retries: 1 + # Max parallel agent tasks per stage. null (or 0) = unlimited. + # Set to a positive integer to cap concurrent agent executions per pipeline stage. + max_concurrency: null + # Global LLM rate limit (requests/second) shared across all agent functions; per-function + # settings override this. null = no limit. Set a number only if the LLM endpoint enforces + # rate limiting (the upstream blueprint used 5 for that reason). + llm_max_rate: null + output_dir: outputs diff --git a/src/vuln_analysis/configs/openapi/openapi.json b/src/vuln_analysis/configs/openapi/openapi.json deleted file mode 100644 index b0155549b..000000000 --- a/src/vuln_analysis/configs/openapi/openapi.json +++ /dev/null @@ -1,2919 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "FastAPI", - "version": "0.1.0" - }, - "paths": { - "/generate": { - "post": { - "summary": "Post Single", - "description": "Executes the default NeMo Agent Toolkit workflow from the loaded configuration", - "operationId": "post_single_generate_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentMorpheusInput-Input" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentMorpheusOutput" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/generate/stream": { - "post": { - "summary": "Post Stream", - "description": "Executes the default NeMo Agent Toolkit workflow from the loaded configuration", - "operationId": "post_stream_generate_stream_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentMorpheusInput-Input" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentMorpheusOutput" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/generate/full": { - "post": { - "summary": "Post Stream", - "description": "Stream raw intermediate steps without any step adaptor translations.\nUse filter_steps query parameter to filter steps by type (comma-separated list) or set to 'none' to suppress all intermediate steps.", - "operationId": "post_stream_generate_full_post", - "parameters": [ - { - "name": "filter_steps", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Filter Steps" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentMorpheusInput-Input" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentMorpheusOutput" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/chat": { - "post": { - "summary": "Post Single", - "description": "Executes the default NeMo Agent Toolkit workflow from the loaded configuration", - "operationId": "post_single_chat_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/chat/stream": { - "post": { - "summary": "Post Stream", - "description": "Executes the default NeMo Agent Toolkit workflow from the loaded configuration", - "operationId": "post_stream_chat_stream_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ChatResponseChunk" - }, - { - "$ref": "#/components/schemas/ResponseIntermediateStep" - } - ], - "title": "Response Post Stream Chat Stream Post" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/evaluate/job/last": { - "get": { - "summary": "Get Last Job Status", - "description": "Get the status of the last created evaluation job", - "operationId": "get_last_job_status_evaluate_job_last_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EvaluateStatusResponse" - } - } - } - }, - "404": { - "description": "No jobs found" - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - } - } - } - }, - "/evaluate/job/{job_id}": { - "get": { - "summary": "Get Job Status", - "description": "Get the status of an evaluation job", - "operationId": "get_job_status_evaluate_job__job_id__get", - "parameters": [ - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Job Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EvaluateStatusResponse" - } - } - } - }, - "404": { - "description": "Job not found" - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/evaluate/jobs": { - "get": { - "summary": "Get Jobs", - "description": "Get all jobs, optionally filtered by status", - "operationId": "get_jobs_evaluate_jobs_get", - "parameters": [ - { - "name": "status", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EvaluateStatusResponse" - }, - "title": "Response Get Jobs Evaluate Jobs Get" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/evaluate": { - "post": { - "summary": "Start Evaluation", - "description": "Evaluates the performance and accuracy of the workflow on a dataset", - "operationId": "start_evaluation_evaluate_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EvaluateRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EvaluateResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "example": { - "detail": "Internal server error occurred" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "ChatRequest": { - "properties": { - "messages": { - "items": { - "$ref": "#/components/schemas/Message" - }, - "type": "array", - "title": "Messages" - }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Model" - }, - "temperature": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Temperature" - }, - "max_tokens": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Max Tokens" - }, - "top_p": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Top P" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "messages" - ], - "title": "ChatRequest", - "description": "ChatRequest is a data model that represents a request to the NeMo Agent Toolkit chat API." - }, - "ChatResponse": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "object": { - "type": "string", - "title": "Object" - }, - "model": { - "type": "string", - "title": "Model", - "default": "" - }, - "created": { - "type": "string", - "format": "date-time", - "title": "Created" - }, - "choices": { - "items": { - "$ref": "#/components/schemas/Choice" - }, - "type": "array", - "title": "Choices" - }, - "usage": { - "anyOf": [ - { - "$ref": "#/components/schemas/Usage" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "id", - "object", - "created", - "choices" - ], - "title": "ChatResponse", - "description": "ChatResponse is a data model that represents a response from the NeMo Agent Toolkit chat API." - }, - "ChatResponseChunk": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "choices": { - "items": { - "$ref": "#/components/schemas/Choice" - }, - "type": "array", - "title": "Choices" - }, - "created": { - "type": "string", - "format": "date-time", - "title": "Created" - }, - "model": { - "type": "string", - "title": "Model", - "default": "" - }, - "object": { - "type": "string", - "title": "Object", - "default": "chat.completion.chunk" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "id", - "choices", - "created" - ], - "title": "ChatResponseChunk", - "description": "ChatResponseChunk is a data model that represents a response chunk from the NeMo Agent Toolkit chat streaming API." - }, - "Choice": { - "properties": { - "message": { - "$ref": "#/components/schemas/ChoiceMessage" - }, - "finish_reason": { - "anyOf": [ - { - "type": "string", - "enum": [ - "stop", - "length", - "tool_calls", - "content_filter", - "function_call" - ] - }, - { - "type": "null" - } - ], - "title": "Finish Reason" - }, - "index": { - "type": "integer", - "title": "Index" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "message", - "index" - ], - "title": "Choice" - }, - "ChoiceMessage": { - "properties": { - "content": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Content" - }, - "role": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Role" - } - }, - "type": "object", - "title": "ChoiceMessage" - }, - "EvaluateRequest": { - "properties": { - "config_file": { - "type": "string", - "title": "Config File", - "description": "Path to the configuration file for evaluation" - }, - "job_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Job Id", - "description": "Unique identifier for the evaluation job" - }, - "reps": { - "type": "integer", - "title": "Reps", - "description": "Number of repetitions for the evaluation, defaults to 1", - "default": 1 - }, - "expiry_seconds": { - "type": "integer", - "title": "Expiry Seconds", - "description": "Optional time (in seconds) before the job expires. Clamped between 600 (10 min) and 86400 (24h).", - "default": 3600 - } - }, - "type": "object", - "required": [ - "config_file" - ], - "title": "EvaluateRequest", - "description": "Request model for the evaluate endpoint." - }, - "EvaluateResponse": { - "properties": { - "job_id": { - "type": "string", - "title": "Job Id", - "description": "Unique identifier for the evaluation job" - }, - "status": { - "type": "string", - "title": "Status", - "description": "Current status of the evaluation job" - } - }, - "type": "object", - "required": [ - "job_id", - "status" - ], - "title": "EvaluateResponse", - "description": "Response model for the evaluate endpoint." - }, - "EvaluateStatusResponse": { - "properties": { - "job_id": { - "type": "string", - "title": "Job Id", - "description": "Unique identifier for the evaluation job" - }, - "status": { - "type": "string", - "title": "Status", - "description": "Current status of the evaluation job" - }, - "config_file": { - "type": "string", - "title": "Config File", - "description": "Path to the configuration file used for evaluation" - }, - "error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error", - "description": "Error message if the job failed" - }, - "output_path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Output Path", - "description": "Path to the output file if the job completed successfully" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At", - "description": "Timestamp when the job was created" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At", - "description": "Timestamp when the job was last updated" - }, - "expires_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Expires At", - "description": "Timestamp when the job will expire" - } - }, - "type": "object", - "required": [ - "job_id", - "status", - "config_file", - "created_at", - "updated_at" - ], - "title": "EvaluateStatusResponse", - "description": "Response model for the evaluate status endpoint." - }, - "ResponseIntermediateStep": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "parent_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Parent Id" - }, - "type": { - "type": "string", - "title": "Type", - "default": "markdown" - }, - "name": { - "type": "string", - "title": "Name" - }, - "payload": { - "type": "string", - "title": "Payload" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "id", - "name", - "payload" - ], - "title": "ResponseIntermediateStep", - "description": "ResponseSerializedStep is a data model that represents a serialized step in the NeMo Agent Toolkit chat streaming API." - }, - "Usage": { - "properties": { - "prompt_tokens": { - "type": "integer", - "title": "Prompt Tokens" - }, - "completion_tokens": { - "type": "integer", - "title": "Completion Tokens" - }, - "total_tokens": { - "type": "integer", - "title": "Total Tokens" - } - }, - "type": "object", - "required": [ - "prompt_tokens", - "completion_tokens", - "total_tokens" - ], - "title": "Usage" - }, - "AgentIntermediateStep": { - "properties": { - "tool_name": { - "type": "string", - "title": "Tool Name" - }, - "action_log": { - "type": "string", - "title": "Action Log" - }, - "tool_input": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object" - } - ], - "title": "Tool Input" - }, - "tool_output": { - "title": "Tool Output" - } - }, - "type": "object", - "required": [ - "tool_name", - "action_log", - "tool_input", - "tool_output" - ], - "title": "AgentIntermediateStep", - "description": "Represents info for an intermediate step taken by an agent." - }, - "AgentMorpheusEngineOutput": { - "properties": { - "vuln_id": { - "type": "string", - "title": "Vuln Id" - }, - "checklist": { - "items": { - "$ref": "#/components/schemas/ChecklistItemOutput" - }, - "type": "array", - "title": "Checklist" - }, - "summary": { - "type": "string", - "title": "Summary" - }, - "justification": { - "$ref": "#/components/schemas/JustificationOutput" - } - }, - "type": "object", - "required": [ - "vuln_id", - "checklist", - "summary", - "justification" - ], - "title": "AgentMorpheusEngineOutput", - "description": "Contains all output generated by the main Agent Morpheus LLM Engine for a given vulnerability.\n\n- vuln_id: the ID of the vulnerability being processed by the LLM engine.\n- checklist: a list of ChecklistItemOutput objects, each containing an input and a response from the LLM agent.\n- summary: a short summary of the checklist inputs and responses, generated by an LLM.\n- justification: a JustificationOutput object containing details of the model's justification decision." - }, - "AgentMorpheusInfo": { - "properties": { - "vdb": { - "anyOf": [ - { - "$ref": "#/components/schemas/VdbPaths" - }, - { - "type": "null" - } - ] - }, - "intel": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/CveIntel" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Intel" - }, - "sbom": { - "anyOf": [ - { - "$ref": "#/components/schemas/SBOMInfo" - }, - { - "type": "null" - } - ] - }, - "vulnerable_dependencies": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/VulnerableDependencies" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Vulnerable Dependencies" - } - }, - "type": "object", - "title": "AgentMorpheusInfo", - "description": "Information used for decisioning in the Agent Morpheus engine. These information can all be automatically\ngenerated or retrieved by the pipeline from the input information.\n\n- vdb: paths to source code and documentation vector databases (VDBs) used to understand whether a vulnerability\n is exploitable in the source code.\n- intel: list of CveIntel objects representing intelligence for each vulnerability pulled from various vulnerability\n databases and APIs.\n- sbom: software bill of materials listing the packages and versions in the container image, used to understand\n whether the vulnerable package exists in the image.\n- vulnerable_dependencies: a list of VulnerableDependencies objects for each vuln_id, representing the SBOM packages\n and transitive dependencies that are vulnerable for the vuln_id." - }, - "AgentMorpheusInput-Input": { - "properties": { - "scan": { - "$ref": "#/components/schemas/ScanInfoInput" - }, - "image": { - "$ref": "#/components/schemas/ImageInfoInput-Input" - } - }, - "type": "object", - "required": [ - "scan", - "image" - ], - "title": "AgentMorpheusInput", - "description": "Inputs required by the Agent Morpheus pipeline." - }, - "AgentMorpheusInput-Output": { - "properties": { - "scan": { - "$ref": "#/components/schemas/ScanInfoInput" - }, - "image": { - "$ref": "#/components/schemas/ImageInfoInput-Output" - } - }, - "type": "object", - "required": [ - "scan", - "image" - ], - "title": "AgentMorpheusInput", - "description": "Inputs required by the Agent Morpheus pipeline." - }, - "AgentMorpheusOutput": { - "properties": { - "input": { - "$ref": "#/components/schemas/AgentMorpheusInput-Output" - }, - "info": { - "$ref": "#/components/schemas/AgentMorpheusInfo" - }, - "output": { - "items": { - "$ref": "#/components/schemas/AgentMorpheusEngineOutput" - }, - "type": "array", - "title": "Output" - } - }, - "type": "object", - "required": [ - "input", - "info", - "output" - ], - "title": "AgentMorpheusOutput", - "description": "\"\nThe final output of the Agent Morpheus pipeline.\nContains all fields in the AgentMorpheusEngineInput, plus the AgentMorpheusEngineOuput for each input vulnerability." - }, - "AudioContent": { - "properties": { - "type": { - "type": "string", - "const": "input_audio", - "title": "Type", - "default": "input_audio" - }, - "input_audio": { - "$ref": "#/components/schemas/InputAudio", - "default": { - "data": "default", - "format": "default" - } - } - }, - "additionalProperties": false, - "type": "object", - "title": "AudioContent" - }, - "BaseMetricV3": { - "properties": { - "cvssV3": { - "$ref": "#/components/schemas/CVSSV3" - }, - "exploitabilityScore": { - "type": "number", - "title": "Exploitabilityscore" - }, - "impactScore": { - "type": "number", - "title": "Impactscore" - } - }, - "type": "object", - "required": [ - "cvssV3" - ], - "title": "BaseMetricV3" - }, - "Bugzilla": { - "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Id" - }, - "url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Url" - } - }, - "type": "object", - "title": "Bugzilla" - }, - "CVSS": { - "properties": { - "score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Score" - }, - "vector_string": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Vector String" - } - }, - "type": "object", - "title": "CVSS" - }, - "CVSS3": { - "properties": { - "cvss3_base_score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Cvss3 Base Score" - }, - "cvss3_scoring_vector": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cvss3 Scoring Vector" - }, - "status": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" - } - }, - "type": "object", - "title": "CVSS3" - }, - "CVSSV3": { - "properties": { - "attackComplexity": { - "type": "string", - "title": "Attackcomplexity" - }, - "attackVector": { - "type": "string", - "title": "Attackvector" - }, - "availabilityImpact": { - "type": "string", - "title": "Availabilityimpact" - }, - "baseScore": { - "type": "number", - "title": "Basescore" - }, - "baseSeverity": { - "type": "string", - "title": "Baseseverity" - }, - "confidentialityImpact": { - "type": "string", - "title": "Confidentialityimpact" - }, - "integrityImpact": { - "type": "string", - "title": "Integrityimpact" - }, - "privilegesRequired": { - "type": "string", - "title": "Privilegesrequired" - }, - "scope": { - "type": "string", - "title": "Scope" - }, - "userInteraction": { - "type": "string", - "title": "Userinteraction" - }, - "vectorString": { - "type": "string", - "title": "Vectorstring" - }, - "version": { - "type": "string", - "title": "Version" - } - }, - "type": "object", - "required": [ - "attackComplexity", - "attackVector", - "availabilityImpact", - "baseScore", - "baseSeverity", - "confidentialityImpact", - "integrityImpact", - "privilegesRequired", - "scope", - "userInteraction", - "vectorString", - "version" - ], - "title": "CVSSV3" - }, - "CWE": { - "properties": { - "cwe_id": { - "type": "string", - "title": "Cwe Id" - }, - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - } - }, - "type": "object", - "required": [ - "cwe_id" - ], - "title": "CWE" - }, - "ChecklistItemOutput": { - "properties": { - "input": { - "type": "string", - "title": "Input" - }, - "response": { - "type": "string", - "title": "Response" - }, - "intermediate_steps": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/AgentIntermediateStep" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Intermediate Steps" - } - }, - "type": "object", - "required": [ - "input", - "response" - ], - "title": "ChecklistItemOutput", - "description": "Input, response, and intermediate steps for a single checklist item provided to the LLM agent." - }, - "Configuration": { - "properties": { - "package": { - "type": "string", - "title": "Package" - }, - "system": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "System" - }, - "versionStartExcluding": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Versionstartexcluding" - }, - "versionEndExcluding": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Versionendexcluding" - }, - "versionStartIncluding": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Versionstartincluding" - }, - "versionEndIncluding": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Versionendincluding" - } - }, - "type": "object", - "required": [ - "package" - ], - "title": "Configuration" - }, - "CveIntel": { - "properties": { - "vuln_id": { - "type": "string", - "title": "Vuln Id" - }, - "ghsa": { - "anyOf": [ - { - "$ref": "#/components/schemas/CveIntelGhsa" - }, - { - "type": "null" - } - ] - }, - "nvd": { - "anyOf": [ - { - "$ref": "#/components/schemas/CveIntelNvd" - }, - { - "type": "null" - } - ] - }, - "rhsa": { - "anyOf": [ - { - "$ref": "#/components/schemas/CveIntelRhsa" - }, - { - "type": "null" - } - ] - }, - "ubuntu": { - "anyOf": [ - { - "$ref": "#/components/schemas/CveIntelUbuntu" - }, - { - "type": "null" - } - ] - }, - "epss": { - "anyOf": [ - { - "$ref": "#/components/schemas/CveIntelEpss" - }, - { - "type": "null" - } - ] - }, - "has_sufficient_intel_for_agent": { - "type": "boolean", - "title": "Has Sufficient Intel For Agent", - "description": "Logic to determine if the CVE has sufficient intel and can be passed to the agent.\n\nReturns\n-------\nbool\n True if enough intel has been found for the CVE", - "readOnly": true - } - }, - "type": "object", - "required": [ - "vuln_id", - "has_sufficient_intel_for_agent" - ], - "title": "CveIntel", - "description": "Information about a CVE (Common Vulnerabilities and Exposures) entry." - }, - "CveIntelEpss": { - "properties": { - "epss": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Epss" - }, - "percentile": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Percentile" - }, - "date": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Date" - } - }, - "additionalProperties": true, - "type": "object", - "title": "CveIntelEpss", - "description": "Information about an EPSS (Elastic Product Security Service) entry." - }, - "CveIntelGhsa": { - "properties": { - "ghsa_id": { - "type": "string", - "title": "Ghsa Id" - }, - "cve_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cve Id" - }, - "summary": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Summary" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "severity": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Severity" - }, - "vulnerabilities": { - "anyOf": [ - { - "items": {}, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Vulnerabilities" - }, - "cvss": { - "anyOf": [ - { - "$ref": "#/components/schemas/CVSS" - }, - { - "type": "null" - } - ] - }, - "cwes": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/CWE" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Cwes" - }, - "published_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Published At" - }, - "updated_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Updated At" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "ghsa_id" - ], - "title": "CveIntelGhsa", - "description": "Information about a GHSA (GitHub Security Advisory) entry." - }, - "CveIntelNvd": { - "properties": { - "cve_id": { - "type": "string", - "title": "Cve Id" - }, - "cve_description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cve Description" - }, - "cvss_vector": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cvss Vector" - }, - "cvss_base_score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Cvss Base Score" - }, - "cvss_severity": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cvss Severity" - }, - "cwe_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cwe Name" - }, - "cwe_description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cwe Description" - }, - "cwe_extended_description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cwe Extended Description" - }, - "configurations": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/Configuration" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Configurations" - }, - "vendor_names": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Vendor Names" - }, - "references": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "References" - }, - "disputed": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Disputed" - }, - "published_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Published At" - }, - "updated_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Updated At" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "cve_id" - ], - "title": "CveIntelNvd", - "description": "Information about an NVD (National Vulnerability Database) entry." - }, - "CveIntelRhsa": { - "properties": { - "bugzilla": { - "$ref": "#/components/schemas/Bugzilla" - }, - "details": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Details" - }, - "statement": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Statement" - }, - "package_state": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/PackageState" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Package State" - }, - "upstream_fix": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Upstream Fix" - }, - "cvss3": { - "anyOf": [ - { - "$ref": "#/components/schemas/CVSS3" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true, - "type": "object", - "title": "CveIntelRhsa", - "description": "Information about a RHSA (Red Hat Security Advisory) entry." - }, - "CveIntelUbuntu": { - "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "notes": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/Note" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Notes" - }, - "notices": { - "anyOf": [ - { - "items": {}, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Notices" - }, - "priority": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Priority" - }, - "ubuntu_description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Ubuntu Description" - }, - "impact": { - "anyOf": [ - { - "$ref": "#/components/schemas/Impact" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true, - "type": "object", - "title": "CveIntelUbuntu", - "description": "Information about a Ubuntu CVE entry." - }, - "DependencyPackage": { - "properties": { - "system": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "System" - }, - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "version": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Version" - }, - "relation": { - "anyOf": [ - { - "type": "string", - "enum": [ - "SELF", - "DIRECT", - "INDIRECT" - ] - }, - { - "type": "null" - } - ], - "title": "Relation" - } - }, - "type": "object", - "title": "DependencyPackage", - "description": "Information about a dependency package as obtained from deps.dev API for a given SBOM package." - }, - "FileSBOMInfoInput": { - "properties": { - "_type": { - "type": "string", - "const": "file", - "title": "Type", - "description": "The type of the object", - "default": "file" - }, - "file_path": { - "type": "string", - "title": "File Path" - } - }, - "type": "object", - "required": [ - "file_path" - ], - "title": "FileSBOMInfoInput", - "description": "A file path pointing to a Software Bill of Materials file." - }, - "HTTPSBOMInfoInput": { - "properties": { - "_type": { - "type": "string", - "const": "http", - "title": "Type", - "description": "The type of the object", - "default": "http" - }, - "url": { - "type": "string", - "minLength": 1, - "format": "uri", - "title": "Url" - } - }, - "type": "object", - "required": [ - "url" - ], - "title": "HTTPSBOMInfoInput", - "description": "A URL pointing to a Software Bill of Materials file." - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail" - } - }, - "type": "object", - "title": "HTTPValidationError" - }, - "ImageContent": { - "properties": { - "type": { - "type": "string", - "const": "image_url", - "title": "Type", - "default": "image_url" - }, - "image_url": { - "$ref": "#/components/schemas/ImageUrl", - "default": { - "url": "http://default.com/" - } - } - }, - "additionalProperties": false, - "type": "object", - "title": "ImageContent" - }, - "ImageInfoInput-Input": { - "properties": { - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "tag": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Tag" - }, - "digest": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Digest" - }, - "platform": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Platform" - }, - "feed_group": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Feed Group" - }, - "source_info": { - "items": { - "$ref": "#/components/schemas/SourceDocumentsInfo" - }, - "type": "array", - "title": "Source Info" - }, - "sbom_info": { - "oneOf": [ - { - "$ref": "#/components/schemas/ManualSBOMInfoInput" - }, - { - "$ref": "#/components/schemas/FileSBOMInfoInput" - }, - { - "$ref": "#/components/schemas/HTTPSBOMInfoInput" - } - ], - "title": "Sbom Info" - } - }, - "type": "object", - "required": [ - "source_info", - "sbom_info" - ], - "title": "ImageInfoInput", - "description": "Information about a container image, including the source information and sbom information." - }, - "ImageInfoInput-Output": { - "properties": { - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "tag": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Tag" - }, - "digest": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Digest" - }, - "platform": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Platform" - }, - "feed_group": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Feed Group" - }, - "source_info": { - "items": { - "$ref": "#/components/schemas/SourceDocumentsInfo" - }, - "type": "array", - "title": "Source Info" - }, - "sbom_info": { - "oneOf": [ - { - "$ref": "#/components/schemas/ManualSBOMInfoInput" - }, - { - "$ref": "#/components/schemas/FileSBOMInfoInput" - }, - { - "$ref": "#/components/schemas/HTTPSBOMInfoInput" - } - ], - "title": "Sbom Info" - } - }, - "type": "object", - "required": [ - "source_info", - "sbom_info" - ], - "title": "ImageInfoInput", - "description": "Information about a container image, including the source information and sbom information." - }, - "ImageUrl": { - "properties": { - "url": { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri", - "title": "Url", - "default": "http://default.com/" - } - }, - "type": "object", - "title": "ImageUrl" - }, - "Impact": { - "properties": { - "baseMetricV3": { - "$ref": "#/components/schemas/BaseMetricV3" - } - }, - "type": "object", - "required": [ - "baseMetricV3" - ], - "title": "Impact" - }, - "InputAudio": { - "properties": { - "data": { - "type": "string", - "title": "Data", - "default": "default" - }, - "format": { - "type": "string", - "title": "Format", - "default": "default" - } - }, - "type": "object", - "title": "InputAudio" - }, - "JustificationOutput": { - "properties": { - "label": { - "type": "string", - "title": "Label" - }, - "reason": { - "type": "string", - "title": "Reason" - }, - "status": { - "type": "string", - "enum": [ - "TRUE", - "FALSE", - "UNKNOWN" - ], - "title": "Status" - } - }, - "type": "object", - "required": [ - "label", - "reason", - "status" - ], - "title": "JustificationOutput", - "description": "Final justification for the vulnerability.\n\n- label: a categorical justification label classifying the status of an image against a given vulnerability, e.g.\n code_not_present, code_not_reachable, false_positive.\n- reason: a human-readable explanation for why justification label was selected.\n- status: a ternary status (TRUE, FALSE, OR UNKNOWN) that indicates whether the image can be exploited for a given\n vulnerability. Determined based on a mapping from the justification label." - }, - "ManualSBOMInfoInput": { - "properties": { - "_type": { - "type": "string", - "const": "manual", - "title": "Type", - "description": "The type of the object", - "default": "manual" - }, - "packages": { - "items": { - "$ref": "#/components/schemas/SBOMPackage" - }, - "type": "array", - "title": "Packages" - } - }, - "type": "object", - "required": [ - "packages" - ], - "title": "ManualSBOMInfoInput", - "description": "Manually provided Software Bill of Materials, consisting of a list of SBOMPackage objects." - }, - "Message": { - "properties": { - "content": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/TextContent" - }, - { - "$ref": "#/components/schemas/ImageContent" - }, - { - "$ref": "#/components/schemas/AudioContent" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "image_url": "#/components/schemas/ImageContent", - "input_audio": "#/components/schemas/AudioContent", - "text": "#/components/schemas/TextContent" - } - } - }, - "type": "array" - } - ], - "title": "Content" - }, - "role": { - "type": "string", - "title": "Role" - } - }, - "type": "object", - "required": [ - "content", - "role" - ], - "title": "Message" - }, - "Note": { - "properties": { - "author": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Author" - }, - "note": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Note" - } - }, - "type": "object", - "title": "Note" - }, - "PackageState": { - "properties": { - "product_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Product Name" - }, - "fix_state": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Fix State" - }, - "package_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Package Name" - }, - "cpe": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cpe" - } - }, - "type": "object", - "title": "PackageState" - }, - "SBOMInfo": { - "properties": { - "packages": { - "items": { - "$ref": "#/components/schemas/SBOMPackage" - }, - "type": "array", - "title": "Packages" - } - }, - "type": "object", - "required": [ - "packages" - ], - "title": "SBOMInfo", - "description": "List of SBOMPackage objects representing the packages found in the input image." - }, - "SBOMPackage": { - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "version": { - "type": "string", - "title": "Version" - }, - "path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Path" - }, - "system": { - "type": "string", - "title": "System" - } - }, - "type": "object", - "required": [ - "name", - "version", - "system" - ], - "title": "SBOMPackage", - "description": "Information about a single package in the container image's Software Bill of Materials (SBOM)." - }, - "ScanInfoInput": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Type" - }, - "started_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Started At" - }, - "completed_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Completed At" - }, - "vulns": { - "items": { - "$ref": "#/components/schemas/VulnInfo" - }, - "type": "array", - "minItems": 1, - "title": "Vulns" - } - }, - "type": "object", - "required": [ - "vulns" - ], - "title": "ScanInfoInput", - "description": "Information about a unique scan for a container image against a list of vulnerabilies." - }, - "SourceDocumentsInfo": { - "properties": { - "type": { - "type": "string", - "enum": [ - "code", - "doc" - ], - "title": "Type" - }, - "git_repo": { - "type": "string", - "minLength": 1, - "title": "Git Repo" - }, - "ref": { - "type": "string", - "minLength": 1, - "title": "Ref" - }, - "include": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Include", - "default": [ - "*.py", - "*.ipynb" - ] - }, - "exclude": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Exclude", - "default": [] - } - }, - "type": "object", - "required": [ - "type", - "git_repo", - "ref" - ], - "title": "SourceDocumentsInfo", - "description": "Information about the source documents for the container image.\n\n- type: document type.\n- git_repo: git repo URL where the source documents can be cloned.\n- ref: git reference, such as tag/branch/commit_id\n- include: file extensions to include when indexing the source documents.\n- exclude: file extensions to exclude when indexing the source documents." - }, - "TextContent": { - "properties": { - "type": { - "type": "string", - "const": "text", - "title": "Type", - "default": "text" - }, - "text": { - "type": "string", - "title": "Text", - "default": "default" - } - }, - "additionalProperties": false, - "type": "object", - "title": "TextContent" - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "type": "array", - "title": "Location" - }, - "msg": { - "type": "string", - "title": "Message" - }, - "type": { - "type": "string", - "title": "Error Type" - } - }, - "type": "object", - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError" - }, - "VdbPaths": { - "properties": { - "code_vdb_path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Code Vdb Path" - }, - "doc_vdb_path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Doc Vdb Path" - }, - "code_index_path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Code Index Path" - } - }, - "type": "object", - "title": "VdbPaths", - "description": "Paths to where the generated VDBs are stored." - }, - "VulnInfo": { - "properties": { - "vuln_id": { - "type": "string", - "title": "Vuln Id" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Score" - }, - "severity": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Severity" - }, - "published_date": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Published Date" - }, - "last_modified_date": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Last Modified Date" - }, - "url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Url" - }, - "feed_group": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Feed Group" - }, - "package": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Package" - }, - "package_version": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Package Version" - }, - "package_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Package Name" - }, - "package_type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Package Type" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "vuln_id" - ], - "title": "VulnInfo", - "description": "Information about a vulnerability." - }, - "VulnerableDependencies": { - "properties": { - "vuln_id": { - "type": "string", - "title": "Vuln Id" - }, - "vuln_package_intel_sources": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Vuln Package Intel Sources" - }, - "vulnerable_sbom_packages": { - "items": { - "$ref": "#/components/schemas/VulnerableSBOMPackage" - }, - "type": "array", - "title": "Vulnerable Sbom Packages" - } - }, - "type": "object", - "required": [ - "vuln_id", - "vuln_package_intel_sources", - "vulnerable_sbom_packages" - ], - "title": "VulnerableDependencies", - "description": "Information about the vulnerable SBOM packages associated with the vuln_id.\n\n- vuln_id: vulnerability ID (e.g. CVE ID, GHSA ID) associated with the vulnerable package list.\n- vuln_package_intel_sources: list of sources (e.g. \"ghsa\", \"nvd\", \"ubuntu\", \"rhsa\") that provided\n the vulnerable package/version intel for the vuln_id.\n- vulnerable_sbom_packages: list of VulnerableSBOMPackage objects, representing the SBOM packages that are\n vulnerable for a given vuln_id." - }, - "VulnerableSBOMPackage": { - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "version": { - "type": "string", - "title": "Version" - }, - "vulnerable_dependency_package": { - "$ref": "#/components/schemas/DependencyPackage" - } - }, - "type": "object", - "required": [ - "name", - "version", - "vulnerable_dependency_package" - ], - "title": "VulnerableSBOMPackage", - "description": "Information about a vulnerable SBOM package and its related vulnerable dependency package.\n\n- name: SBOM package name\n- version: SBOM package version\n- vulnerable_dependency_package: DependencyPackage object with info about the vulnerable dependency package.\n If an SBOM package itself is vulnerable, the vulnerable_dependency_package.relation will be \"SELF\".\n Otherwise, if it is vulnerable due to its dependency, the vulnerable_dependency_package.relation will be either\n \"DIRECT\" or \"INDIRECT\"." - } - } - } -} diff --git a/src/vuln_analysis/data/eval_datasets/cve_report_benchmark/CVE-2024-5187.md b/src/vuln_analysis/data/eval_datasets/cve_report_benchmark/CVE-2024-5187.md new file mode 100644 index 000000000..4e9abcb8f --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/cve_report_benchmark/CVE-2024-5187.md @@ -0,0 +1,166 @@ +I now have all the information needed to produce the comprehensive vulnerability report. Let me compile the final report. + +# CVE Analysis: CVE-2024-5187 + +## Overview + +| Field | Value | +|-------|-------| +| **CVE ID** | CVE-2024-5187 | +| **GHSA ID** | GHSA-6rq9-53c3-f7vj | +| **Affected Package** | onnx | +| **Vulnerable Versions** | < 1.16.1 | +| **Fixed In** | 1.16.1 (see Affected Version Ranges below) | +| **CVSS v3.1** | 8.8 HIGH | +| **CVSS Vector** | AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H | +| **CWE** | CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') | +| **Disclosure Date** | June 6, 2024 | + +### Affected Version Ranges + +| Branch | Vulnerable Range | First Fixed Version | +|--------|-----------------|---------------------| +| 1.16.x | >= 1.16.0, < 1.16.1 | 1.16.1 | +| **<= 1.15 (all older branches)** | **All versions** | **No upstream fix** | + +**Note:** The NVD CPE configuration lists `cpe:2.3:a:linuxfoundation:onnx:1.16.0` as affected. OSV data confirms version 1.16.0 is AFFECTED and 1.16.1 is NOT AFFECTED. The fix was backported to the 1.16.x branch in version 1.16.1, and then further refined in 1.16.2. Older branches (1.15.x and earlier) do not have upstream fixes for this vulnerability. + +### Affected Packages + +| Package | Ecosystem | Vulnerable Versions | Fixed In | +|---------|-----------|---------------------|----------| +| onnx | PyPI | < 1.16.1 | 1.16.1 | + +## Description + +A vulnerability in the `download_model_with_test_data` function of the onnx/onnx framework allows for arbitrary file overwrite due to inadequate prevention of path traversal attacks in malicious tar files. This vulnerability enables attackers to overwrite any file on the system, potentially leading to remote code execution, deletion of system, personal, or application files, thus impacting the integrity and availability of the system. + +The issue arises from the function's handling of tar file extraction without performing security checks on the paths within the tar file. An attacker can craft a malicious tar file containing entries with absolute paths or path traversal sequences (e.g., `../`) that, when extracted, write files outside the intended extraction directory. + +## Technical Details + +### Vulnerable Component + +- **File:** `onnx/hub.py` +- **Function:** `download_model_with_test_data()` +- **Secondary File:** `onnx/backend/test/runner/__init__.py` +- **Secondary Function:** `download_model()` + +### Root Cause + +The vulnerable code used Python's `tarfile.extractall()` method without any path validation or filtering. The original code: + +```python +with tarfile.open(local_model_with_data_path) as model_with_data_zipped: + local_model_with_data_dir_path = local_model_with_data_path[ + 0 : len(local_model_with_data_path) - 7 + ] + model_with_data_zipped.extractall(local_model_with_data_dir_path) +``` + +This allowed malicious tar files to contain entries with: +1. **Absolute paths** (e.g., `/home/kali/.ssh/authorized_keys`) - directly specifying where to write +2. **Path traversal sequences** (e.g., `../../etc/passwd`) - escaping the intended directory +3. **Symbolic links** - pointing to sensitive files outside the extraction directory + +### Attack Vector Analysis + +| Factor | Value | Explanation | +|--------|-------|-------------| +| **Attack Vector** | Network | The vulnerability can be exploited remotely by providing a malicious model tar file | +| **Attack Complexity** | Low | No special conditions required; attacker just needs to craft a malicious tar file | +| **Privileges Required** | None | No authentication or privileges needed to exploit | +| **User Interaction** | Required | User must download and extract a malicious model using the vulnerable function | +| **Scope** | Unchanged | The vulnerability affects only the system running the ONNX framework | +| **Confidentiality Impact** | High | Attacker can overwrite sensitive files, potentially gaining access to credentials | +| **Integrity Impact** | High | Attacker can overwrite any file on the system with arbitrary content | +| **Availability Impact** | High | Attacker can delete or corrupt critical system/application files | + +## Exploit Intelligence + +| Indicator | Status | +|-----------|--------| +| **Public Exploit** | Yes | +| **PoC Available** | Yes | +| **Active Exploitation (CISA KEV)** | No | +| **EPSS Percentile** | 81st percentile | +| **EPSS Probability** | 1.36% | + +### Exploitation Requirements + +- **Prerequisites:** + - Target must be using the ONNX framework with the `download_model_with_test_data()` or `download_model()` functions + - Attacker must be able to provide a malicious tar file (e.g., via a compromised model repository or man-in-the-middle attack) +- **Privileges Required:** None +- **User Interaction:** Required - user must invoke the vulnerable function to download/extract a model +- **Exploitation Difficulty:** Low - crafting a malicious tar file with path traversal is straightforward + +### Demonstrated Attack Scenarios + +1. **SSH Access Compromise:** On servers with SSH enabled, an attacker can inject their own public RSA key into the `/home/user/.ssh/authorized_keys` file, leading to remote code execution. + +2. **Web Server Compromise:** On servers hosting web applications: + - PHP/JSP servers: Upload a webshell for remote code execution + - Other servers: Upload malicious HTML files for Cross-site Scripting (XSS) + +3. **System File Corruption:** Overwrite critical system files to cause denial of service or further compromise. + +## Impact + +If exploited, this vulnerability can lead to: + +1. **Remote Code Execution (RCE):** By overwriting SSH authorized_keys, cron jobs, or web server files +2. **Data Loss:** Deletion or corruption of system, personal, or application files +3. **Privilege Escalation:** Overwriting configuration files to gain elevated privileges +4. **System Compromise:** Full control of the affected system through various file overwrite techniques + +The vulnerability is particularly dangerous in machine learning pipelines where models are frequently downloaded from external sources, as users may trust model repositories without verifying the integrity of downloaded files. + +## Remediation + +### Patch + +Upgrade `onnx` to version 1.16.1 or higher (1.16.2 recommended for additional refinements). + +```bash +pip install --upgrade onnx>=1.16.2 +``` + +The fix was implemented in PR #6164 and cherry-picked to the 1.16.2 release. The patch introduces: + +1. **Path validation:** A new `_tar_members_filter()` function that validates each tar member's path before extraction +2. **Absolute path checking:** Ensures extracted paths remain within the intended directory using `os.path.abspath()` comparison +3. **Symlink/hardlink blocking:** Rejects tar entries containing symbolic or hard links +4. **Python 3.12+ compatibility:** Uses the new `tarfile.data_filter` when available for enhanced security + +**Fix Commit:** `84051888d0943883a0edbf683f68c05ca3b28c40` (1.16.2 cherry-picks) + +### Workarounds + +If immediate patching is not possible: + +1. **Avoid using `download_model_with_test_data()` and `download_model()` functions** with untrusted model sources +2. **Manually verify tar file contents** before extraction using `tar -tvf` to inspect paths +3. **Use trusted model sources only** and verify model integrity with checksums +4. **Run ONNX operations in sandboxed environments** with restricted file system access + +### Recommendation + +**Patch immediately.** This is a high-severity vulnerability with a public proof-of-concept and straightforward exploitation. The EPSS score places it in the 81st percentile, indicating a higher-than-average likelihood of exploitation. Organizations using ONNX for machine learning workflows should prioritize upgrading to version 1.16.2 or later. + +## References + +- [NVD Entry - CVE-2024-5187](https://nvd.nist.gov/vuln/detail/CVE-2024-5187) +- [GitHub Security Advisory - GHSA-6rq9-53c3-f7vj](https://github.com/advisories/GHSA-6rq9-53c3-f7vj) +- [Snyk Vulnerability Database](https://security.snyk.io/vuln/SNYK-PYTHON-ONNX-7231121) +- [ONNX v1.16.2 Release Notes](https://github.com/onnx/onnx/releases/tag/v1.16.2) +- [Fix PR #6164 - Mitigate tarball directory traversal risks](https://github.com/onnx/onnx/pull/6164) +- [Fix Commit](https://github.com/onnx/onnx/commit/84051888d0943883a0edbf683f68c05ca3b28c40) +- [Vulnerable Code Location](https://github.com/onnx/onnx/blob/4128a09009aa67622c6308c82fe4199813a71682/onnx/hub.py#L369) +- [Huntr.dev Bug Bounty Report](https://huntr.com/bounties/50235ebd-3410-4ada-b064-1a648e11237e) + +## Credits + +- **Reporter:** sunrisexu (via huntr.dev) +- **Bug Bounty:** $750 awarded through huntr.dev +- **Fix Author:** sunflowersxu (PR #6164) \ No newline at end of file diff --git a/src/vuln_analysis/data/eval_datasets/eval_dataset.json b/src/vuln_analysis/data/eval_datasets/eval_dataset.json deleted file mode 100644 index a0107930d..000000000 --- a/src/vuln_analysis/data/eval_datasets/eval_dataset.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "dataset_id": "example-eval-dataset", - "dataset_description": "Example eval dataset with two containers", - "containers": { - "morpheus:23.11-runtime": { - "container_image": { - "name": "nvcr.io/nvidia/morpheus/morpheus", - "tag": "23.11-runtime", - "source_info": [ - { - "type": "code", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "branch-23.11", - "include": [ - "**/*.cpp", - "**/*.cu", - "**/*.cuh", - "**/*.h", - "**/*.hpp", - "**/*.ipynb", - "**/*.py", - "**/*Dockerfile" - ], - "exclude": [ - "tests/**/*" - ] - }, - { - "type": "doc", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "branch-23.11", - "include": [ - "**/*.md", - "docs/**/*.rst" - ] - } - ], - "sbom_info": { - "_type": "file", - "file_path": "data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom" - } - }, - "ground_truth": [ - { - "vuln_id": "GHSA-3f63-hfp8-52jq", - "status": "NOT AFFECTED", - "label": "code_not_reachable" - }, - { - "vuln_id": "CVE-2023-5388", - "status": "NOT AFFECTED", - "label": "false_positive" - } - ] - }, - "morpheus:24.03-runtime": { - "container_image": { - "name": "nvcr.io/nvidia/morpheus/morpheus", - "tag": "v24.03.02-runtime", - "source_info": [ - { - "type": "code", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "v24.03.02", - "include": [ - "**/*.cpp", - "**/*.cu", - "**/*.cuh", - "**/*.h", - "**/*.hpp", - "**/*.ipynb", - "**/*.py", - "**/*Dockerfile" - ], - "exclude": [ - "tests/**/*" - ] - }, - { - "type": "doc", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "v24.03.02", - "include": [ - "**/*.md", - "docs/**/*.rst" - ] - } - ], - "sbom_info": { - "_type": "file", - "file_path": "data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom" - } - }, - "ground_truth": [ - { - "vuln_id": "CVE-2023-5388", - "status": "NOT AFFECTED", - "label": "false_positive" - } - ] - } - } -} diff --git a/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv new file mode 100644 index 000000000..184aee536 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv @@ -0,0 +1,30 @@ +vuln_id,image_name,image_tag,ground_truth_label +GHSA-53q9-r3pm-6pq6,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +CVE-2025-4517,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +CVE-2025-13836,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-wf7f-8fxf-xfxc,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-pgqp-8h46-6x4j,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-gm62-xv2j-4w53,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-7f5h-v6xp-fcq8,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-6mq8-rvhq-8wgg,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-63vm-454h-vhhq,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-5rjg-fvgr-3xxf,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +GHSA-58pv-8j8x-9vj2,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-38jv-5279-wg99,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-2xpw-w6gg-jr37,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +GHSA-2qfp-q593-8484,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_present +GHSA-ph84-rcj2-fxxm,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +CVE-2024-9287,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +CVE-2025-4138,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +CVE-2025-4330,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +CVE-2025-4435,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,code_not_reachable +CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +CVE-2025-8194,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +CVE-2025-9230,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,vulnerable +CVE-2025-48384,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models,25.06,code_not_reachable +CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models,25.06,code_not_reachable +GHSA-58pv-8j8x-9vj2,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models,25.06,code_not_reachable +GHSA-5rjg-fvgr-3xxf,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models,25.06,code_not_reachable +GHSA-rqc4-2hc7-8c8v,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models,25.06,code_not_reachable diff --git a/src/vuln_analysis/data/examples/container_analyzer_input.json b/src/vuln_analysis/data/examples/container_analyzer_input.json new file mode 100644 index 000000000..7c299e506 --- /dev/null +++ b/src/vuln_analysis/data/examples/container_analyzer_input.json @@ -0,0 +1,7 @@ +{ + "image_name": "nvcr.io/nvidia/morpheus/morpheus", + "image_tag": "25.06-runtime", + "filesystem_path": "/path/to/extracted/morpheus_25.06-runtime", + "image_config_path": "/path/to/extracted/morpheus_25.06-runtime.image_config.json", + "skip_web_research": false +} diff --git a/src/vuln_analysis/data/examples/cve_researcher_input.json b/src/vuln_analysis/data/examples/cve_researcher_input.json new file mode 100644 index 000000000..331bf0f35 --- /dev/null +++ b/src/vuln_analysis/data/examples/cve_researcher_input.json @@ -0,0 +1,5 @@ +{ + "cve_id": "CVE-2024-5187", + "package_hint": "onnx", + "version_hint": "1.16.0" +} diff --git a/src/vuln_analysis/data/examples/exploitability_analyzer_input.json b/src/vuln_analysis/data/examples/exploitability_analyzer_input.json new file mode 100644 index 000000000..85a0a1445 --- /dev/null +++ b/src/vuln_analysis/data/examples/exploitability_analyzer_input.json @@ -0,0 +1,5 @@ +{ + "vulnerability_report_path": "outputs/vulnerability_reports/CVE-2025-47273.md", + "container_report_path": "outputs/container_reports/morpheus_25.06-runtime.md", + "filesystem_path": "/path/to/extracted/morpheus_25.06-runtime" +} diff --git a/src/vuln_analysis/data/examples/pipeline_input.json b/src/vuln_analysis/data/examples/pipeline_input.json new file mode 100644 index 000000000..3862ef49e --- /dev/null +++ b/src/vuln_analysis/data/examples/pipeline_input.json @@ -0,0 +1,8 @@ +{ + "vulns": [{"vuln_id": "CVE-2025-47273"}, + {"vuln_id": "CVE-2026-25990"}], + "image_name": "nvcr.io/nvidia/morpheus/morpheus", + "image_tag": "25.06-runtime", + "output_dir": "outputs", + "cache": true +} diff --git a/src/vuln_analysis/data/examples/vex_categorizer_input.json b/src/vuln_analysis/data/examples/vex_categorizer_input.json new file mode 100644 index 000000000..1bce3ee0e --- /dev/null +++ b/src/vuln_analysis/data/examples/vex_categorizer_input.json @@ -0,0 +1,4 @@ +{ + "exploitability_report_path": "outputs/exploitability_reports/CVE-2025-47273_morpheus_25.06-runtime.md", + "container_report_path": "outputs/container_reports/morpheus_25.06-runtime.md" +} diff --git a/src/vuln_analysis/data/input_messages/morpheus:23.11-runtime.json b/src/vuln_analysis/data/input_messages/morpheus:23.11-runtime.json deleted file mode 100644 index 21ebe640d..000000000 --- a/src/vuln_analysis/data/input_messages/morpheus:23.11-runtime.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "image": { - "name": "nvcr.io/nvidia/morpheus/morpheus", - "tag": "23.11-runtime", - "source_info": [ - { - "type": "code", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "v23.11.01", - "include": [ - "**/*.cpp", - "**/*.cu", - "**/*.cuh", - "**/*.h", - "**/*.hpp", - "**/*.ipynb", - "**/*.py", - "**/*Dockerfile" - ], - "exclude": [ - "tests/**/*" - ] - }, - { - "type": "doc", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "v23.11.01", - "include": [ - "**/*.md", - "docs/**/*.rst" - ] - } - ], - "sbom_info": { - "_type": "file", - "file_path": "data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom" - } - }, - "scan": { - "vulns": [ - { - "vuln_id": "GHSA-3f63-hfp8-52jq" - }, - { - "vuln_id": "CVE-2023-50782" - }, - { - "vuln_id": "CVE-2023-36632" - }, - { - "vuln_id": "CVE-2023-43804" - }, - { - "vuln_id": "GHSA-cxfr-5q3r-2rc2" - }, - { - "vuln_id": "GHSA-554w-xh4j-8w64" - }, - { - "vuln_id": "GHSA-3ww4-gg4f-jr7f" - }, - { - "vuln_id": "CVE-2023-51767" - } - ] - } -} diff --git a/src/vuln_analysis/data/input_messages/morpheus:24.03-runtime.json b/src/vuln_analysis/data/input_messages/morpheus:24.03-runtime.json deleted file mode 100644 index feb3c2025..000000000 --- a/src/vuln_analysis/data/input_messages/morpheus:24.03-runtime.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "image": { - "name": "nvcr.io/nvidia/morpheus/morpheus", - "tag": "v24.03.02-runtime", - "source_info": [ - { - "type": "code", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "v24.03.02", - "include": [ - "**/*.cpp", - "**/*.cu", - "**/*.cuh", - "**/*.h", - "**/*.hpp", - "**/*.ipynb", - "**/*.py", - "**/*Dockerfile" - ], - "exclude": [ - "tests/**/*" - ] - }, - { - "type": "doc", - "git_repo": "https://github.com/nv-morpheus/Morpheus.git", - "ref": "v24.03.02", - "include": [ - "**/*.md", - "docs/**/*.rst" - ] - } - ], - "sbom_info": { - "_type": "file", - "file_path": "data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom" - } - }, - "scan": { - "vulns": [ - { - "vuln_id": "GHSA-3f63-hfp8-52jq" - }, - { - "vuln_id": "CVE-2023-50782" - }, - { - "vuln_id": "CVE-2023-36632" - }, - { - "vuln_id": "CVE-2023-43804" - }, - { - "vuln_id": "GHSA-cxfr-5q3r-2rc2" - }, - { - "vuln_id": "GHSA-554w-xh4j-8w64" - }, - { - "vuln_id": "GHSA-3ww4-gg4f-jr7f" - } - ] - } -} diff --git a/src/vuln_analysis/data/profiler_datasets/profiler_dataset.json b/src/vuln_analysis/data/profiler_datasets/profiler_dataset.json deleted file mode 100644 index 1f4597f4e..000000000 --- a/src/vuln_analysis/data/profiler_datasets/profiler_dataset.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": 1, - "question": "{\"image\":{\"name\":\"nvcr.io\/nvidia\/morpheus\/morpheus\",\"tag\":\"23.11-runtime\",\"source_info\":[{\"type\":\"code\",\"git_repo\":\"https:\/\/github.com\/nv-morpheus\/Morpheus.git\",\"ref\":\"v23.11.01\",\"include\":[\"**\/*.cpp\",\"**\/*.cu\",\"**\/*.cuh\",\"**\/*.h\",\"**\/*.hpp\",\"**\/*.ipynb\",\"**\/*.py\",\"**\/*Dockerfile\"],\"exclude\":[\"tests\/**\/*\"]},{\"type\":\"doc\",\"git_repo\":\"https:\/\/github.com\/nv-morpheus\/Morpheus.git\",\"ref\":\"v23.11.01\",\"include\":[\"**\/*.md\",\"docs\/**\/*.rst\"]}],\"sbom_info\":{\"_type\":\"file\",\"file_path\":\"data\/sboms\/nvcr.io\/nvidia\/morpheus\/morpheus:v23.11.01-runtime.sbom\"}},\"scan\":{\"vulns\":[{\"vuln_id\":\"GHSA-3f63-hfp8-52jq\"},{\"vuln_id\":\"CVE-2023-50782\"}]}}", - "answer": "N/A" - } -] diff --git a/src/vuln_analysis/data/sboms/README.md b/src/vuln_analysis/data/sboms/README.md deleted file mode 100644 index e128d516d..000000000 --- a/src/vuln_analysis/data/sboms/README.md +++ /dev/null @@ -1,75 +0,0 @@ - - -## Overview -This directory contains the SBOMs for the containers used in the LLM example. An SBOM is a Software Bill of Materials. It is a machine-readable manifest of all the dependencies of a software package or container. The blueprint cross-references every entry in the SBOM for known vulnerabilities and looks at the code implementation to see whether the implementation puts users at risk—just as a security analyst would do. For this reason, starting with an accurate SBOM is an important first step. - -## SBOM Format -The expected SBOM format is a syft-table format, which is a human-readable table containing package information including name, version, and type. While syft supports multiple output formats (JSON, CycloneDX, SPDX), the blueprint expects the syft-table format for compatibility. - -## Generating an SBOM from a container - -To generate an SBOM for a container, you can use [syft](https://github.com/anchore/syft). - -To install syft, you can use the following command: - -```bash -curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin -``` - -For more installation options and detailed documentation, see the [official Syft documentation](https://github.com/anchore/syft#installation). - -The following steps show how to generate an SBOM for the Morpheus container. - -```bash -# Save the Morpheus repo directory -export VULN_ANALYSIS_ROOT=$(git rev-parse --show-toplevel) - -# Change directory to the SBOMs directory -cd ${VULN_ANALYSIS_ROOT}/data/sboms - -# Disable colors for syft -export NO_COLORS=y - -# Specify which container to generate an SBOM for -export CONTAINER="nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime" - -# Generate SBOM -syft scan ${CONTAINER} -o syft-table=${CONTAINER}.sbom -``` - -To generate an SBOM for a list of containers, you can use the following script: - -```bash -# Specify which containers to generate SBOMs for -export CONTAINERS=( - "nvcr.io/nvidia/morpheus/morpheus:24.03-runtime" - "nvcr.io/nvidia/morpheus/morpheus:23.11-runtime" -) - -# Generate SBOMs -for CONTAINER in "${CONTAINERS[@]}"; do - syft scan ${CONTAINER} -o syft-table=${CONTAINER}.sbom -done -``` - -## Converting SBOM Formats -If you have an SBOM in a different format, you can convert it to syft-table format using syft's convert command: - -```bash -syft convert cyclonedx.json -o syft-table=output.sbom -``` diff --git a/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom b/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom deleted file mode 100644 index cd469cc90..000000000 --- a/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom +++ /dev/null @@ -1,465 +0,0 @@ -NAME VERSION TYPE -Babel 2.13.1 python  -Brotli 1.0.9 python  -Brotli 1.1.0 python  -ConfigArgParse 1.5.5 python  -Flask 3.0.0 python  -GitPython 3.1.40 python  -Jinja2 3.1.2 python (+1 duplicate) -Mako 1.3.0 python  -Markdown 3.5.1 python  -MarkupSafe 2.1.3 python (+1 duplicate) -Pillow 10.1.0 python  -PyJWT 2.8.0 python  -PySocks 1.7.1 python (+1 duplicate) -PyYAML 6.0.1 python (+1 duplicate) -Pygments 2.17.2 python (+1 duplicate) -SQLAlchemy 2.0.0 python  -SciPy 1.11.4 python  -Simple Launcher 1.1.0.14 dotnet (+5 duplicates) -Sphinx 7.2.6 python  -Werkzeug 3.0.1 python  -absl-py 1.4.0 python  -adduser 3.118ubuntu5 deb  -alabaster 0.7.13 python  -alembic 1.13.0 python  -anyio 3.7.1 python  -appdirs 1.4.4 python  -apt 2.4.11 deb  -asn1crypto 1.5.1 python  -attrs 23.1.0 python (+1 duplicate) -base-files 12ubuntu4.4 deb  -base-passwd 3.5.52build1 deb  -bash 5.1-6ubuntu1 deb  -bc 1.07.1-3build1 deb  -beautifulsoup4 4.12.2 python  -betterproto 1.2.5 python  -binutils 2.38-4ubuntu2.3 deb  -binutils-common 2.38-4ubuntu2.3 deb  -binutils-x86-64-linux-gnu 2.38-4ubuntu2.3 deb  -blinker 1.7.0 python  -boa 0.16.0 python  -bokeh 2.4.3 python  -boltons 23.0.0 python  -bsdutils 1:2.37.2-4ubuntu3 deb  -build-essential 12.9ubuntu3 deb  -bzip2 1.0.8-5build1 deb  -ca-certificates 20230311ubuntu0.22.04.1 deb  -ca-certificates-java java-archive  -ca-certificates-java 20190909ubuntu1.2 deb  -cachetools 5.3.2 python  -cattrs 23.2.3 python  -certifi 2023.11.17 python (+1 duplicate) -cffi 1.15.1 python  -cffi 1.16.0 python  -chardet 5.2.0 python  -charset-normalizer 3.2.0 python  -charset-normalizer 3.3.2 python  -click 8.1.7 python (+1 duplicate) -cloudpickle 3.0.0 python  -colorama 0.4.6 python (+1 duplicate) -conda 23.3.1 python  -conda-build 3.28.1 python  -conda-libmamba-solver 23.3.0 python  -conda-package-handling 2.2.0 python  -conda_index 0.3.0 python  -conda_package_streaming 0.9.0 python  -configparser 5.3.0 python  -confluent-kafka 1.9.2 python  -contourpy 1.2.0 python  -coreutils 8.32-4.1ubuntu1 deb  -cpp 4:11.2.0-1ubuntu1 deb  -cpp-11 11.4.0-1ubuntu1~22.04 deb  -cryptography 41.0.3 python  -cryptography 41.0.7 python  -cubinlinker 0.3.0 python  -cuda-cccl-11-8 11.8.89-1 deb  -cuda-compat-11-8 520.61.05-1 deb  -cuda-compiler-11-8 11.8.0-1 deb  -cuda-cudart-11-8 11.8.89-1 deb  -cuda-cudart-dev-11-8 11.8.89-1 deb  -cuda-cuobjdump-11-8 11.8.86-1 deb  -cuda-cupti-11-8 11.8.87-1 deb  -cuda-cuxxfilt-11-8 11.8.86-1 deb  -cuda-driver-dev-11-8 11.8.89-1 deb  -cuda-gdb-11-8 11.8.86-1 deb  -cuda-keyring 1.1-1 deb  -cuda-nvcc-11-8 11.8.89-1 deb  -cuda-nvdisasm-11-8 11.8.86-1 deb  -cuda-nvprune-11-8 11.8.86-1 deb  -cuda-python 11.8.3 python  -cuda-toolkit-11-8-config-common 11.8.89-1 deb  -cuda-toolkit-11-config-common 11.8.89-1 deb  -cuda-toolkit-config-common 12.3.101-1 deb  -cudf 23.6.1 python  -cudf-kafka 23.6.1 python  -cupy 12.2.0 python  -curl 7.81.0-1ubuntu1.15 deb  -cycler 0.12.1 python  -cytoolz 0.12.2 python  -dash 0.5.11+git20210903+057cd650a4ed-3build1 deb  -dask 2023.3.2 python  -dask-cuda 23.6.0 python  -dask-cudf 23.6.1 python  -databricks-cli 0.18.0 python  -dataclasses 0.8 python  -datacompy 0.8.4 python  -debconf 1.5.79ubuntu1 deb  -debianutils 5.5-1ubuntu2 deb  -diffutils 1:3.8-0ubuntu2 deb  -dill 0.3.7 python  -dirmngr 2.2.27-3ubuntu2.1 deb  -distributed 2023.3.2.1 python  -docker 5.0.3 python  -docker-pycreds 0.4.0 python  -docutils 0.20.1 python  -dpkg 1.21.1ubuntu2.2 deb  -dpkg-dev 1.21.1ubuntu2.2 deb  -e2fsprogs 1.46.5-2ubuntu1.1 deb  -entrypoints 0.4 python  -exceptiongroup 1.2.0 python (+1 duplicate) -fastrlock 0.8.2 python  -filelock 3.13.1 python (+1 duplicate) -findutils 4.8.0-1ubuntu3 deb  -fontconfig-config 2.13.1-4.2ubuntu5 deb  -fonts-dejavu-core 2.37-2build1 deb  -fonttools 4.46.0 python  -fsspec 2023.12.1 python  -g++ 4:11.2.0-1ubuntu1 deb  -g++-11 11.4.0-1ubuntu1~22.04 deb  -gcc 4:11.2.0-1ubuntu1 deb  -gcc-11 11.4.0-1ubuntu1~22.04 deb  -gcc-11-base 11.4.0-1ubuntu1~22.04 deb  -gcc-12-base 12.3.0-1ubuntu1~22.04 deb  -gitdb 4.0.11 python  -gmpy2 2.1.2 python  -gnupg 2.2.27-3ubuntu2.1 deb  -gnupg-l10n 2.2.27-3ubuntu2.1 deb  -gnupg-utils 2.2.27-3ubuntu2.1 deb  -gnupg2 2.2.27-3ubuntu2.1 deb  -googleapis-common-protos 1.61.0 python  -gpg 2.2.27-3ubuntu2.1 deb  -gpg-agent 2.2.27-3ubuntu2.1 deb  -gpg-wks-client 2.2.27-3ubuntu2.1 deb  -gpg-wks-server 2.2.27-3ubuntu2.1 deb  -gpgconf 2.2.27-3ubuntu2.1 deb  -gpgsm 2.2.27-3ubuntu2.1 deb  -gpgv 2.2.27-3ubuntu2.1 deb  -greenlet 3.0.1 python  -grep 3.7-1build1 deb  -grpcio 1.54.2 python  -grpclib 0.4.6 python  -gunicorn 21.2.0 python  -gzip 1.10-4ubuntu4.1 deb  -h2 4.1.0 python  -hostname 3.23ubuntu2 deb  -hpack 4.0.0 python  -hyperframe 6.0.1 python  -idna 3.4 python  -idna 3.6 python  -imagesize 1.4.1 python  -importlib-metadata 7.0.0 python  -importlib-resources 6.1.1 python (+1 duplicate) -init-system-helpers 1.62 deb  -itsdangerous 2.1.2 python  -java-common 0.72build2 deb  -joblib 1.3.2 python (+1 duplicate) -jq 1.6-2.1ubuntu3 deb  -jrt-fs 11.0.21 java-archive  -js 1.0.0 npm  -json5 0.9.14 python  -jsonpatch 1.32 python  -jsonpointer 2.0 python  -jsonschema 4.20.0 python  -jsonschema-specifications 2023.11.2 python  -jupyter_client 8.6.0 python  -jupyter_core 5.5.0 python  -kiwisolver 1.4.5 python  -libacl1 2.3.1-1 deb  -libapt-pkg6.0 2.4.11 deb  -libarchive-c 5.0 python  -libasan6 11.4.0-1ubuntu1~22.04 deb  -libasound2 1.2.6.1-1ubuntu1 deb  -libasound2-data 1.2.6.1-1ubuntu1 deb  -libassuan0 2.5.5-1build1 deb  -libatomic1 12.3.0-1ubuntu1~22.04 deb  -libattr1 1:2.5.1-1build1 deb  -libaudit-common 1:3.0.7-1build1 deb  -libaudit1 1:3.0.7-1build1 deb  -libavahi-client3 0.8-5ubuntu5.2 deb  -libavahi-common-data 0.8-5ubuntu5.2 deb  -libavahi-common3 0.8-5ubuntu5.2 deb  -libbinutils 2.38-4ubuntu2.3 deb  -libblkid1 2.37.2-4ubuntu3 deb  -libbrotli1 1.0.9-2build6 deb  -libbsd0 0.11.5-1 deb  -libbz2-1.0 1.0.8-5build1 deb  -libc-bin 2.35-0ubuntu3.5 deb  -libc-dev-bin 2.35-0ubuntu3.5 deb  -libc6 2.35-0ubuntu3.5 deb  -libc6-dev 2.35-0ubuntu3.5 deb  -libcap-ng0 0.7.9-2.2build3 deb  -libcap2 1:2.44-1ubuntu0.22.04.1 deb  -libcbor0.8 0.8.0-2ubuntu1 deb  -libcc1-0 12.3.0-1ubuntu1~22.04 deb  -libcom-err2 1.46.5-2ubuntu1.1 deb  -libcrypt-dev 1:4.4.27-1 deb  -libcrypt1 1:4.4.27-1 deb  -libctf-nobfd0 2.38-4ubuntu2.3 deb  -libctf0 2.38-4ubuntu2.3 deb  -libcublas-11-8 11.11.3.6-1 deb  -libcufft-11-8 10.9.0.58-1 deb  -libcups2 2.4.1op1-1ubuntu4.7 deb  -libcurand-11-8 10.3.0.86-1 deb  -libcurl4 7.81.0-1ubuntu1.15 deb  -libcusolver-11-8 11.4.1.48-1 deb  -libdb5.3 5.3.28+dfsg1-0.8ubuntu3 deb  -libdbus-1-3 1.12.20-2ubuntu4.1 deb  -libdebconfclient0 0.261ubuntu1 deb  -libdpkg-perl 1.21.1ubuntu2.2 deb  -libedit2 3.1-20210910-1build1 deb  -libexpat1 2.4.7-1ubuntu0.2 deb  -libext2fs2 1.46.5-2ubuntu1.1 deb  -libffi8 3.4.2-4 deb  -libfido2-1 1.10.0-1 deb  -libfontconfig1 2.13.1-4.2ubuntu5 deb  -libfreetype6 2.11.1+dfsg-1ubuntu0.2 deb  -libgcc-11-dev 11.4.0-1ubuntu1~22.04 deb  -libgcc-s1 12.3.0-1ubuntu1~22.04 deb  -libgcrypt20 1.9.4-3ubuntu3 deb  -libgdbm-compat4 1.23-1 deb  -libgdbm6 1.23-1 deb  -libglib2.0-0 2.72.4-0ubuntu2.2 deb  -libgmp10 2:6.2.1+dfsg-3ubuntu1 deb  -libgnutls30 3.7.3-4ubuntu1.3 deb  -libgomp1 12.3.0-1ubuntu1~22.04 deb  -libgpg-error0 1.43-3 deb  -libgraphite2-3 1.3.14-1build2 deb  -libgssapi-krb5-2 1.19.2-2ubuntu0.3 deb  -libharfbuzz0b 2.7.4-1ubuntu3.1 deb  -libhogweed6 3.7.3-1build2 deb  -libidn2-0 2.3.2-2build1 deb  -libisl23 0.24-2build1 deb  -libitm1 12.3.0-1ubuntu1~22.04 deb  -libjpeg-turbo8 2.1.2-0ubuntu1 deb  -libjpeg8 8c-2ubuntu10 deb  -libjq1 1.6-2.1ubuntu3 deb  -libk5crypto3 1.19.2-2ubuntu0.3 deb  -libkeyutils1 1.6.1-2ubuntu3 deb  -libkrb5-3 1.19.2-2ubuntu0.3 deb  -libkrb5support0 1.19.2-2ubuntu0.3 deb  -libksba8 1.6.0-2ubuntu0.2 deb  -liblcms2-2 2.12~rc1-2build2 deb  -libldap-2.5-0 2.5.16+dfsg-0ubuntu0.22.04.1 deb  -liblsan0 12.3.0-1ubuntu1~22.04 deb  -liblz4-1 1.9.3-2build2 deb  -liblzma5 5.2.5-2ubuntu1 deb  -libmambapy 1.5.0 python  -libmd0 1.0.4-1build1 deb  -libmount1 2.37.2-4ubuntu3 deb  -libmpc3 1.2.1-2build1 deb  -libmpfr6 4.1.0-3build3 deb  -libncurses6 6.3-2ubuntu0.1 deb  -libncursesw6 6.3-2ubuntu0.1 deb  -libnettle8 3.7.3-1build2 deb  -libnghttp2-14 1.43.0-1ubuntu0.1 deb  -libnpth0 1.6-3build2 deb  -libnsl-dev 1.3.0-2build2 deb  -libnsl2 1.3.0-2build2 deb  -libnspr4 2:4.32-3build1 deb  -libnss3 2:3.68.2-0ubuntu1.2 deb  -libnuma1 2.0.14-3ubuntu2 deb  -libonig5 6.9.7.1-2build1 deb  -libp11-kit0 0.24.0-6build1 deb  -libpam-modules 1.4.0-11ubuntu2.3 deb  -libpam-modules-bin 1.4.0-11ubuntu2.3 deb  -libpam-runtime 1.4.0-11ubuntu2.3 deb  -libpam0g 1.4.0-11ubuntu2.3 deb  -libpcre2-8-0 10.39-3ubuntu0.1 deb  -libpcre3 2:8.39-13ubuntu0.22.04.1 deb  -libpcsclite1 1.9.5-3ubuntu1 deb  -libperl5.34 5.34.0-3ubuntu1.3 deb  -libpng16-16 1.6.37-3build5 deb  -libprocps8 2:3.3.17-6ubuntu2.1 deb  -libpsl5 0.21.0-1.2build2 deb  -libquadmath0 12.3.0-1ubuntu1~22.04 deb  -libreadline8 8.1.2-1 deb  -librtmp1 2.4+20151223.gitfa8646d.1-2build4 deb  -libsasl2-2 2.1.27+dfsg2-3ubuntu1.2 deb  -libsasl2-modules-db 2.1.27+dfsg2-3ubuntu1.2 deb  -libseccomp2 2.5.3-2ubuntu2 deb  -libselinux1 3.3-1build2 deb  -libsemanage-common 3.3-1build2 deb  -libsemanage2 3.3-1build2 deb  -libsepol2 3.3-1build1 deb  -libsmartcols1 2.37.2-4ubuntu3 deb  -libsqlite3-0 3.37.2-2ubuntu0.1 deb  -libss2 1.46.5-2ubuntu1.1 deb  -libssh-4 0.9.6-2ubuntu0.22.04.1 deb  -libssl3 3.0.2-0ubuntu1.12 deb  -libstdc++-11-dev 11.4.0-1ubuntu1~22.04 deb  -libstdc++6 12.3.0-1ubuntu1~22.04 deb  -libsystemd0 249.11-0ubuntu3.11 deb  -libtasn1-6 4.18.0-4build1 deb  -libtinfo6 6.3-2ubuntu0.1 deb  -libtirpc-common 1.3.2-2ubuntu0.1 deb  -libtirpc-dev 1.3.2-2ubuntu0.1 deb  -libtirpc3 1.3.2-2ubuntu0.1 deb  -libtsan0 11.4.0-1ubuntu1~22.04 deb  -libubsan1 12.3.0-1ubuntu1~22.04 deb  -libudev1 249.11-0ubuntu3.11 deb  -libunistring2 1.0-1 deb  -libuuid1 2.37.2-4ubuntu3 deb  -libxxhash0 0.8.1-1 deb  -libzstd1 1.4.8+dfsg-3build1 deb  -linux-libc-dev 5.15.0-89.99 deb  -llvmlite 0.41.1 python  -locket 1.0.0 python  -login 1:4.8.1-2ubuntu2.1 deb  -logsave 1.46.5-2ubuntu1.1 deb  -lsb-base 11.1.0ubuntu4 deb  -lto-disabled-list 24 deb  -lz4 4.3.2 python  -make 4.3-4.1build1 deb  -mamba 1.5.0 python  -markdown-it-py 3.0.0 python  -matplotlib 3.8.2 python  -mawk 1.3.4.20200120-3 deb  -mdurl 0.1.0 python  -menuinst 2.0.0 python  -merlin-core 23.6.0 python  -merlin-dataloader 23.6.0 python  -mlflow 2.9.1 python  -more-itertools 10.1.0 python  -morpheus 23.11.1 python  -mount 2.37.2-4ubuntu3 deb  -mpmath 1.3.0 python  -mrc 23.11.0 python  -msgpack 1.0.7 python  -multidict 6.0.4 python  -munkres 1.1.4 python  -nb-conda-kernels 2.3.1 python  -ncurses-base 6.3-2ubuntu0.1 deb  -ncurses-bin 6.3-2ubuntu0.1 deb  -networkx 3.2 python  -npy-append-array 0.9.16 python  -numba 0.58.1 python  -numpy 1.26.2 python  -numpydoc 1.4.0 python  -nvtabular 23.6.0 python  -nvtx 0.2.8 python  -oauthlib 3.2.2 python  -openjdk-11-jre-headless 11.0.21+9-0ubuntu1~22.04 deb  -openssh-client 1:8.9p1-3ubuntu0.4 deb  -openssl 3.0.2-0ubuntu1.12 deb  -openssl 3.2.0 binary  -ordered-set 4.1.0 python  -packaging 23.1 python  -packaging 23.2 python  -pandas 1.3.5 python  -partd 1.4.1 python  -passwd 1:4.8.1-2ubuntu2.1 deb  -patch 2.7.6-7build2 deb  -perl 5.34.0-3ubuntu1.3 deb  -perl-base 5.34.0-3ubuntu1.3 deb  -perl-modules-5.34 5.34.0-3ubuntu1.3 deb  -pinentry-curses 1.1.1-1build2 deb  -pip 23.3.1 python  -pkg-config 0.29.2-1ubuntu3 deb  -pkginfo 1.9.6 python  -pkgutil_resolve_name 1.3.10 python  -platformdirs 4.1.0 python  -pluggy 1.0.0 python  -pluggy 1.3.0 python  -procps 2:3.3.17-6ubuntu2.1 deb  -prometheus-client 0.19.0 python  -prometheus-flask-exporter 0.23.0 python  -prompt-toolkit 3.0.41 python  -protobuf 4.21.12 python  -psutil 5.9.5 python (+1 duplicate) -ptxcompiler 0.8.1 python  -pyOpenSSL 23.2.0 python  -pyOpenSSL 23.3.0 python  -pyarrow 11.0.0 python  -pyarrow-hotfix 0.6 python  -pycosat 0.6.4 python  -pycparser 2.21 python (+1 duplicate) -pynvml 11.4.1 python  -pyparsing 3.1.1 python  -python 3.10.12 binary  -python 3.10.13 binary  -python-dateutil 2.8.2 python  -python-rapidjson 1.13 python  -pytz 2023.3.post1 python (+1 duplicate) -pyzmq 25.1.2 python  -querystring-parser 1.2.4 python  -readline-common 8.1.2-1 deb  -referencing 0.31.1 python  -requests 2.31.0 python (+1 duplicate) -requests-cache 1.1.1 python  -rich 13.7.0 python  -rmm 23.6.0 python  -rpcsvc-proto 1.4.2-0ubuntu6 deb  -rpds-py 0.13.2 python  -ruamel.yaml 0.17.32 python  -ruamel.yaml.clib 0.2.7 python  -scikit-learn 1.2.2 python  -sed 4.8-1ubuntu2 deb  -sensible-utils 0.0.17 deb  -setuptools 59.8.0 python  -setuptools 68.1.2 python  -six 1.16.0 python  -smmap 5.0.0 python  -sniffio 1.3.0 python  -snowballstemmer 2.2.0 python  -sortedcontainers 2.4.0 python  -soupsieve 2.5 python  -sphinxcontrib-applehelp 1.0.7 python  -sphinxcontrib-devhelp 1.0.5 python  -sphinxcontrib-htmlhelp 2.0.4 python  -sphinxcontrib-jsmath 1.0.1 python  -sphinxcontrib-qthelp 1.0.6 python  -sphinxcontrib-serializinghtml 1.1.9 python  -sqlparse 0.4.4 python  -stringcase 1.2.0 python  -sympy 1.12 python  -sysvinit-utils 3.01-1ubuntu1 deb  -tabulate 0.9.0 python  -tar 1.34+dfsg-1ubuntu0.1.22.04.1 deb  -tblib 2.0.0 python  -tensorflow-metadata 1.13.1 python  -threadpoolctl 3.2.0 python  -tomli 2.0.1 python  -toolz 0.12.0 python (+1 duplicate) -torch 2.0.1 python  -tornado 6.3.3 python  -tqdm 4.66.1 python (+1 duplicate) -traitlets 5.14.0 python  -triton 2.0.0 python  -tritonclient 2.26.0 python  -typing-utils 0.1.0 python  -typing_extensions 4.8.0 python (+1 duplicate) -ubuntu-keyring 2021.03.26 deb  -ucf 3.0043 deb  -ujson 5.8.0 python  -unicodedata2 15.1.0 python  -unzip 6.0-26ubuntu3.1 deb  -url-normalize 1.4.3 python  -urllib3 2.0.4 python  -urllib3 2.1.0 python  -usrmerge 25ubuntu2 deb  -util-linux 2.37.2-4ubuntu3 deb  -watchdog 2.1.9 python  -watchgod 0.8.2 python  -wcwidth 0.2.12 python  -websocket-client 1.7.0 python  -websockets 12.0 python  -wget 1.21.2-2ubuntu1 deb  -wheel 0.41.2 python  -wheel 0.42.0 python  -xz-utils 5.2.5-2ubuntu1 deb  -zict 3.0.0 python  -zip 3.0-12build2 deb  -zipp 3.17.0 python (+1 duplicate) -zlib1g 1:1.2.11.dfsg-2ubuntu9.2 deb  -zstandard 0.19.0 python  diff --git a/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom b/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom deleted file mode 100644 index ceef72f3d..000000000 --- a/src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom +++ /dev/null @@ -1,493 +0,0 @@ -NAME VERSION TYPE -Babel 2.14.0 python  -Brotli 1.1.0 python (+1 duplicate) -ConfigArgParse 1.5.5 python  -Flask 3.0.3 python  -GitPython 3.1.43 python  -Jinja2 3.1.3 python (+1 duplicate) -Mako 1.3.3 python  -Markdown 3.6 python  -MarkupSafe 2.1.5 python (+1 duplicate) -PyJWT 2.8.0 python  -PySocks 1.7.1 python (+1 duplicate) -PyYAML 6.0.1 python (+1 duplicate) -Pygments 2.17.2 python (+1 duplicate) -SQLAlchemy 1.4.49 python  -Simple Launcher 1.1.0.14 dotnet (+5 duplicates) -Sphinx 7.3.7 python  -Werkzeug 3.0.2 python  -absl-py 1.4.0 python  -adagio 0.2.4 python  -adduser 3.118ubuntu5 deb  -alabaster 0.7.16 python  -alembic 1.13.1 python  -aniso8601 9.0.1 python  -annotated-types 0.6.0 python  -antlr4-python3-runtime 4.11.1 python  -anyio 3.7.1 python  -appdirs 1.4.4 python  -apt 2.4.12 deb  -archspec 0.2.3 python  -argon2-cffi 23.1.0 python  -argon2-cffi-bindings 21.2.0 python  -asn1crypto 1.5.1 python  -attrs 23.2.0 python (+1 duplicate) -base-files 12ubuntu4.6 deb  -base-passwd 3.5.52build1 deb  -bash 5.1-6ubuntu1.1 deb  -bc 1.07.1-3build1 deb  -beautifulsoup4 4.12.3 python  -betterproto 1.2.5 python  -binutils 2.38-4ubuntu2.6 deb  -binutils-common 2.38-4ubuntu2.6 deb  -binutils-x86-64-linux-gnu 2.38-4ubuntu2.6 deb  -blinker 1.7.0 python  -boa 0.17.0 python  -bokeh 3.4.1 python  -boltons 24.0.0 python  -bsdutils 1:2.37.2-4ubuntu3.4 deb  -build-essential 12.9ubuntu3 deb  -bzip2 1.0.8-5build1 deb  -ca-certificates 20230311ubuntu0.22.04.1 deb  -ca-certificates-java java-archive  -ca-certificates-java 20190909ubuntu1.2 deb  -cachetools 5.3.3 python  -cattrs 23.2.3 python  -certifi 2024.2.2 python (+1 duplicate) -cffi 1.16.0 python (+1 duplicate) -chardet 5.2.0 python  -charset-normalizer 3.3.2 python (+1 duplicate) -click 8.1.7 python (+1 duplicate) -cloudpickle 3.0.0 python  -colorama 0.4.6 python (+1 duplicate) -conda 24.3.0 python  -conda-build 24.3.0 python  -conda-libmamba-solver 24.1.0 python  -conda-package-handling 2.2.0 python  -conda_index 0.4.0 python  -conda_package_streaming 0.9.0 python  -confluent-kafka 1.9.2 python  -contourpy 1.2.1 python  -coreutils 8.32-4.1ubuntu1.2 deb  -cpp 4:11.2.0-1ubuntu1 deb  -cpp-11 11.4.0-1ubuntu1~22.04 deb  -cryptography 42.0.5 python  -cuda-cccl-12-1 12.1.109-1 deb  -cuda-compat-12-1 530.30.02-1 deb  -cuda-compiler-12-1 12.1.1-1 deb  -cuda-cudart-12-1 12.1.105-1 deb  -cuda-cudart-dev-12-1 12.1.105-1 deb  -cuda-cuobjdump-12-1 12.1.111-1 deb  -cuda-cupti-12-1 12.1.105-1 deb  -cuda-cuxxfilt-12-1 12.1.105-1 deb  -cuda-driver-dev-12-1 12.1.105-1 deb  -cuda-gdb-12-1 12.1.105-1 deb  -cuda-keyring 1.1-1 deb  -cuda-nvcc-12-1 12.1.105-1 deb  -cuda-nvdisasm-12-1 12.1.105-1 deb  -cuda-nvprune-12-1 12.1.105-1 deb  -cuda-python 12.4.0 python  -cuda-toolkit-12-1-config-common 12.1.105-1 deb  -cuda-toolkit-12-config-common 12.4.127-1 deb  -cuda-toolkit-config-common 12.4.127-1 deb  -cudf 24.2.2 python  -cupy 13.1.0 python  -curl 7.81.0-1ubuntu1.16 deb  -cycler 0.12.1 python  -cytoolz 0.12.3 python  -dash 0.5.11+git20210903+057cd650a4ed-3build1 deb  -dask 2024.1.1 python  -dask-cuda 24.2.0 python  -dask-cudf 24.2.2 python  -databricks-cli 0.18.0 python  -databricks-connect 14.3.1 python  -databricks-sdk 0.26.0 python  -dataclasses 0.8 python  -datacompy 0.10.5 python  -debconf 1.5.79ubuntu1 deb  -debianutils 5.5-1ubuntu2 deb  -diffutils 1:3.8-0ubuntu2 deb  -dill 0.3.7 python  -dirmngr 2.2.27-3ubuntu2.1 deb  -distributed 2024.1.1 python  -distro 1.9.0 python  -docker 5.0.3 python  -docker-pycreds 0.4.0 python  -docutils 0.21.2 python  -dpkg 1.21.1ubuntu2.3 deb  -dpkg-dev 1.21.1ubuntu2.3 deb  -e2fsprogs 1.46.5-2ubuntu1.1 deb  -elastic-transport 8.13.0 python  -elasticsearch 8.9.0 python  -entrypoints 0.4 python  -environs 9.5.0 python  -exceptiongroup 1.2.0 python (+1 duplicate) -fastrlock 0.8.2 python  -feedparser 6.0.10 python  -filelock 3.13.4 python (+1 duplicate) -findutils 4.8.0-1ubuntu3 deb  -fontconfig-config 2.13.1-4.2ubuntu5 deb  -fonts-dejavu-core 2.37-2build1 deb  -fonttools 4.51.0 python  -fs 2.4.16 python  -fsspec 2024.3.1 python  -fugue 0.8.7 python  -fugue-sql-antlr 0.2.0 python  -g++ 4:11.2.0-1ubuntu1 deb  -g++-11 11.4.0-1ubuntu1~22.04 deb  -gcc 4:11.2.0-1ubuntu1 deb  -gcc-11 11.4.0-1ubuntu1~22.04 deb  -gcc-11-base 11.4.0-1ubuntu1~22.04 deb  -gcc-12-base 12.3.0-1ubuntu1~22.04 deb  -gitdb 4.0.11 python  -gmpy2 2.1.5 python  -gnupg 2.2.27-3ubuntu2.1 deb  -gnupg-l10n 2.2.27-3ubuntu2.1 deb  -gnupg-utils 2.2.27-3ubuntu2.1 deb  -gnupg2 2.2.27-3ubuntu2.1 deb  -google-auth 2.29.0 python  -googleapis-common-protos 1.63.0 python  -gpg 2.2.27-3ubuntu2.1 deb  -gpg-agent 2.2.27-3ubuntu2.1 deb  -gpg-wks-client 2.2.27-3ubuntu2.1 deb  -gpg-wks-server 2.2.27-3ubuntu2.1 deb  -gpgconf 2.2.27-3ubuntu2.1 deb  -gpgsm 2.2.27-3ubuntu2.1 deb  -gpgv 2.2.27-3ubuntu2.1 deb  -graphene 3.3 python  -graphql-core 3.2.3 python  -graphql-relay 3.2.0 python  -graphviz 0.20.3 python  -greenlet 3.0.3 python  -grep 3.7-1build1 deb  -grpcio 1.60.0 python  -grpcio-status 1.60.0 python  -grpclib 0.4.7 python  -gunicorn 21.2.0 python  -gzip 1.10-4ubuntu4.1 deb  -h2 4.1.0 python  -hostname 3.23ubuntu2 deb  -hpack 4.0.0 python  -hyperframe 6.0.1 python  -idna 3.6 python  -idna 3.7 python  -imagesize 1.4.1 python  -importlib_metadata 7.1.0 python  -importlib_resources 6.4.0 python (+1 duplicate) -init-system-helpers 1.62 deb  -itsdangerous 2.2.0 python  -java-common 0.72build2 deb  -joblib 1.4.0 python (+1 duplicate) -jq 1.6-2.1ubuntu3 deb  -jrt-fs 11.0.22 java-archive  -js 1.0.0 npm  -json5 0.9.25 python  -jsonpatch 1.33 python  -jsonpointer 2.4 python  -jsonschema 4.21.1 python  -jsonschema-specifications 2023.12.1 python  -kiwisolver 1.4.5 python  -libacl1 2.3.1-1 deb  -libapt-pkg6.0 2.4.12 deb  -libarchive-c 5.1 python  -libasan6 11.4.0-1ubuntu1~22.04 deb  -libasound2 1.2.6.1-1ubuntu1 deb  -libasound2-data 1.2.6.1-1ubuntu1 deb  -libassuan0 2.5.5-1build1 deb  -libatomic1 12.3.0-1ubuntu1~22.04 deb  -libattr1 1:2.5.1-1build1 deb  -libaudit-common 1:3.0.7-1build1 deb  -libaudit1 1:3.0.7-1build1 deb  -libavahi-client3 0.8-5ubuntu5.2 deb  -libavahi-common-data 0.8-5ubuntu5.2 deb  -libavahi-common3 0.8-5ubuntu5.2 deb  -libbinutils 2.38-4ubuntu2.6 deb  -libblkid1 2.37.2-4ubuntu3.4 deb  -libbrotli1 1.0.9-2build6 deb  -libbsd0 0.11.5-1 deb  -libbz2-1.0 1.0.8-5build1 deb  -libc-bin 2.35-0ubuntu3.7 deb  -libc-dev-bin 2.35-0ubuntu3.7 deb  -libc6 2.35-0ubuntu3.7 deb  -libc6-dev 2.35-0ubuntu3.7 deb  -libcap-ng0 0.7.9-2.2build3 deb  -libcap2 1:2.44-1ubuntu0.22.04.1 deb  -libcbor0.8 0.8.0-2ubuntu1 deb  -libcc1-0 12.3.0-1ubuntu1~22.04 deb  -libcom-err2 1.46.5-2ubuntu1.1 deb  -libcrypt-dev 1:4.4.27-1 deb  -libcrypt1 1:4.4.27-1 deb  -libctf-nobfd0 2.38-4ubuntu2.6 deb  -libctf0 2.38-4ubuntu2.6 deb  -libcublas-12-1 12.1.3.1-1 deb  -libcufft-12-1 11.0.2.54-1 deb  -libcups2 2.4.1op1-1ubuntu4.8 deb  -libcurand-12-1 10.3.2.106-1 deb  -libcurl4 7.81.0-1ubuntu1.16 deb  -libcusolver-12-1 11.4.5.107-1 deb  -libdb5.3 5.3.28+dfsg1-0.8ubuntu3 deb  -libdbus-1-3 1.12.20-2ubuntu4.1 deb  -libdebconfclient0 0.261ubuntu1 deb  -libdpkg-perl 1.21.1ubuntu2.3 deb  -libedit2 3.1-20210910-1build1 deb  -libexpat1 2.4.7-1ubuntu0.3 deb  -libext2fs2 1.46.5-2ubuntu1.1 deb  -libffi8 3.4.2-4 deb  -libfido2-1 1.10.0-1 deb  -libfontconfig1 2.13.1-4.2ubuntu5 deb  -libfreetype6 2.11.1+dfsg-1ubuntu0.2 deb  -libgcc-11-dev 11.4.0-1ubuntu1~22.04 deb  -libgcc-s1 12.3.0-1ubuntu1~22.04 deb  -libgcrypt20 1.9.4-3ubuntu3 deb  -libgdbm-compat4 1.23-1 deb  -libgdbm6 1.23-1 deb  -libglib2.0-0 2.72.4-0ubuntu2.2 deb  -libgmp10 2:6.2.1+dfsg-3ubuntu1 deb  -libgnutls30 3.7.3-4ubuntu1.5 deb  -libgomp1 12.3.0-1ubuntu1~22.04 deb  -libgpg-error0 1.43-3 deb  -libgraphite2-3 1.3.14-1build2 deb  -libgssapi-krb5-2 1.19.2-2ubuntu0.3 deb  -libharfbuzz0b 2.7.4-1ubuntu3.1 deb  -libhogweed6 3.7.3-1build2 deb  -libidn2-0 2.3.2-2build1 deb  -libisl23 0.24-2build1 deb  -libitm1 12.3.0-1ubuntu1~22.04 deb  -libjpeg-turbo8 2.1.2-0ubuntu1 deb  -libjpeg8 8c-2ubuntu10 deb  -libjq1 1.6-2.1ubuntu3 deb  -libk5crypto3 1.19.2-2ubuntu0.3 deb  -libkeyutils1 1.6.1-2ubuntu3 deb  -libkrb5-3 1.19.2-2ubuntu0.3 deb  -libkrb5support0 1.19.2-2ubuntu0.3 deb  -libksba8 1.6.0-2ubuntu0.2 deb  -liblcms2-2 2.12~rc1-2build2 deb  -libldap-2.5-0 2.5.17+dfsg-0ubuntu0.22.04.1 deb  -liblsan0 12.3.0-1ubuntu1~22.04 deb  -liblz4-1 1.9.3-2build2 deb  -liblzma5 5.2.5-2ubuntu1 deb  -libmambapy 1.5.8 python  -libmd0 1.0.4-1build1 deb  -libmount1 2.37.2-4ubuntu3.4 deb  -libmpc3 1.2.1-2build1 deb  -libmpfr6 4.1.0-3build3 deb  -libncurses6 6.3-2ubuntu0.1 deb  -libncursesw6 6.3-2ubuntu0.1 deb  -libnettle8 3.7.3-1build2 deb  -libnghttp2-14 1.43.0-1ubuntu0.1 deb  -libnpth0 1.6-3build2 deb  -libnsl-dev 1.3.0-2build2 deb  -libnsl2 1.3.0-2build2 deb  -libnspr4 2:4.35-0ubuntu0.22.04.1 deb  -libnss3 2:3.98-0ubuntu0.22.04.2 deb  -libnuma1 2.0.14-3ubuntu2 deb  -libonig5 6.9.7.1-2build1 deb  -libp11-kit0 0.24.0-6build1 deb  -libpam-modules 1.4.0-11ubuntu2.4 deb  -libpam-modules-bin 1.4.0-11ubuntu2.4 deb  -libpam-runtime 1.4.0-11ubuntu2.4 deb  -libpam0g 1.4.0-11ubuntu2.4 deb  -libpcre2-8-0 10.39-3ubuntu0.1 deb  -libpcre3 2:8.39-13ubuntu0.22.04.1 deb  -libpcsclite1 1.9.5-3ubuntu1 deb  -libperl5.34 5.34.0-3ubuntu1.3 deb  -libpng16-16 1.6.37-3build5 deb  -libprocps8 2:3.3.17-6ubuntu2.1 deb  -libpsl5 0.21.0-1.2build2 deb  -libquadmath0 12.3.0-1ubuntu1~22.04 deb  -libreadline8 8.1.2-1 deb  -librtmp1 2.4+20151223.gitfa8646d.1-2build4 deb  -libsasl2-2 2.1.27+dfsg2-3ubuntu1.2 deb  -libsasl2-modules-db 2.1.27+dfsg2-3ubuntu1.2 deb  -libseccomp2 2.5.3-2ubuntu2 deb  -libselinux1 3.3-1build2 deb  -libsemanage-common 3.3-1build2 deb  -libsemanage2 3.3-1build2 deb  -libsepol2 3.3-1build1 deb  -libsmartcols1 2.37.2-4ubuntu3.4 deb  -libsqlite3-0 3.37.2-2ubuntu0.3 deb  -libss2 1.46.5-2ubuntu1.1 deb  -libssh-4 0.9.6-2ubuntu0.22.04.3 deb  -libssl3 3.0.2-0ubuntu1.15 deb  -libstdc++-11-dev 11.4.0-1ubuntu1~22.04 deb  -libstdc++6 12.3.0-1ubuntu1~22.04 deb  -libsystemd0 249.11-0ubuntu3.12 deb  -libtasn1-6 4.18.0-4build1 deb  -libtinfo6 6.3-2ubuntu0.1 deb  -libtirpc-common 1.3.2-2ubuntu0.1 deb  -libtirpc-dev 1.3.2-2ubuntu0.1 deb  -libtirpc3 1.3.2-2ubuntu0.1 deb  -libtsan0 11.4.0-1ubuntu1~22.04 deb  -libubsan1 12.3.0-1ubuntu1~22.04 deb  -libudev1 249.11-0ubuntu3.12 deb  -libunistring2 1.0-1 deb  -libuuid1 2.37.2-4ubuntu3.4 deb  -libxxhash0 0.8.1-1 deb  -libzstd1 1.4.8+dfsg-3build1 deb  -lief 0.14.1 python  -linux-libc-dev 5.15.0-105.115 deb  -llvmlite 0.42.0 python  -locket 1.0.0 python  -login 1:4.8.1-2ubuntu2.2 deb  -logsave 1.46.5-2ubuntu1.1 deb  -lsb-base 11.1.0ubuntu4 deb  -lto-disabled-list 24 deb  -lz4 4.3.3 python  -make 4.3-4.1build1 deb  -mamba 1.5.8 python  -markdown-it-py 3.0.0 python (+1 duplicate) -marshmallow 3.21.1 python  -matplotlib 3.8.4 python  -mawk 1.3.4.20200120-3 deb  -mdurl 0.1.2 python (+1 duplicate) -menuinst 2.0.2 python  -merlin-core 23.8.0 python  -merlin-dataloader 23.8.0 python  -milvus 2.3.5 python  -minio 7.2.5 python  -mlflow 2.12.1 python  -mlflow-skinny 2.12.1 python  -more-itertools 10.2.0 python  -morpheus 24.3.2 python  -mount 2.37.2-4ubuntu3.4 deb  -mpmath 1.3.0 python  -mrc 24.3.1 python  -msgpack 1.0.7 python  -multidict 6.0.5 python  -munkres 1.1.4 python  -ncurses-base 6.3-2ubuntu0.1 deb  -ncurses-bin 6.3-2ubuntu0.1 deb  -networkx 2.8.8 python  -npy-append-array 0.9.16 python  -numba 0.59.1 python  -numpy 1.24.4 python  -numpydoc 1.5.0 python  -nvtabular 23.8.0 python  -nvtx 0.2.10 python  -oauthlib 3.2.2 python  -openjdk-11-jre-headless 11.0.22+7-0ubuntu2~22.04.1 deb  -openssh-client 1:8.9p1-3ubuntu0.7 deb  -openssl 3.0.2-0ubuntu1.15 deb  -openssl 3.2.1 binary  -ordered-set 4.1.0 python  -packaging 24.0 python (+1 duplicate) -pandas 1.5.3 python  -partd 1.4.1 python  -passwd 1:4.8.1-2ubuntu2.2 deb  -patch 2.7.6-7build2 deb  -perl 5.34.0-3ubuntu1.3 deb  -perl-base 5.34.0-3ubuntu1.3 deb  -perl-modules-5.34 5.34.0-3ubuntu1.3 deb  -pillow 10.3.0 python  -pinentry-curses 1.1.1-1build2 deb  -pip 24.0 python  -pkg-config 0.29.2-1ubuntu3 deb  -pkginfo 1.10.0 python  -pkgutil_resolve_name 1.3.10 python  -platformdirs 4.2.0 python  -platformdirs 4.2.1 python  -pluggy 1.3.0 python  -pluggy 1.4.0 python  -procps 2:3.3.17-6ubuntu2.1 deb  -prometheus-flask-exporter 0.23.0 python  -prometheus_client 0.20.0 python  -prompt-toolkit 3.0.42 python  -protobuf 4.24.4 python  -psutil 5.9.8 python (+1 duplicate) -py4j 0.10.9.7 python  -py4j0 10.9.7 java-archive  -pyOpenSSL 24.0.0 python  -pyarrow 14.0.2 python  -pyarrow-hotfix 0.6 python  -pyasn1 0.6.0 python  -pyasn1_modules 0.4.0 python  -pycosat 0.6.6 python  -pycparser 2.22 python (+1 duplicate) -pycryptodome 3.20.0 python  -pydantic 2.7.1 python  -pydantic_core 2.18.2 python  -pymilvus 2.3.6 python  -pynvjitlink 0.2.1 python  -pynvml 11.4.1 python  -pyparsing 3.1.2 python  -python 3.10.14 binary  -python-dateutil 2.9.0 python  -python-dotenv 1.0.1 python  -python-rapidjson 1.16 python  -pytz 2024.1 python (+1 duplicate) -qpd 0.4.4 python  -querystring-parser 1.2.4 python  -readline-common 8.1.2-1 deb  -referencing 0.34.0 python  -requests 2.31.0 python (+1 duplicate) -requests-cache 1.1.1 python  -requests-toolbelt 1.0.0 python  -rich 13.7.1 python (+1 duplicate) -rmm 24.2.0 python  -rpcsvc-proto 1.4.2-0ubuntu6 deb  -rpds-py 0.18.0 python  -rsa 4.9 python  -ruamel.yaml 0.18.6 python  -ruamel.yaml.clib 0.2.8 python  -scikit-learn 1.3.2 python  -scipy 1.13.0 python  -sed 4.8-1ubuntu2 deb  -sensible-utils 0.0.17 deb  -setuptools 69.5.1 python (+1 duplicate) -sgmllib3k 1.0.0 python  -six 1.16.0 python  -smmap 5.0.0 python  -sniffio 1.3.1 python  -snowballstemmer 2.2.0 python  -sortedcontainers 2.4.0 python  -soupsieve 2.5 python  -sphinxcontrib-applehelp 1.0.8 python  -sphinxcontrib-devhelp 1.0.6 python  -sphinxcontrib-htmlhelp 2.0.5 python  -sphinxcontrib-jsmath 1.0.1 python  -sphinxcontrib-qthelp 1.0.7 python  -sphinxcontrib-serializinghtml 1.1.10 python  -sqlglot 23.11.2 python  -sqlparse 0.4.4 python  -stringcase 1.2.0 python  -sympy 1.12 python  -sysvinit-utils 3.01-1ubuntu1 deb  -tabulate 0.9.0 python  -tar 1.34+dfsg-1ubuntu0.1.22.04.2 deb  -tblib 3.0.0 python  -tensorflow-metadata 1.13.1 python  -threadpoolctl 3.4.0 python  -tomli 2.0.1 python (+1 duplicate) -toolz 0.12.1 python  -torch 2.3.0 python  -tornado 6.4 python  -tqdm 4.66.2 python (+1 duplicate) -triad 0.9.6 python  -triton 2.3.0 python  -tritonclient 2.34.0 python  -truststore 0.8.0 python  -typing-utils 0.1.0 python  -typing_extensions 4.11.0 python (+1 duplicate) -ubuntu-keyring 2021.03.26 deb  -ucf 3.0043 deb  -ujson 5.9.0 python  -unicodedata2 15.1.0 python  -unzip 6.0-26ubuntu3.2 deb  -url-normalize 1.4.3 python  -urllib3 2.2.1 python (+1 duplicate) -usrmerge 25ubuntu2 deb  -util-linux 2.37.2-4ubuntu3.4 deb  -watchdog 3.0.0 python  -watchgod 0.8.2 python  -wcwidth 0.2.13 python  -websocket-client 1.8.0 python  -websockets 12.0 python  -wget 1.21.2-2ubuntu1 deb  -wheel 0.43.0 python (+1 duplicate) -xyzservices 2024.4.0 python  -xz-utils 5.2.5-2ubuntu1 deb  -zict 3.0.0 python  -zip 3.0-12build2 deb  -zipp 3.17.0 python (+1 duplicate) -zlib1g 1:1.2.11.dfsg-2ubuntu9.2 deb  -zstandard 0.22.0 python  diff --git a/src/vuln_analysis/data_models/__init__.py b/src/vuln_analysis/data_models/__init__.py index cf7c586a5..e69de29bb 100644 --- a/src/vuln_analysis/data_models/__init__.py +++ b/src/vuln_analysis/data_models/__init__.py @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/src/vuln_analysis/data_models/common.py b/src/vuln_analysis/data_models/common.py deleted file mode 100644 index 39455cf77..000000000 --- a/src/vuln_analysis/data_models/common.py +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -import typing -from hashlib import sha512 - -from pydantic import BaseModel -from pydantic import Field - -_LT = typing.TypeVar("_LT") - - -class HashableModel(BaseModel): - """ - Subclass of a Pydantic BaseModel that is hashable. Use in objects that need to be hashed for caching purposes. - """ - - def __hash__(self): - return int.from_bytes(bytes=sha512(f"{self.__class__.__qualname__}::{self.model_dump_json()}".encode( - 'utf-8', errors='ignore')).digest(), - byteorder=sys.byteorder) - - def __lt__(self, other): - return self.__hash__() < other.__hash__() - - def __eq__(self, other): - return self.__hash__() == other.__hash__() - - def __ne__(self, other): - return self.__hash__() != other.__hash__() - - def __gt__(self, other): - return self.__hash__() > other.__hash__() - - -class TypedBaseModel(BaseModel, typing.Generic[_LT]): - """ - Subclass of Pydantic BaseModel that allows for specifying the object type. Use in Pydantic discriminated unions. - """ - - @classmethod - def __pydantic_init_subclass__(cls, **kwargs): - super().__pydantic_init_subclass__(**kwargs) - - if (cls.__pydantic_generic_metadata__["origin"] is TypedBaseModel): - # Because we are using a generic type, we need to set the default value of the type field when instantiating - # a base class. This is the only way to get the value from a typing.Literal - cls.model_fields["type"].default = typing.get_args(cls.__pydantic_generic_metadata__["args"][0])[0] - - type: _LT = Field(init=False, alias="_type", description="The type of the object", title="Type") - - @classmethod - def static_type(cls): - return cls.model_fields["type"].default - - @staticmethod - def discriminator(v: typing.Any) -> str | None: - # If its serialized, then we use the alias - if isinstance(v, dict): - return v.get("_type", v.get("type")) - - # Otherwise we use the property - return getattr(v, "type") diff --git a/src/vuln_analysis/data_models/container_analyzer.py b/src/vuln_analysis/data_models/container_analyzer.py new file mode 100644 index 000000000..1508cbafd --- /dev/null +++ b/src/vuln_analysis/data_models/container_analyzer.py @@ -0,0 +1,42 @@ +from __future__ import annotations +from pydantic import BaseModel, model_validator + +from vuln_analysis.data_models.output import AgentResultBase + + +class ContainerAnalyzerInput(BaseModel): + image_name: str + image_tag: str + filesystem_path: str + image_config_path: str | None = None + skip_web_research: bool = False + + @model_validator(mode="after") + def _check_required(self): + if not self.image_name: + raise ValueError("image_name must be provided.") + if not self.image_tag: + raise ValueError("image_tag must be provided.") + if not self.filesystem_path: + raise ValueError("filesystem_path must be provided.") + return self + + @property + def pull_ref(self) -> str: + return compose_pull_ref(self.image_name, self.image_tag) + + +class ContainerAnalyzerResult(AgentResultBase): + image_name: str + image_tag: str + report_path: str | None = None + phase1_skipped: bool = False + + +def compose_pull_ref(image_name: str, image_tag: str) -> str: + return f"{image_name}:{image_tag}" + + +def derive_report_filename(image_name: str, image_tag: str) -> str: + base = image_name.strip().rsplit("/", 1)[-1] + return f"{base}_{image_tag}.md" diff --git a/src/vuln_analysis/data_models/cve_intel.py b/src/vuln_analysis/data_models/cve_intel.py deleted file mode 100644 index 8efe7ffa3..000000000 --- a/src/vuln_analysis/data_models/cve_intel.py +++ /dev/null @@ -1,333 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import functools -import typing - -from pydantic import BaseModel -from pydantic import ConfigDict -from pydantic import Field -from pydantic import computed_field - - -def rgetattr(obj, attr, *args): - """ - Splits attributes on '.' and recursively retrieves the attributes from the given obj. - """ - - def _getattr(obj, attr): - return getattr(obj, attr, *args) - - return functools.reduce(_getattr, [obj] + attr.split('.')) - - -class IntelSource(BaseModel, abc.ABC): - - @property - @abc.abstractmethod - def description_fields(self) -> list: - """ - Return the list of fields that contain CVE descriptions that the agent can utilize in it's analysis. - """ - - @property - def agent_sufficient(self) -> bool: - """ - Logic to determine whether the IntelSource contains sufficient info for the agent. - """ - # Return True if at least one description field is populated. - desc_fields = self.description_fields - for field in desc_fields: - if rgetattr(self, field) is not None: - return True - return False - - -class CveIntelGhsa(IntelSource): - """ - Information about a GHSA (GitHub Security Advisory) entry. - """ - model_config = ConfigDict(extra="allow") - - class CVSS(BaseModel): - score: float | None = None - vector_string: str | None = None - - class CWE(BaseModel): - cwe_id: str - name: str | None = None - - ghsa_id: str - cve_id: str | None = None - summary: str | None = None - description: str | None = None - severity: str | None = None - vulnerabilities: list | None = None - cvss: CVSS | None = None - cwes: list[CWE] | None = None - published_at: str | None = None - updated_at: str | None = None - - @property - def description_fields(self): - return ['summary', 'description'] - - -class CveIntelNvd(IntelSource): - """ - Information about an NVD (National Vulnerability Database) entry. - """ - - model_config = ConfigDict(extra="allow") - - class Configuration(BaseModel): - package: str - vendor: str | None = None - system: str | None = None - versionStartExcluding: str | None = None - versionEndExcluding: str | None = None - versionStartIncluding: str | None = None - versionEndIncluding: str | None = None - - cve_id: str - cve_description: str | None = None - cvss_vector: str | None = None - cvss_base_score: float | None = None - cvss_severity: str | None = None - cwe_name: str | None = None - cwe_description: str | None = None - cwe_extended_description: str | None = None - configurations: list[Configuration] | None = None - vendor_names: list[str] | None = None - references: list[str] | None = None - disputed: bool | None = None - published_at: str | None = None - updated_at: str | None = None - - @property - def description_fields(self): - return ['cve_description', 'cwe_description', 'cwe_extended_description'] - - -class CveIntelRhsa(IntelSource): - """ - Information about a RHSA (Red Hat Security Advisory) entry. - """ - model_config = ConfigDict(extra="allow") - - class Bugzilla(BaseModel): - description: str | None = None - id: str | None = None - url: str | None = None - - class PackageState(BaseModel): - product_name: str | None = None - fix_state: str | None = None - package_name: str | None = None - cpe: str | None = None - - class CVSS3(BaseModel): - cvss3_base_score: float | None = None - cvss3_scoring_vector: str | None = None - status: str | None = None - - bugzilla: typing.Annotated[Bugzilla, Field(default_factory=Bugzilla)] - details: list[str] | None = None - statement: str | None = None - package_state: list[PackageState] | None = None - upstream_fix: str | None = None - cvss3: CVSS3 | None = None - - @property - def description_fields(self): - return ['bugzilla.description'] - - -class CveIntelUbuntu(IntelSource): - """ - Information about a Ubuntu CVE entry. - """ - model_config = ConfigDict(extra="allow") - - class Note(BaseModel): - author: str | None = None - note: str | None = None - - class CVSSV3(BaseModel): - attackComplexity: str - attackVector: str - availabilityImpact: str - baseScore: float - baseSeverity: str - confidentialityImpact: str - integrityImpact: str - privilegesRequired: str - scope: str - userInteraction: str - vectorString: str - version: str - - class BaseMetricV3(BaseModel): - cvssV3: "CVSSV3" - exploitabilityScore: float | None = None - impactScore: float | None = None - - class Impact(BaseModel): - baseMetricV3: "BaseMetricV3" - - description: str | None = None - notes: list[Note] | None = None - notices: list | None = None - priority: str | None = None - ubuntu_description: str | None = None - impact: Impact | None = None - - @property - def description_fields(self): - return ['description', 'ubuntu_description'] - - -class CveIntelEpss(IntelSource): - """ - Information about an EPSS (Elastic Product Security Service) entry. - """ - model_config = ConfigDict(extra="allow") - - epss: float | None = None - percentile: float | None = None - date: str | None = None - - @property - def description_fields(self): - return [] - - -class CveIntel(BaseModel): - """ - Information about a CVE (Common Vulnerabilities and Exposures) entry. - """ - - vuln_id: str - """ - The input indentifier. Can be either GHSA or CVE - """ - - ghsa: CveIntelGhsa | None = None - nvd: CveIntelNvd | None = None - rhsa: CveIntelRhsa | None = None - ubuntu: CveIntelUbuntu | None = None - epss: CveIntelEpss | None = None - - @computed_field() - @property - def has_sufficient_intel_for_agent(self) -> bool: - """ - Logic to determine if the CVE has sufficient intel and can be passed to the agent. - - Returns - ------- - bool - True if enough intel has been found for the CVE - """ - for field_name, _ in self.model_fields.items(): - if isinstance(getattr(self, field_name), IntelSource): - # Return True as long as any one intel source is agent sufficient - if getattr(self, field_name).agent_sufficient: - return True - return False - - @property - def cve_id(self): - """ - The CVE identifier. - - Returns - ------- - str - The CVE identifier. - - Raises - ------ - ValueError - If the CVE ID is not found. An exception will be raised - """ - cve_id = self.get_cve_id() - - if (cve_id is not None): - return cve_id - - raise ValueError("CVE ID not found") - - @property - def ghsa_id(self): - """ - The GHSA identifier. - """ - ghsa_id = self.get_ghsa_id() - - if (ghsa_id is not None): - return ghsa_id - - raise ValueError("GHSA ID not found") - - def has_cve_id(self): - """ - Check if the object has a CVE ID. - """ - return self.get_cve_id() is not None - - def has_ghsa_id(self): - """ - Check if the object has a GHSA ID. - """ - return self.get_ghsa_id() is not None - - def get_cve_id(self): - """ - Get the CVE ID. - - Returns - ------- - str | None - The CVE ID or None if not found. - """ - if (self.nvd is not None): - return self.nvd.cve_id - - if (self.ghsa is not None and self.ghsa.cve_id is not None): - return self.ghsa.cve_id - - if (self.vuln_id.startswith("CVE-")): - return self.vuln_id - - return None - - def get_ghsa_id(self): - """ - Get the GHSA ID. - - Returns - ------- - str | None - The GHSA ID or None if not found. - """ - if (self.ghsa is not None): - return self.ghsa.ghsa_id - - if (self.vuln_id.startswith("GHSA-")): - return self.vuln_id - - return None diff --git a/src/vuln_analysis/data_models/cve_researcher.py b/src/vuln_analysis/data_models/cve_researcher.py new file mode 100644 index 000000000..78ef7e628 --- /dev/null +++ b/src/vuln_analysis/data_models/cve_researcher.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import model_validator + +from vuln_analysis.data_models.output import AgentResultBase + + +class CVEResearcherInput(BaseModel): + model_config = ConfigDict(title="CVEResearcherInput") + + cve_id: str | None = None + ghsa_id: str | None = None + package_hint: str | None = None + version_hint: str | None = None + + @model_validator(mode="after") + def _check_ids(self): + if not self.cve_id and not self.ghsa_id: + raise ValueError("At least one CVE or GHSA identifier must be provided.") + return self + + @property + def identifier(self) -> str: + return self.cve_id or self.ghsa_id + + +class CVEResearcherResult(AgentResultBase): + model_config = ConfigDict(title="CVEResearcherResult") + + identifier: str + report_path: str | None = None + + +__all__ = [ + "CVEResearcherInput", + "CVEResearcherResult", +] diff --git a/src/vuln_analysis/data_models/dependencies.py b/src/vuln_analysis/data_models/dependencies.py deleted file mode 100644 index 1af1325f0..000000000 --- a/src/vuln_analysis/data_models/dependencies.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing - -from pydantic import BaseModel - - -class DependencyPackage(BaseModel): - """ - Information about a dependency package as obtained from deps.dev API for a given SBOM package. - """ - system: str | None = None - name: str | None = None - version: str | None = None - relation: typing.Literal["SELF", "DIRECT", "INDIRECT"] | None = None - - -class VulnerableSBOMPackage(BaseModel): - """ - Information about a vulnerable SBOM package and its related vulnerable dependency package. - - - name: SBOM package name - - version: SBOM package version - - vulnerable_dependency_package: DependencyPackage object with info about the vulnerable dependency package. - If an SBOM package itself is vulnerable, the vulnerable_dependency_package.relation will be "SELF". - Otherwise, if it is vulnerable due to its dependency, the vulnerable_dependency_package.relation will be either - "DIRECT" or "INDIRECT". - """ - name: str - version: str - vulnerable_dependency_package: DependencyPackage - - -class VulnerableDependencies(BaseModel): - """ - Information about the vulnerable SBOM packages associated with the vuln_id. - - - vuln_id: vulnerability ID (e.g. CVE ID, GHSA ID) associated with the vulnerable package list. - - vuln_package_intel_sources: list of sources (e.g. "ghsa", "nvd", "ubuntu", "rhsa") that provided - the vulnerable package/version intel for the vuln_id. - - vulnerable_sbom_packages: list of VulnerableSBOMPackage objects, representing the SBOM packages that are - vulnerable for a given vuln_id. - """ - vuln_id: str - vuln_package_intel_sources: list[str] - vulnerable_sbom_packages: list[VulnerableSBOMPackage] diff --git a/src/vuln_analysis/data_models/eval_input.py b/src/vuln_analysis/data_models/eval_input.py deleted file mode 100644 index 9954a5f70..000000000 --- a/src/vuln_analysis/data_models/eval_input.py +++ /dev/null @@ -1,89 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Literal - -from pydantic import BaseModel -from pydantic import Field -from pydantic import field_validator -from pydantic import model_validator - -from vuln_analysis.data_models.input import ImageInfoInput -from vuln_analysis.utils.justification_parser import JustificationParser -from vuln_analysis.utils.string_utils import is_valid_cve_id -from vuln_analysis.utils.string_utils import is_valid_ghsa_id - -# Use label/status mappings from JustificationParser -ALLOWED_LABELS: tuple[str, ...] = tuple(JustificationParser.JUSTIFICATION_TO_AFFECTED_STATUS_MAP.keys()) - -# Map justification status (TRUE/FALSE/UNKNOWN) to eval dataset statuses (AFFECTED/NOT AFFECTED/UNKNOWN) -_JUST_STATUS_TO_EVAL_STATUS: dict[str, str] = { - "TRUE": "AFFECTED", - "FALSE": "NOT AFFECTED", - "UNKNOWN": "UNKNOWN", -} - -ALLOWED_STATUSES: tuple[str, ...] = tuple(_JUST_STATUS_TO_EVAL_STATUS.values()) - - -class GroundTruthItem(BaseModel): - vuln_id: str - status: str - label: str - - @field_validator("vuln_id") - @classmethod - def validate_vuln_id(cls, v: str) -> str: - if not (is_valid_cve_id(v) or is_valid_ghsa_id(v)): - raise ValueError(f"{v} is not a valid CVE ID or GHSA ID.") - return v - - @field_validator("label") - @classmethod - def validate_label(cls, v: str) -> str: - if v not in ALLOWED_LABELS: - raise ValueError(f"{v} is not a valid justification label. Allowed: {sorted(ALLOWED_LABELS)}") - return v - - @field_validator("status") - @classmethod - def validate_status(cls, v: str) -> str: - if v not in ALLOWED_STATUSES: - raise ValueError(f"{v} is not a valid status. Allowed: {sorted(ALLOWED_STATUSES)}") - return v - - @model_validator(mode="after") - def validate_label_status_consistency(self): - # Expected status from label via mapping from JustificationParser - expected_status = JustificationParser.JUSTIFICATION_TO_AFFECTED_STATUS_MAP[self.label] - expected_status_transformed = _JUST_STATUS_TO_EVAL_STATUS[expected_status] - - if self.status != expected_status_transformed: - raise ValueError( - f"Label/status mismatch: label '{self.label}' implies '{expected_status_transformed}' but got '{self.status}'." - ) - - return self - - -class AgentMorpheusEvalInputItem(BaseModel): - container_image: ImageInfoInput - ground_truth: list[GroundTruthItem] - - -class AgentMorpheusEvalDataset(BaseModel): - dataset_id: str - dataset_description: str | None = None - containers: dict[str, AgentMorpheusEvalInputItem] diff --git a/src/vuln_analysis/data_models/exploitability_analyzer.py b/src/vuln_analysis/data_models/exploitability_analyzer.py new file mode 100644 index 000000000..16a8d43ab --- /dev/null +++ b/src/vuln_analysis/data_models/exploitability_analyzer.py @@ -0,0 +1,139 @@ +from __future__ import annotations +import re +from pathlib import Path +from pydantic import BaseModel, Field, model_validator + +from vuln_analysis.data_models.output import AgentResultBase + + +class ExploitabilityAnalyzerInput(BaseModel): + vulnerability_report_path: str + container_report_path: str + filesystem_path: str + + @model_validator(mode="after") + def _check_required(self): + if not self.vulnerability_report_path: + raise ValueError("vulnerability_report_path must be provided.") + if not self.container_report_path: + raise ValueError("container_report_path must be provided.") + if not self.filesystem_path: + raise ValueError("filesystem_path must be provided.") + return self + + +class ExploitabilityAnalyzerResult(AgentResultBase): + cve_id: str + container_name: str + report_path: str | None = None + verdict: str | None = None + confidence: str | None = None + + +class AffectedRange(BaseModel): + branch: str + lower: str + upper: str + first_fixed: str + + +class AffectedPackageEntry(BaseModel): + """A single package affected by a CVE (from the Affected Packages table). + + A CVE may affect multiple packages -- e.g., CVE-2025-52881 affects both + ``github.com/opencontainers/runc`` and ``github.com/opencontainers/selinux``. + """ + package: str + ecosystem: str | None + vulnerable_versions: str + fixed_in: str + + +class ParsedVulnReport(BaseModel): + cve_id: str + package_name: str + package_raw: str + ecosystem_hint: str | None + vulnerable_versions: str + fixed_in: str + cvss_score: float | None + cvss_severity: str | None + cvss_vector: str | None + cwe: str | None + vulnerable_function: str | None + affected_ranges: list[AffectedRange] = Field(default_factory=list) + affected_packages: list[AffectedPackageEntry] = Field(default_factory=list) + + +class ParsedContainerReport(BaseModel): + image_name: str + image_tag: str | None = None + container_type: str + primary_language: str + base_os: str + default_environment: str | None + runtime_user: str + entry_points: list[str] = Field(default_factory=list) + + +class FilesystemLayout(BaseModel): + conda_envs: list[str] = Field(default_factory=list) + site_packages_paths: list[str] = Field(default_factory=list) + app_code_dirs: list[str] = Field(default_factory=list) + entrypoint_scripts: list[str] = Field(default_factory=list) + key_binaries: list[str] = Field(default_factory=list) + language_ecosystem_paths: dict[str, list[str]] = Field(default_factory=dict) + + +class PackageInstallation(BaseModel): + version: str + location: str + source: str + environment: str | None = None + + +class PackageVerification(BaseModel): + found: bool + installations: list[PackageInstallation] = Field(default_factory=list) + active_env_installation: PackageInstallation | None = None + is_vulnerable: bool | None = None + backport_status: str | None = None + backport_evidence: str | None = None + searched_names: list[str] = Field(default_factory=list) + notes: list[str] = Field(default_factory=list) + + +class AdditionalPackageResult(BaseModel): + """Verification result for an additional affected package beyond the primary.""" + entry: AffectedPackageEntry + verification: PackageVerification + + +class PreTriageResult(BaseModel): + vuln_report: ParsedVulnReport + container_report: ParsedContainerReport + filesystem_layout: FilesystemLayout + package_verification: PackageVerification + early_exit: bool + early_exit_verdict: str | None = None + early_exit_reason: str | None = None + package_not_found_note: str | None = None + additional_package_verifications: list[AdditionalPackageResult] = Field( + default_factory=list, + ) + reverse_dependencies: list[str] = Field(default_factory=list) + llm_call_count: int = 0 + + +def derive_report_filename(cve_id: str, container_name: str) -> str: + return f"{cve_id}_{container_name}.md" + + +def extract_cve_id_from_path(vulnerability_report_path: str) -> str: + stem = Path(vulnerability_report_path).stem + match = re.match(r"(CVE-\d{4}-\d+)", stem) + return match.group(1) if match else stem + + +def extract_container_name_from_path(container_report_path: str) -> str: + return Path(container_report_path).stem diff --git a/src/vuln_analysis/data_models/info.py b/src/vuln_analysis/data_models/info.py deleted file mode 100644 index e94094350..000000000 --- a/src/vuln_analysis/data_models/info.py +++ /dev/null @@ -1,64 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pydantic import BaseModel - -from .cve_intel import CveIntel -from .dependencies import VulnerableDependencies - - -class SBOMPackage(BaseModel): - """ - Information about a single package in the container image's Software Bill of Materials (SBOM). - """ - name: str - version: str - path: str | None = None - system: str - - -class AgentMorpheusInfo(BaseModel): - """ - Information used for decisioning in the Agent Morpheus engine. These information can all be automatically - generated or retrieved by the pipeline from the input information. - - - vdb: paths to source code and documentation vector databases (VDBs) used to understand whether a vulnerability - is exploitable in the source code. - - intel: list of CveIntel objects representing intelligence for each vulnerability pulled from various vulnerability - databases and APIs. - - sbom: software bill of materials listing the packages and versions in the container image, used to understand - whether the vulnerable package exists in the image. - - vulnerable_dependencies: a list of VulnerableDependencies objects for each vuln_id, representing the SBOM packages - and transitive dependencies that are vulnerable for the vuln_id. - """ - - class VdbPaths(BaseModel): - """ - Paths to where the generated VDBs are stored. - """ - code_vdb_path: str | None = None - doc_vdb_path: str | None = None - code_index_path: str | None = None - - class SBOMInfo(BaseModel): - """ - List of SBOMPackage objects representing the packages found in the input image. - """ - packages: list[SBOMPackage] - - vdb: VdbPaths | None = None - intel: list[CveIntel] | None = None - sbom: SBOMInfo | None = None - vulnerable_dependencies: list[VulnerableDependencies] | None = None diff --git a/src/vuln_analysis/data_models/input.py b/src/vuln_analysis/data_models/input.py index c079ca295..abd054cc2 100644 --- a/src/vuln_analysis/data_models/input.py +++ b/src/vuln_analysis/data_models/input.py @@ -1,188 +1,93 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing -from collections import defaultdict -from uuid import uuid4 - -from pydantic import AliasChoices -from pydantic import AnyHttpUrl -from pydantic import BaseModel -from pydantic import ConfigDict -from pydantic import Discriminator -from pydantic import Field -from pydantic import Tag -from pydantic import field_validator - -from ..utils.string_utils import is_valid_cve_id -from ..utils.string_utils import is_valid_ghsa_id -from .common import HashableModel -from .common import TypedBaseModel -from .info import AgentMorpheusInfo -from .info import SBOMPackage - - -class SourceDocumentsInfo(HashableModel): - """ - Information about the source documents for the container image. - - - type: document type. - - git_repo: git repo URL where the source documents can be cloned. - - ref: git reference, such as tag/branch/commit_id - - include: file extensions to include when indexing the source documents. - - exclude: file extensions to exclude when indexing the source documents. - """ - - type: typing.Literal["code", "doc"] - - git_repo: typing.Annotated[str, Field(min_length=1)] - ref: typing.Annotated[str, Field(min_length=1, validation_alias=AliasChoices( - "ref", "tag"))] # Support "tag" as alias for backward compatibility - - include: list[str] = ["*.py", "*.ipynb"] - exclude: list[str] = [] - - @field_validator("include", "exclude") - @classmethod - def sort_lists(cls, v: list[str]) -> list[str]: - return list(sorted(v)) - - -class VulnInfo(HashableModel): - """ - Information about a vulnerability. - """ - - model_config = ConfigDict(extra="allow") +from __future__ import annotations + +import os + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +def _cache_default_from_env() -> bool: + """Match standalone pipeline: CACHE_ENABLED=false disables on-disk report caching.""" + raw = os.environ.get("CACHE_ENABLED") + if raw is None: + return True + return str(raw).strip().lower() not in ("0", "false", "no", "off") + + +class CVEEntry(BaseModel): + """A single vulnerability identifier with optional hints for the pipeline.""" + model_config = ConfigDict(extra="forbid") vuln_id: str - description: str | None = None - score: float | None = None - severity: str | None = None - published_date: str | None = None - last_modified_date: str | None = None - url: str | None = None - feed_group: str | None = None - - package: str | None = None - """ - The full package name, including the version, that is affected by the vulnerability. - """ - package_version: str | None = None - """ - The version of the package that is affected by the vulnerability. - """ - package_name: str | None = None - """ - The name of the package that is affected by the vulnerability without the version. - """ - package_type: str | None = None - """ - The type of the package that is affected by the vulnerability. - """ - - @field_validator("vuln_id") - @classmethod - def is_valid_vuln_id(cls, v: str): - if not (is_valid_cve_id(v) or is_valid_ghsa_id(v)): - raise ValueError(f"{v} is not a valid CVE ID or GHSA ID.") - else: - return v - - -class ScanInfoInput(HashableModel): - """ - Information about a unique scan for a container image against a list of vulnerabilies. - """ - id: str = Field(default_factory=lambda: str(uuid4())) - type: str | None = None - started_at: str | None = None - completed_at: str | None = None - - vulns: typing.Annotated[list[VulnInfo], Field(min_length=1)] - - -class ManualSBOMInfoInput(TypedBaseModel[typing.Literal["manual"]]): - """ - Manually provided Software Bill of Materials, consisting of a list of SBOMPackage objects. - """ - packages: list[SBOMPackage] - - -class FileSBOMInfoInput(TypedBaseModel[typing.Literal["file"]]): - """ - A file path pointing to a Software Bill of Materials file. - """ - file_path: str - - -class HTTPSBOMInfoInput(TypedBaseModel[typing.Literal["http"]]): - """ - A URL pointing to a Software Bill of Materials file. - """ - url: AnyHttpUrl - - -SBOMInfoInput = typing.Annotated[typing.Annotated[ManualSBOMInfoInput, Tag(ManualSBOMInfoInput.static_type())] - | typing.Annotated[FileSBOMInfoInput, Tag(FileSBOMInfoInput.static_type())] - | typing.Annotated[HTTPSBOMInfoInput, Tag(HTTPSBOMInfoInput.static_type())], - Discriminator(TypedBaseModel.discriminator)] - - -class ImageInfoInput(HashableModel): - """ - Information about a container image, including the source information and sbom information. - """ - name: str | None = None # The image name - tag: str | None = None # i.e. latest - digest: str | None = None # i.e. sha256:... - platform: str | None = None # i.e. linux/amd64 - feed_group: str | None = None # i.e. ubuntu:22.04 - - source_info: list[SourceDocumentsInfo] - sbom_info: SBOMInfoInput - - @field_validator('source_info', mode='after') - @classmethod - def check_conflicting_refs(cls, source_info: list[SourceDocumentsInfo]) -> list[SourceDocumentsInfo]: - """Check if any git repos have conflicting refs""" - repo_refset = defaultdict(set) - - for si in source_info: - repo_refset[si.git_repo].add(si.ref) - - for repo, refset in repo_refset.items(): - if len(refset) > 1: - raise ValueError(f"Conflicting refs specified for git repo: {repo}, refs: {refset}") - return source_info - - -class AgentMorpheusInput(HashableModel): - """ - Inputs required by the Agent Morpheus pipeline. - """ - scan: ScanInfoInput - image: ImageInfoInput - - -class AgentMorpheusEngineInput(BaseModel): - """ - Inputs required by the Agent Morpheus Engine. - - - input: AgentMorpheusInput object that must be provided to the pipeline. - - info: AgentMorpheusInfo object that is retrieved/generated by the pipeline. - """ - input: AgentMorpheusInput - info: AgentMorpheusInfo + package_hint: str | None = None + version_hint: str | None = None + + @property + def is_ghsa(self) -> bool: + return self.vuln_id.startswith("GHSA-") + + +class PipelineInput(BaseModel): + """Everything needed to run the full 4-agent vulnerability analysis pipeline.""" + model_config = ConfigDict(extra="forbid") + + vulns: list[CVEEntry] = Field(min_length=1) + image_name: str + image_tag: str + image_digest: str | None = None + filesystem_path: str = "" + image_config_path: str | None = None + skip_web_research: bool = False + output_dir: str | None = None + model: str | None = None + cache: bool = Field(default_factory=_cache_default_from_env) + + cache_cve_researcher: bool | None = None + cache_container_analyzer: bool | None = None + cache_exploitability_analyzer: bool | None = None + cache_vex_categorizer: bool | None = None + + @model_validator(mode="after") + def _check_input(self): + seen: set[str] = set() + for entry in self.vulns: + if entry.vuln_id in seen: + raise ValueError(f"Duplicate vuln_id: {entry.vuln_id}") + seen.add(entry.vuln_id) + + if not self.image_name: + raise ValueError("image_name must be provided.") + if not self.image_tag: + raise ValueError("image_tag must be provided.") + if _final_path_component_has_tag(self.image_name): + raise ValueError("image_name must not include a tag when image_tag is provided.") + return self + + _AGENT_CACHE_FIELDS = { + "cve_researcher": "cache_cve_researcher", + "container_analyzer": "cache_container_analyzer", + "exploitability_analyzer": "cache_exploitability_analyzer", + "vex_categorizer": "cache_vex_categorizer", + } + + def use_cache_for(self, agent: str) -> bool: + """Resolve the effective cache setting for *agent*. + + Per-agent overrides take precedence when set. If the global + ``cache`` flag is ``False`` it always wins (disables everything). + """ + if not self.cache: + return False + field_name = self._AGENT_CACHE_FIELDS.get(agent) + if field_name is None: + raise ValueError(f"Unknown agent: {agent!r}") + override = getattr(self, field_name) + return override if override is not None else self.cache + + @property + def identifier(self) -> str: + return self.vulns[0].vuln_id + + +def _final_path_component_has_tag(image_name: str) -> bool: + """Return True when the repository name already contains a tag suffix.""" + return ":" in image_name.rsplit("/", 1)[-1] diff --git a/src/vuln_analysis/data_models/output.py b/src/vuln_analysis/data_models/output.py index 7d8481329..d54ae0de1 100644 --- a/src/vuln_analysis/data_models/output.py +++ b/src/vuln_analysis/data_models/output.py @@ -1,88 +1,82 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +from __future__ import annotations +from pydantic import BaseModel, ConfigDict, Field, model_validator -import typing -from pydantic import BaseModel -from pydantic import model_validator +class AgentResultBase(BaseModel): + """Common fields shared by all agent result types.""" + success: bool = True + error: str | None = None + report_content: str | None = None + tool_call_count: int = 0 + llm_call_count: int = 0 -from .input import AgentMorpheusEngineInput +class StageResult(BaseModel): + """Outcome of a single agent stage.""" + agent_name: str + success: bool + report_path: str | None = None + error: str | None = None + model: str | None = None + tool_call_count: int = 0 + llm_call_count: int = 0 + extra: dict = Field(default_factory=dict) -class AgentIntermediateStep(BaseModel): - """ - Represents info for an intermediate step taken by an agent. - """ - tool_name: str - action_log: str - tool_input: str | dict - tool_output: typing.Any - -class ChecklistItemOutput(BaseModel): - """ - Input, response, and intermediate steps for a single checklist item provided to the LLM agent. - """ - input: str - response: str - intermediate_steps: list[AgentIntermediateStep] | None = None - - -class JustificationOutput(BaseModel): - """ - Final justification for the vulnerability. - - - label: a categorical justification label classifying the status of an image against a given vulnerability, e.g. - code_not_present, code_not_reachable, false_positive. - - reason: a human-readable explanation for why justification label was selected. - - status: a ternary status (TRUE, FALSE, OR UNKNOWN) that indicates whether the image can be exploited for a given - vulnerability. Determined based on a mapping from the justification label. - """ - label: str # TODO: convert to typing.Literal after completing issue #115 - reason: str - status: typing.Literal["TRUE", "FALSE", "UNKNOWN"] - - -class AgentMorpheusEngineOutput(BaseModel): - """ - Contains all output generated by the main Agent Morpheus LLM Engine for a given vulnerability. - - - vuln_id: the ID of the vulnerability being processed by the LLM engine. - - checklist: a list of ChecklistItemOutput objects, each containing an input and a response from the LLM agent. - - summary: a short summary of the checklist inputs and responses, generated by an LLM. - - justification: a JustificationOutput object containing details of the model's justification decision. - """ +class CVEResult(BaseModel): + """Result for a single vulnerability within a multi-vulnerability pipeline run.""" vuln_id: str - checklist: list[ChecklistItemOutput] - summary: str - justification: JustificationOutput - - -class AgentMorpheusOutput(AgentMorpheusEngineInput): - """" - The final output of the Agent Morpheus pipeline. - Contains all fields in the AgentMorpheusEngineInput, plus the AgentMorpheusEngineOuput for each input vulnerability. - """ - output: list[AgentMorpheusEngineOutput] - - @model_validator(mode="before") - @classmethod - def validate_not_none(cls, data: typing.Any) -> typing.Any: - """Validate that no fields are None in the output data""" - if isinstance(data, dict): - for k, v in data.items(): - if v is None: - raise ValueError(f"{k} must not be None") - return data + cve_id: str | None = None + ghsa_id: str | None = None + success: bool = False + cve_researcher: StageResult | None = None + exploitability_analyzer: StageResult | None = None + vex_categorizer: StageResult | None = None + vex_status: str | None = None + vex_justification: str | None = None + error: str | None = None + + @model_validator(mode="after") + def _classify_ids(self): + if self.vuln_id.startswith("GHSA-"): + self.ghsa_id = self.vuln_id + else: + self.cve_id = self.vuln_id + return self + + @property + def total_tool_calls(self) -> int: + return sum( + (s.tool_call_count if s else 0) + for s in [self.cve_researcher, self.exploitability_analyzer, self.vex_categorizer] + ) + + @property + def total_llm_calls(self) -> int: + return sum( + (s.llm_call_count if s else 0) + for s in [self.cve_researcher, self.exploitability_analyzer, self.vex_categorizer] + ) + + +class PipelineResult(BaseModel): + """Composite result for the full 4-agent pipeline.""" + + model_config = ConfigDict(extra="forbid") + + image_name: str + image_tag: str + success: bool = False + container_analyzer: StageResult | None = None + elapsed_seconds: float = 0.0 + results: list[CVEResult] = Field(min_length=1) + + @property + def total_tool_calls(self) -> int: + container = self.container_analyzer.tool_call_count if self.container_analyzer else 0 + return container + sum(r.total_tool_calls for r in self.results) + + @property + def total_llm_calls(self) -> int: + container = self.container_analyzer.llm_call_count if self.container_analyzer else 0 + return container + sum(r.total_llm_calls for r in self.results) diff --git a/src/vuln_analysis/data_models/state.py b/src/vuln_analysis/data_models/state.py index 66973fc19..0659e2ac5 100644 --- a/src/vuln_analysis/data_models/state.py +++ b/src/vuln_analysis/data_models/state.py @@ -1,41 +1,14 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing -from enum import Enum - -from pydantic import BaseModel - -from vuln_analysis.data_models.cve_intel import CveIntel - - -class LLMEngineSkipReason(str, Enum): - """Reasons why a vulnerability might be skipped during LLM engine processing.""" - NO_SBOM_PACKAGES = "no_sbom_packages" - MISSING_SOURCE = "missing_source" - INSUFFICIENT_INTEL = "insufficient_intel" - NO_VULNERABLE_PACKAGES = "no_vulnerable_packages" - - -class AgentMorpheusEngineState(BaseModel): - code_vdb_path: str | None = None - doc_vdb_path: str | None = None - code_index_path: str | None = None - cve_intel: list[CveIntel] - checklist_plans: dict[str, list[str]] = {} - checklist_results: dict[str, list[dict[str, typing.Any]]] = {} - final_summaries: dict[str, str] = {} - justifications: dict[str, dict[str, str]] = {} - skip_reasons: dict[str, LLMEngineSkipReason | None] = {} +from typing import TypedDict +from vuln_analysis.data_models.output import StageResult + + +class PipelineState(TypedDict): + input: dict + vulns: list[dict] # CVE entries (from PipelineInput.vulns) + container_report: StageResult | None # single — CVE-agnostic + cve_reports: dict[str, StageResult] # keyed by CVE/GHSA identifier + exploit_reports: dict[str, StageResult] # keyed by CVE/GHSA identifier + vex_results: dict[str, StageResult] # keyed by CVE/GHSA identifier + vex_statuses: dict[str, str | None] # keyed by CVE/GHSA identifier + vex_justifications: dict[str, str | None] # keyed by CVE/GHSA identifier + success: bool diff --git a/src/vuln_analysis/data_models/vex_categorizer.py b/src/vuln_analysis/data_models/vex_categorizer.py new file mode 100644 index 000000000..e2f5060b9 --- /dev/null +++ b/src/vuln_analysis/data_models/vex_categorizer.py @@ -0,0 +1,37 @@ +from __future__ import annotations +import re +from pathlib import Path +from pydantic import BaseModel, model_validator + +from vuln_analysis.data_models.output import AgentResultBase + + +class VEXCategorizerInput(BaseModel): + exploitability_report_path: str + container_report_path: str + + @model_validator(mode="after") + def _check_required(self): + if not self.exploitability_report_path: + raise ValueError("exploitability_report_path must be provided.") + if not self.container_report_path: + raise ValueError("container_report_path must be provided.") + return self + + +class VEXCategorizerResult(AgentResultBase): + cve_id: str + container_name: str + status: str | None = None + justification: str | None = None + reasoning: str | None = None + summary_report_path: str | None = None + json_path: str | None = None + + +def parse_report_filename(exploitability_report_path: str) -> tuple[str, str]: + stem = Path(exploitability_report_path).stem + match = re.match(r"(CVE-\d{4}-\d+)_(.*)", stem) + if match: + return match.group(1), match.group(2) + return stem, "unknown" diff --git a/src/vuln_analysis/data_models/vdb_type.py b/src/vuln_analysis/eval/__init__.py similarity index 88% rename from src/vuln_analysis/data_models/vdb_type.py rename to src/vuln_analysis/eval/__init__.py index 8407e6941..cf7c586a5 100644 --- a/src/vuln_analysis/data_models/vdb_type.py +++ b/src/vuln_analysis/eval/__init__.py @@ -12,10 +12,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from enum import Enum - - -class VdbType(str, Enum): - CODE = "code" - DOC = "doc" diff --git a/src/vuln_analysis/utils/clients/__init__.py b/src/vuln_analysis/eval/evaluators/__init__.py similarity index 84% rename from src/vuln_analysis/utils/clients/__init__.py rename to src/vuln_analysis/eval/evaluators/__init__.py index a1744724e..cf7c586a5 100644 --- a/src/vuln_analysis/utils/clients/__init__.py +++ b/src/vuln_analysis/eval/evaluators/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/vuln_analysis/eval/evaluators/accuracy.py b/src/vuln_analysis/eval/evaluators/accuracy.py index b54d902d1..44ea081c9 100644 --- a/src/vuln_analysis/eval/evaluators/accuracy.py +++ b/src/vuln_analysis/eval/evaluators/accuracy.py @@ -13,228 +13,317 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""NAT custom evaluator for CVE pipeline benchmark accuracy. + +Computes label accuracy, binary accuracy, precision, recall, F1, and +confusion-matrix metrics. Each evaluator instance is configured with an +``uncertain_mode`` that determines how uncertain/fail-safe predictions are +treated: + +* ``exclude`` — drop uncertain predictions from the evaluation set +* ``wrong`` — count uncertain as always incorrect (assign opposite binary class) +* ``vulnerable`` — map uncertain to a ``"vulnerable"`` prediction +""" + +from __future__ import annotations + import asyncio import json import typing -from typing import Literal -import pandas as pd from nat.builder.builder import EvalBuilder from nat.builder.evaluator import EvaluatorInfo from nat.cli.register_workflow import register_evaluator from nat.data_models.evaluator import EvaluatorBaseConfig -from nat.eval.evaluator.evaluator_model import EvalInput -from nat.eval.evaluator.evaluator_model import EvalInputItem -from nat.eval.evaluator.evaluator_model import EvalOutput -from nat.eval.evaluator.evaluator_model import EvalOutputItem -from nat.eval.utils.tqdm_position_registry import TqdmPositionRegistry +from nat.data_models.evaluator import EvalInput +from nat.data_models.evaluator import EvalInputItem +from nat.data_models.evaluator import EvalOutputLike +from pydantic import BaseModel from pydantic import Field from tqdm import tqdm -_STATUS_MAP = {"TRUE": "AFFECTED", "FALSE": "NOT AFFECTED", "UNKNOWN": "UNKNOWN"} +try: + from nat.plugins.eval.utils.tqdm_position_registry import TqdmPositionRegistry +except ImportError: + TqdmPositionRegistry = None # type: ignore[assignment,misc] + + +class EvalOutputItem(BaseModel): + """Single accuracy-evaluator result item matching NAT's EvalOutputItemLike protocol.""" + + id: typing.Any + score: float + reasoning: dict[str, typing.Any] + error: str | None = None + + +class EvalOutput(BaseModel): + """Accuracy-evaluator output matching NAT's EvalOutputLike protocol.""" + + average_score: dict[str, typing.Any] + eval_output_items: list[EvalOutputItem] + + +# --------------------------------------------------------------------------- +# Binary mapping (ported from run_benchmark.py) +# --------------------------------------------------------------------------- + + +def _to_binary(label: str | None) -> str: + """Collapse any justification label into a binary classification.""" + return "vulnerable" if label == "vulnerable" else "not_vulnerable" + + +def _is_uncertain(justification: str | None) -> bool: + """True if the pipeline returned the unknown/uncertain fail-safe.""" + return justification == "uncertain" + + +# --------------------------------------------------------------------------- +# Metrics (ported from run_benchmark.py ``_compute_metrics``) +# --------------------------------------------------------------------------- + + +def _compute_metrics(predictions: list[dict], uncertain_mode: str) -> dict: + """Compute benchmark metrics under a given treatment of uncertain predictions. + + Returns a dict with label_accuracy, binary_accuracy, binary_precision, + binary_recall, binary_f1, confusion_matrix, and supporting counts. + """ + if uncertain_mode == "exclude": + preds = [p for p in predictions if not _is_uncertain(p.get("predicted_justification"))] + else: + preds = list(predictions) + + total = len(preds) + if total == 0: + return { + "total_evaluated": 0, + "label_correct": 0, + "binary_correct": 0, + "label_accuracy": 0.0, + "binary_accuracy": 0.0, + "binary_precision": 0.0, + "binary_recall": 0.0, + "binary_f1": 0.0, + "confusion_matrix": {"tp": 0, "fp": 0, "fn": 0, "tn": 0}, + } + + tp = fp = fn = tn = 0 + n_label = 0 + n_binary = 0 + + for p in preds: + gt = p["ground_truth_label"] + binary_gt = _to_binary(gt) + pred_just = p.get("predicted_justification") + + if _is_uncertain(pred_just): + if uncertain_mode == "wrong": + binary_pred = "not_vulnerable" if binary_gt == "vulnerable" else "vulnerable" + label_match = False + else: # "vulnerable" + binary_pred = "vulnerable" + label_match = ("vulnerable" == gt) + elif pred_just is None: + binary_pred = "not_vulnerable" + label_match = False + else: + binary_pred = _to_binary(pred_just) + label_match = pred_just == gt + + binary_match = binary_gt == binary_pred + if label_match: + n_label += 1 + if binary_match: + n_binary += 1 + + if binary_gt == "vulnerable" and binary_pred == "vulnerable": + tp += 1 + elif binary_gt == "not_vulnerable" and binary_pred == "vulnerable": + fp += 1 + elif binary_gt == "vulnerable": + fn += 1 + else: + tn += 1 + + label_accuracy = n_label / total + binary_accuracy = n_binary / total + precision = tp / (tp + fp) if (tp + fp) else 0.0 + recall = tp / (tp + fn) if (tp + fn) else 0.0 + f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0 + + return { + "total_evaluated": total, + "label_correct": n_label, + "binary_correct": n_binary, + "label_accuracy": round(label_accuracy, 4), + "binary_accuracy": round(binary_accuracy, 4), + "binary_precision": round(precision, 4), + "binary_recall": round(recall, 4), + "binary_f1": round(f1, 4), + "confusion_matrix": {"tp": tp, "fp": fp, "fn": fn, "tn": tn}, + } + + +# --------------------------------------------------------------------------- +# NAT evaluator registration +# --------------------------------------------------------------------------- class AccuracyEvaluatorConfig(EvaluatorBaseConfig, name="accuracy"): - """Configuration for unified accuracy evaluator""" - field: typing.Literal["status", "label"] = Field(default="status", description="The field to evaluate accuracy on.") - duplicates_policy: typing.Literal["drop_all", "keep_first", "keep_all"] = Field( - default="keep_first", description="Policy for handling duplicate CVEs in ground truth.") + """Configuration for the CVE pipeline accuracy evaluator.""" + uncertain_mode: typing.Literal["exclude", "wrong", "vulnerable"] = Field( + default="exclude", + description=( + "How to treat uncertain/fail-safe predictions: " + "'exclude' drops them, 'wrong' counts them as always incorrect, " + "'vulnerable' maps them to a vulnerable prediction." + ), + ) @register_evaluator(config_type=AccuracyEvaluatorConfig) async def accuracy_evaluator(config: AccuracyEvaluatorConfig, builder: EvalBuilder): - """Register unified accuracy evaluator""" - evaluator = AccuracyEvaluator(builder.get_max_concurrency(), - field=config.field, - duplicates_policy=config.duplicates_policy) - yield EvaluatorInfo(config=config, - evaluate_fn=evaluator.evaluate, - description=f"{config.field.title()} Accuracy Evaluator") + """Register the accuracy evaluator with NAT.""" + evaluator = AccuracyEvaluator( + max_concurrency=builder.get_max_concurrency(), + uncertain_mode=config.uncertain_mode, + ) + yield EvaluatorInfo( + config=config, + evaluate_fn=evaluator.evaluate, + description=f"Accuracy Evaluator (uncertain={config.uncertain_mode})", + ) class AccuracyEvaluator: - '''Configurable accuracy evaluator for either the status or label field.''' + """Evaluator that computes label accuracy, binary classification metrics, + and confusion matrix for the CVE pipeline benchmark.""" - def __init__(self, - max_concurrency: int, - field: str = "status", - duplicates_policy: Literal["drop_all", "keep_first", "keep_all"] = "keep_first"): + def __init__(self, max_concurrency: int, uncertain_mode: str = "exclude"): self.max_concurrency = max_concurrency - self.semaphore = asyncio.Semaphore(self.max_concurrency) - # field must be either "status" or "label" - self.field = field - # duplicates_policy: "drop_all" | "keep_first" | "keep_all" - self.duplicates_policy = duplicates_policy - - async def evaluate(self, eval_input: EvalInput) -> EvalOutput: - - def _preprocess_ground_truth(ground_truth_list, field: str, policy: str): - """Build a mapping of vuln_id -> acceptable answer set based on policy. - - - drop_all: only vuln_ids with a single unique answer are kept - - keep_first: take the first encountered answer - - keep_all: any answer in ground truth is considered acceptable - """ - # Collect ground truth answers per vuln_id - answers_per_id = {} - for gt in ground_truth_list: - vuln_id = gt.get("vuln_id") - if not vuln_id: - continue - ans = gt.get(field) - if ans is None: - continue - answers_per_id.setdefault(vuln_id, []) - answers_per_id[vuln_id].append(ans) - - acceptable_map = {} - - if policy == "drop_all": - for vuln_id, answers in answers_per_id.items(): - distinct = list(dict.fromkeys(answers)) # preserve order, unique - if len(distinct) == 1: - acceptable_map[vuln_id] = {distinct[0]} - return acceptable_map - - if policy == "keep_first": - # Keep first encountered answer per vuln_id - for vuln_id, answers in answers_per_id.items(): - first_answer = answers[0] - acceptable_map[vuln_id] = {first_answer} - return acceptable_map - - # keep_all: any of the distinct answers are acceptable - for vuln_id, answers in answers_per_id.items(): - acceptable_map[vuln_id] = set(answers) - return acceptable_map - - def _extract_rep_number(item_id: str) -> str: - """Extract rep number from item id (e.g., 'container1_rep1' -> 'rep1')""" - if "_rep" in item_id: - return "rep" + item_id.split("_rep")[-1] - return "rep1" # Default to rep1 if no rep suffix - - def _group_by_rep(items: list[EvalInputItem]) -> dict[str, list[EvalInputItem]]: - """Group items by rep number""" - groups = {} - for item in items: - rep_id = _extract_rep_number(item.id) - groups.setdefault(rep_id, []) - groups[rep_id].append(item) - return groups - - async def process_rep_group(items: list[EvalInputItem]): - """Process all containers in a single rep together""" - # Aggregate results across all containers in this rep - all_per_item_accuracies = [] - acceptable_answers_map_combined = {} - test_set_file = "unknown" - container_data = {} - - if self.field == "status": - answer_key = "status_answer" - gen_key = "generated_status_answer" - else: - answer_key = "label_answer" - gen_key = "generated_label_answer" - - for item in items: - # Extract container name from item id (e.g., "container1_rep1" -> "container1") - container_name = item.id.split('_rep')[0] if '_rep' in item.id else item.id - - # Build acceptable answers map from the original ground truth list - original_ground_truth = item.full_dataset_entry.get("ground_truth", []) - acceptable_answers_map = _preprocess_ground_truth(original_ground_truth, self.field, self.duplicates_policy) - acceptable_answers_map_combined.update(acceptable_answers_map) - - pipeline_result = json.loads(item.output_obj)["output"] - test_set_file = item.full_dataset_entry.get("test_set_file", "unknown") - - # Track data per container - container_questions = [] - container_generated_answers = [] - - for result in pipeline_result: - vuln_id = result["vuln_id"] - - if self.field == "status": - justification_status = result.get("justification", {}).get("status", "MISSING") - pred = _STATUS_MAP.get(justification_status, "MISSING") - acceptable_set = acceptable_answers_map.get(vuln_id) - else: # label - justification_label = result.get("justification", {}).get("label", "MISSING") - pred = justification_label - acceptable_set = acceptable_answers_map.get(vuln_id) - - # If this vuln_id was filtered out by the duplicates policy, skip - if acceptable_set is None: - continue - - container_questions.append(vuln_id) - container_generated_answers.append(pred) - all_per_item_accuracies.append(float(pred in acceptable_set)) - - # Output the data per-container - container_data[container_name] = { - "question": container_questions, - answer_key: [sorted(list(acceptable_answers_map_combined.get(q, set()))) for q in container_questions], - gen_key: container_generated_answers, - } - - total_correct = sum(all_per_item_accuracies) - total_items = len(all_per_item_accuracies) - avg = round(total_correct / total_items, 2) if total_items > 0 else 0.0 - reasoning = {**container_data, "test_set_file": test_set_file} - - return avg, reasoning, total_items, total_correct - - async def wrapped_process_rep(rep_id: str, items: list[EvalInputItem]) -> tuple[str, float, dict, int, int]: - async with self.semaphore: - result = await process_rep_group(items) - pbar.update(len(items)) - return (rep_id, *result) - - # Group items by rep number - rep_groups = _group_by_rep(eval_input.eval_input_items) + self.semaphore = asyncio.Semaphore(max_concurrency) + self.uncertain_mode = uncertain_mode - try: + async def evaluate(self, eval_input: EvalInput) -> EvalOutputLike: + items = eval_input.eval_input_items + predictions: list[dict] = [] + + tqdm_position = None + if TqdmPositionRegistry is not None: tqdm_position = TqdmPositionRegistry.claim() - pbar = tqdm(total=len(eval_input.eval_input_items), - desc=f"Evaluating {self.field.title()} Accuracy", - position=tqdm_position) - results = await asyncio.gather(*[wrapped_process_rep(rep_id, items) for rep_id, items in rep_groups.items()]) + + try: + pbar = tqdm( + total=len(items), + desc=f"Evaluating Accuracy (uncertain={self.uncertain_mode})", + position=tqdm_position, + ) + + async def _process_item(item: EvalInputItem) -> dict: + async with self.semaphore: + result = self._extract_prediction(item) + pbar.update(1) + return result + + predictions = await asyncio.gather(*[_process_item(item) for item in items]) finally: pbar.close() - TqdmPositionRegistry.release(tqdm_position) + if TqdmPositionRegistry is not None and tqdm_position is not None: + TqdmPositionRegistry.release(tqdm_position) - # Unpack results: rep_id, avg, reasoning, total_items, total_correct - if results: - rep_ids, all_averages, all_reasonings, _, _ = zip(*results) - else: - rep_ids, all_averages, all_reasonings = [], [], [] - - # Output dict format with .describe() summary - scores_sequential = list(all_averages) - if scores_sequential: - series = pd.Series(scores_sequential) - desc = series.describe() # count, mean, std, min, 25%, 50%, 75%, max - # Round values - final_output = {k: (int(v) if k == "count" else round(float(v), 2)) for k, v in desc.items()} - else: - final_output = { - "count": 0, - "mean": 0.0, - "std": 0.0, - "min": 0.0, - "25%": 0.0, - "50%": 0.0, - "75%": 0.0, - "max": 0.0, - } + metrics = _compute_metrics(list(predictions), self.uncertain_mode) + + n_uncertain = sum(1 for p in predictions if _is_uncertain(p.get("predicted_justification"))) + n_success = sum(1 for p in predictions if p.get("success")) + n_failed = sum(1 for p in predictions if p.get("error")) + + summary = { + **metrics, + "total_cases": len(predictions), + "successful_runs": n_success, + "uncertain_runs": n_uncertain, + "failed_runs": n_failed, + "uncertain_mode": self.uncertain_mode, + } eval_output_items = [ - EvalOutputItem(id=rep_id, score=score, reasoning=reasoning) - for rep_id, score, reasoning in zip(rep_ids, all_averages, all_reasonings) + EvalOutputItem( + id=p["item_id"], + score=1.0 if p.get("label_match") else 0.0, + reasoning={ + "vuln_id": p.get("vuln_id"), + "container": p.get("container"), + "ground_truth_label": p.get("ground_truth_label"), + "predicted_justification": p.get("predicted_justification"), + "predicted_status": p.get("predicted_status"), + "label_match": p.get("label_match"), + "binary_match": p.get("binary_match"), + "binary_ground_truth": p.get("binary_ground_truth"), + "binary_prediction": p.get("binary_prediction"), + "success": p.get("success"), + }, + ) + for p in predictions ] - return EvalOutput(average_score=final_output, eval_output_items=eval_output_items) + return EvalOutput(average_score=summary, eval_output_items=eval_output_items) + + @staticmethod + def _extract_prediction(item: EvalInputItem) -> dict: + """Extract prediction data from a completed workflow run.""" + gt_label = item.full_dataset_entry.get("ground_truth_label", "") + vuln_id_raw = item.full_dataset_entry.get("vuln_id", "") + container = ( + item.full_dataset_entry.get("pull_ref") + or item.full_dataset_entry.get("image", "") + ) + item_id = item.full_dataset_entry.get("item_id", item.id) + + pred_just = None + pred_status = None + success = False + error = None + + if item.output_obj is not None: + try: + output = item.output_obj if isinstance(item.output_obj, dict) else json.loads(item.output_obj) + results = output.get("results") or [] + selected = next( + ( + result for result in results + if ( + result.get("vuln_id") + or result.get("cve_id") + or result.get("ghsa_id") + ) == vuln_id_raw + ), + results[0] if results else {}, + ) + pred_just = selected.get("vex_justification") + pred_status = selected.get("vex_status") + success = selected.get("success", output.get("success", False)) + error = selected.get("error") + except (json.JSONDecodeError, TypeError, AttributeError) as exc: + error = str(exc) + + label_match = (pred_just == gt_label) if pred_just else False + binary_gt = _to_binary(gt_label) + binary_pred = _to_binary(pred_just) if pred_just else "not_vulnerable" + binary_match = binary_gt == binary_pred + + return { + "item_id": item_id, + "vuln_id": vuln_id_raw, + "container": container, + "ground_truth_label": gt_label, + "predicted_justification": pred_just, + "predicted_status": pred_status, + "success": success, + "error": error, + "label_match": label_match, + "binary_ground_truth": binary_gt, + "binary_prediction": binary_pred, + "binary_match": binary_match, + } diff --git a/src/vuln_analysis/eval/evaluators/consistency.py b/src/vuln_analysis/eval/evaluators/consistency.py deleted file mode 100644 index 42520c2fe..000000000 --- a/src/vuln_analysis/eval/evaluators/consistency.py +++ /dev/null @@ -1,151 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import json -import typing -from collections import Counter - -from nat.builder.builder import EvalBuilder -from nat.builder.evaluator import EvaluatorInfo -from nat.cli.register_workflow import register_evaluator -from nat.data_models.evaluator import EvaluatorBaseConfig -from nat.eval.evaluator.evaluator_model import EvalInput -from nat.eval.evaluator.evaluator_model import EvalOutput -from nat.eval.evaluator.evaluator_model import EvalOutputItem -from nat.eval.utils.tqdm_position_registry import TqdmPositionRegistry -from pydantic import Field -from tqdm import tqdm - - -class ConsistencyEvaluatorConfig(EvaluatorBaseConfig, name="consistency"): - """Configuration for consistency evaluator""" - field: typing.Literal["label", "status"] = Field(default="label", - description="The field to evaluate consistency on.") - - -@register_evaluator(config_type=ConsistencyEvaluatorConfig) -async def consistency_evaluator(config: ConsistencyEvaluatorConfig, builder: EvalBuilder): - """Register consistency evaluator""" - evaluator = ConsistencyEvaluator(builder.get_max_concurrency(), field=config.field) - yield EvaluatorInfo(config=config, - evaluate_fn=evaluator.evaluate, - description=f"{config.field.title()} Consistency Evaluator") - - -class ConsistencyEvaluator: - '''Evaluator class for measuring consistency of the specified field via the agreement probability metric ∑p², where p is the probability of each unique value''' - - def __init__(self, max_concurrency: int, field: str = "label"): - self.max_concurrency = max_concurrency - self.semaphore = asyncio.Semaphore(self.max_concurrency) - self.field = field # "label" or "status" - - async def evaluate(self, eval_input: EvalInput) -> EvalOutput: - '''Evaluate consistency across multiple runs per CVE, broken down by container''' - - async def extract_run_data(item): - """Extract data from a single evaluation run""" - async with self.semaphore: - evaluation_run = json.loads(item.output_obj)['output'] - pbar.update(1) - return evaluation_run, item - - try: - tqdm_position = TqdmPositionRegistry.claim() - pbar = tqdm(total=len(eval_input.eval_input_items), desc="Evaluating Consistency", position=tqdm_position) - - # Extract data from all evaluation runs in parallel - all_runs_with_items = await asyncio.gather( - *[extract_run_data(item) for item in eval_input.eval_input_items]) - finally: - pbar.close() - TqdmPositionRegistry.release(tqdm_position) - - # Group runs by container (extract container name from item id) - container_runs = {} - for evaluation_run, item in all_runs_with_items: - container_name = item.id.split('_rep')[0] - - if container_name not in container_runs: - container_runs[container_name] = [] - container_runs[container_name].append(evaluation_run) - - # Validate that each container has multiple reps - for container_name, runs in container_runs.items(): - if len(runs) <= 1: - raise ValueError( - f"Consistency evaluator requires multiple runs per container. Container '{container_name}' has only {len(runs)} run(s). Consistency cannot be measured with a single run per container." - ) - - # Calculate consistency for each container - eval_output_items = [] - all_cve_consistencies = [] # Collect all CVE consistency scores across all containers - - for container_name, runs in container_runs.items(): - # Aggregate results across all reps for this container - vuln_responses = {} - for evaluation_run in runs: - for cve_result in evaluation_run: # Group by CVE - vuln_id = cve_result['vuln_id'] - # Measure consistency on the specified field - field_value = cve_result.get('justification', {}).get(self.field, 'MISSING') - - if vuln_id not in vuln_responses: - vuln_responses[vuln_id] = [] - - vuln_responses[vuln_id].append(field_value) - - # Calculate consistency score per CVE for this container - consistencies_dict = {} # Stores consistency scores mapped to CVE - consistencies = [] # Stores all consistency scores in an array - - for vuln_id, values in vuln_responses.items(): - if len(values) > 1: - value_counts = Counter(values) - n = len(values) - probs = [count / n for count in value_counts.values()] - consistency = sum(p * p for p in probs) - else: - consistency = 1.0 # Perfect consistency if there is only one response - - consistencies_dict[vuln_id] = round(consistency, 2) - consistencies.append(round(consistency, 2)) - - # Add all CVE consistency scores from this container to the global collection - all_cve_consistencies.extend(consistencies) - - # Calculate average consistency for this container - container_avg = sum(consistencies) / len(consistencies) if consistencies else 1.0 - container_score = round(container_avg, 2) - - # Create output with per-CVE scores for this container - output = { - f"{self.field}_consistencies": consistencies_dict, - "total_cves": len(vuln_responses), - "num_reps": len(runs), - } - - # Create EvalOutputItem for this container - eval_output_items.append( - EvalOutputItem(id=f"{self.field.title()} Consistency per CVE - {container_name}", - score=container_score, - reasoning=output)) - - # Calculate overall average across all CVEs from all containers - overall_avg = sum(all_cve_consistencies) / len(all_cve_consistencies) if all_cve_consistencies else 1.0 - overall_score = round(overall_avg, 2) - - return EvalOutput(average_score=overall_score, eval_output_items=eval_output_items) diff --git a/src/vuln_analysis/eval/evaluators/cve_report_judge.py b/src/vuln_analysis/eval/evaluators/cve_report_judge.py new file mode 100644 index 000000000..39e51c80a --- /dev/null +++ b/src/vuln_analysis/eval/evaluators/cve_report_judge.py @@ -0,0 +1,557 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""NAT LLM-as-judge evaluator for CVE researcher reports.""" + +from __future__ import annotations + +import asyncio +import json +import logging +import re +from pathlib import Path +from typing import Any + +from nat.builder.builder import EvalBuilder +from nat.builder.evaluator import EvaluatorInfo +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.cli.register_workflow import register_evaluator +from nat.data_models.component_ref import LLMRef +from nat.data_models.evaluator import EvalInput +from nat.data_models.evaluator import EvalInputItem +from nat.data_models.evaluator import EvaluatorBaseConfig +from nat.plugins.eval.data_models.evaluator_io import EvalOutput +from nat.plugins.eval.data_models.evaluator_io import EvalOutputItem +from pydantic import Field +from tqdm import tqdm + +from vuln_analysis.utils.concurrency import atomic_write + +try: + from nat.plugins.eval.utils.tqdm_position_registry import TqdmPositionRegistry +except ImportError: + TqdmPositionRegistry = None # type: ignore[assignment,misc] + +logger = logging.getLogger(__name__) + +_SCORE_FIELDS = ("technical_details", "completeness") +_CONTENT_KEYS = ( + "report_content", + "gold_report", + "candidate_report", + "report", + "content", + "output", +) +_PATH_KEYS = ( + "report_path", + "gold_report_path", + "candidate_report_path", + "vulnerability_report_path", +) + + +class CVEReportJudgeConfig(EvaluatorBaseConfig, name="cve_report_judge"): + """Configuration for the CVE report LLM-as-judge evaluator.""" + + llm_name: LLMRef = Field(description="LLM to use as the report comparison judge") + max_retries: int = Field(default=3, ge=1, description="Maximum judge retries per item") + score_scale_max: int = Field(default=5, ge=1, description="Maximum score in the judge rubric") + max_report_chars: int = Field( + default=60000, + ge=1000, + description="Maximum characters from each report to send to the judge", + ) + save_generated_reports: bool = Field( + default=True, + description="Persist each generated CVE researcher report before judging", + ) + report_output_dir: str | None = Field( + default=None, + description=("Directory for persisted generated reports. Defaults to eval.general.output_dir " + "when unset."), + ) + report_output_subdir: str | None = Field( + default=None, + description=("Subdirectory for persisted generated reports. Defaults to the workflow " + "output_subdir when unset."), + ) + + +@register_evaluator(config_type=CVEReportJudgeConfig) +async def cve_report_judge_evaluator(config: CVEReportJudgeConfig, builder: EvalBuilder): + """Register the CVE report judge evaluator with NAT.""" + get_llm = getattr(builder, "get_llm", None) + if get_llm is None: + raise RuntimeError("This NAT builder does not support LLM-backed evaluators") + + llm = await get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + evaluator = CVEReportJudgeEvaluator( + llm=llm, + max_concurrency=builder.get_max_concurrency(), + max_retries=config.max_retries, + score_scale_max=config.score_scale_max, + max_report_chars=config.max_report_chars, + save_generated_reports=config.save_generated_reports, + report_output_dir=_resolve_report_output_dir(config, builder), + report_output_subdir=_resolve_report_output_subdir(config, builder), + ) + yield EvaluatorInfo( + config=config, + evaluate_fn=evaluator.evaluate, + description="LLM-as-judge comparison of CVE researcher reports against gold reports", + ) + + +class CVEReportJudgeEvaluator: + """Compare generated CVE researcher reports against gold-standard reports.""" + + def __init__( + self, + llm: Any, + max_concurrency: int = 4, + max_retries: int = 3, + score_scale_max: int = 5, + max_report_chars: int = 60000, + save_generated_reports: bool = True, + report_output_dir: Path | None = None, + report_output_subdir: str = "vulnerability_reports", + ): + self.llm = llm + self.max_concurrency = max_concurrency + self.semaphore = asyncio.Semaphore(max_concurrency) + self.max_retries = max_retries + self.score_scale_max = score_scale_max + self.max_report_chars = max_report_chars + self.save_generated_reports = save_generated_reports + self.report_output_dir = report_output_dir + self.report_output_subdir = report_output_subdir + + async def evaluate(self, eval_input: EvalInput) -> EvalOutput: + output_items: list[EvalOutputItem] = [] + + tqdm_position = None + if TqdmPositionRegistry is not None: + tqdm_position = TqdmPositionRegistry.claim() + + pbar = tqdm( + total=len(eval_input.eval_input_items), + desc="Evaluating CVE Reports", + position=tqdm_position, + ) + try: + + async def _process_item(item: EvalInputItem) -> EvalOutputItem: + async with self.semaphore: + try: + result = await self.evaluate_item(item) + except Exception as exc: + logger.exception("CVE report judge failed for item %s", item.id) + result = EvalOutputItem( + id=item.id, + score=0.0, + reasoning={"error": f"Evaluator error: {exc}"}, + error=str(exc), + ) + pbar.update(1) + return result + + output_items = await asyncio.gather(*[_process_item(item) for item in eval_input.eval_input_items]) + finally: + pbar.close() + if TqdmPositionRegistry is not None and tqdm_position is not None: + TqdmPositionRegistry.release(tqdm_position) + + return EvalOutput( + average_score=self._compute_summary(output_items), + eval_output_items=output_items, + ) + + async def evaluate_item(self, item: EvalInputItem) -> EvalOutputItem: + identifier = _item_identifier(item) + gold_report = _extract_report_text( + item.expected_output_obj, + item.full_dataset_entry, + nested_key=None, + ) + candidate_report = _extract_report_text( + item.output_obj, + item.full_dataset_entry, + nested_key="cve_researcher", + ) + + if not gold_report: + return EvalOutputItem( + id=item.id, + score=0.0, + reasoning={ + "error": "No gold-standard report found", "identifier": identifier + }, + error="No gold-standard report found", + ) + if not candidate_report: + return EvalOutputItem( + id=item.id, + score=0.0, + reasoning={ + "error": "No candidate CVE researcher report found", "identifier": identifier + }, + error="No candidate CVE researcher report found", + ) + + generated_report_path = None + if self.save_generated_reports and self.report_output_dir is not None: + generated_report_path = _persist_generated_report( + identifier=identifier, + report_content=candidate_report, + output_dir=self.report_output_dir, + output_subdir=self.report_output_subdir, + ) + + prompt = _build_judge_prompt( + identifier=identifier, + gold_report=_truncate_report(gold_report, self.max_report_chars), + candidate_report=_truncate_report(candidate_report, self.max_report_chars), + score_scale_max=self.score_scale_max, + ) + + parsed: dict[str, Any] | None = None + judge_response = "" + last_error = None + for attempt in range(self.max_retries): + try: + judge_response = await self._call_llm(prompt) + parsed = _parse_judge_response(judge_response) + _validate_score_payload(parsed, self.score_scale_max) + break + except Exception as exc: + parsed = None + last_error = str(exc) + logger.warning( + "Invalid CVE report judge response for %s (attempt %d/%d): %s", + identifier, + attempt + 1, + self.max_retries, + exc, + ) + await asyncio.sleep(1.5**attempt) + + if parsed is None: + return EvalOutputItem( + id=item.id, + score=0.0, + reasoning={ + "identifier": identifier, + "error": f"Failed after {self.max_retries} retries: {last_error}", + "raw_judge_response": judge_response, + }, + error=last_error, + ) + + scores = _extract_scores(parsed) + raw_score = sum(scores.values()) / len(scores) + normalized_score = round(raw_score / self.score_scale_max, 4) + reasoning = { + "identifier": identifier, + "scores": scores, + "normalized_scores": { + key: round(value / self.score_scale_max, 4) + for key, value in scores.items() + }, + "technical_details": parsed["technical_details"], + "completeness": parsed["completeness"], + "gold_report_chars": len(gold_report), + "candidate_report_chars": len(candidate_report), + "generated_report_path": str(generated_report_path) if generated_report_path else None, + "raw_judge_response": judge_response, + } + + return EvalOutputItem(id=item.id, score=normalized_score, reasoning=reasoning) + + async def _call_llm(self, prompt: str) -> str: + response = await self.llm.ainvoke(prompt) + return response.content if hasattr(response, "content") else str(response) + + def _compute_summary(self, output_items: list[EvalOutputItem]) -> dict[str, Any]: + valid_items = [ + item for item in output_items + if not item.error and isinstance(item.reasoning, dict) and isinstance(item.reasoning.get("scores"), dict) + ] + failed_items = len(output_items) - len(valid_items) + if not valid_items: + return { + "total_items": len(output_items), + "evaluated_items": 0, + "failed_items": failed_items, + "score_scale": f"0-{self.score_scale_max}", + "technical_details_avg": 0.0, + "completeness_avg": 0.0, + "combined_normalized": 0.0, + } + + averages = {} + normalized = {} + for field in _SCORE_FIELDS: + avg = sum(item.reasoning["scores"][field] for item in valid_items) / len(valid_items) + averages[f"{field}_avg"] = round(avg, 4) + normalized[f"{field}_normalized"] = round(avg / self.score_scale_max, 4) + + return { + "total_items": len(output_items), + "evaluated_items": len(valid_items), + "failed_items": failed_items, + "score_scale": f"0-{self.score_scale_max}", + "combined_normalized": round(sum(normalized.values()) / len(normalized), 4), + **averages, + **normalized, + } + + +def _build_judge_prompt( + *, + identifier: str, + gold_report: str, + candidate_report: str, + score_scale_max: int, +) -> str: + """Build the rubric prompt for the LLM judge.""" + return f"""\ +You are a senior vulnerability analyst judging a generated CVE researcher report +against a gold-standard CVE researcher report. + +Focus solely on technical correctness, technical detail, and completeness +relative to the gold report. Ignore writing style, formatting, prose polish, +section order, and general readability unless they make a technical claim +ambiguous enough to change its meaning. + +The gold report is the reference for expected coverage. Penalize candidate +claims that are incorrect, unsupported by the gold report, contradictory to the +gold report, or technically misleading. Also penalize missing material facts from +the gold report. + +Score each dimension from 0 to {score_scale_max}: +- technical_details: correctness and precision of technical claims, including + affected components, vulnerable and fixed versions, root cause, vulnerable + functions or APIs, attack vector, exploitability, impact, remediation, and + references. +- completeness: coverage of the important technical facts in the gold report, + including version ranges, patched releases, exploit prerequisites, exploit + availability, observed exploitation, mitigations, source references, and + caveats such as disputed status or environmental assumptions. + +Return valid JSON only, with this exact schema: +{{ + "technical_details": {{ + "score": 0, + "differences": ["specific technical differences"], + "incorrect_or_unsupported_in_candidate": ["candidate claims that are wrong or unsupported"] + }}, + "completeness": {{ + "score": 0, + "missing_from_candidate": ["important gold details missing from candidate"], + "extra_but_relevant_candidate_details": ["candidate details not in gold report that appear technically relevant"], + "coverage_summary": "brief summary of how completely the candidate covers the gold report" + }} +}} + +CVE/GHSA identifier: {identifier} + + +{gold_report} + + + +{candidate_report} + +""" + + +def _extract_scores(payload: dict[str, Any]) -> dict[str, float]: + return {field: float(payload[field]["score"]) for field in _SCORE_FIELDS} + + +def _validate_score_payload(payload: dict[str, Any], score_scale_max: int) -> None: + for field in _SCORE_FIELDS: + section = payload.get(field) + if not isinstance(section, dict): + raise ValueError(f"Missing object field {field!r}") + score = section.get("score") + if not isinstance(score, int | float): + raise ValueError(f"Missing numeric score for {field!r}") + if score < 0 or score > score_scale_max: + raise ValueError(f"Score for {field!r} must be between 0 and {score_scale_max}") + + +def _parse_judge_response(response: str) -> dict[str, Any]: + text = response.strip() + if text.startswith("```"): + lines = text.splitlines() + if lines and lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].startswith("```"): + lines = lines[:-1] + text = "\n".join(lines).strip() + + try: + parsed = json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + end = text.rfind("}") + if start == -1 or end == -1 or end <= start: + raise + parsed = json.loads(text[start:end + 1]) + + if not isinstance(parsed, dict): + raise ValueError("Judge response must be a JSON object") + return parsed + + +def _resolve_report_output_dir(config: CVEReportJudgeConfig, builder: EvalBuilder) -> Path | None: + if config.report_output_dir: + return Path(config.report_output_dir) + + try: + return builder.get_output_dir() + except Exception: + logger.warning("Could not resolve eval output_dir for generated report persistence", exc_info=True) + return None + + +def _resolve_report_output_subdir(config: CVEReportJudgeConfig, builder: EvalBuilder) -> str: + if config.report_output_subdir: + return config.report_output_subdir + + get_workflow_config = getattr(builder, "get_workflow_config", None) + if get_workflow_config is not None: + try: + workflow_config = get_workflow_config() + output_subdir = getattr(workflow_config, "output_subdir", None) + if output_subdir: + return str(output_subdir) + except Exception: + logger.debug("Could not resolve workflow output_subdir", exc_info=True) + + return "vulnerability_reports" + + +def _persist_generated_report( + *, + identifier: str, + report_content: str, + output_dir: Path, + output_subdir: str, +) -> Path: + report_path = output_dir / output_subdir / f"{_safe_filename(identifier)}.md" + atomic_write(report_path, report_content) + return report_path + + +def _safe_filename(value: str) -> str: + safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) + return safe.strip("._") or "generated_report" + + +def _extract_report_text(value: Any, dataset_entry: Any, *, nested_key: str | None) -> str | None: + parsed = _maybe_model_dump(value) + report = _extract_report_text_from_value(parsed, nested_key=nested_key) + if report: + return report + + entry = _maybe_model_dump(dataset_entry) + if isinstance(entry, dict) and nested_key is None: + return _extract_report_text_from_value(entry, nested_key=nested_key) + if isinstance(entry, dict): + candidate_fallback = { + "candidate_report": entry.get("candidate_report"), + "candidate_report_path": entry.get("candidate_report_path"), + "generated_answer": entry.get("generated_answer"), + } + return _extract_report_text_from_value(candidate_fallback, nested_key=None) + return None + + +def _extract_report_text_from_value(value: Any, *, nested_key: str | None) -> str | None: + value = _maybe_model_dump(value) + + if isinstance(value, str): + parsed = _loads_json_or_none(value) + if parsed is not None: + return _extract_report_text_from_value(parsed, nested_key=nested_key) + path_content = _read_path_if_it_exists(value) + return path_content or value.strip() or None + + if not isinstance(value, dict): + return None + + if nested_key and isinstance(value.get(nested_key), dict): + nested_report = _extract_report_text_from_value(value[nested_key], nested_key=None) + if nested_report: + return nested_report + + for key in _CONTENT_KEYS: + content = value.get(key) + if isinstance(content, str) and content.strip(): + parsed = _loads_json_or_none(content) + if parsed is not None and isinstance(parsed, dict): + nested_report = _extract_report_text_from_value(parsed, nested_key=nested_key) + if nested_report: + return nested_report + return content.strip() + + for key in _PATH_KEYS: + content = _read_path_if_it_exists(value.get(key)) + if content: + return content + + return None + + +def _maybe_model_dump(value: Any) -> Any: + if hasattr(value, "model_dump"): + return value.model_dump() + return value + + +def _loads_json_or_none(value: str) -> Any | None: + try: + return json.loads(value) + except (json.JSONDecodeError, TypeError): + return None + + +def _read_path_if_it_exists(value: Any) -> str | None: + if not isinstance(value, str) or not value.strip() or "\n" in value: + return None + try: + path = Path(value) + if path.exists() and path.is_file(): + return path.read_text(encoding="utf-8") + except OSError: + return None + return None + + +def _truncate_report(report: str, max_chars: int) -> str: + if len(report) <= max_chars: + return report + + head_chars = max_chars // 2 + tail_chars = max_chars - head_chars + omitted = len(report) - max_chars + return (report[:head_chars] + f"\n\n[TRUNCATED {omitted} CHARACTERS FROM THE MIDDLE]\n\n" + report[-tail_chars:]) + + +def _item_identifier(item: EvalInputItem) -> str: + entry = item.full_dataset_entry if isinstance(item.full_dataset_entry, dict) else {} + return (entry.get("identifier") or entry.get("cve_id") or entry.get("ghsa_id") or str(item.id)) diff --git a/src/vuln_analysis/eval/parse_cve_report_directory.py b/src/vuln_analysis/eval/parse_cve_report_directory.py new file mode 100644 index 000000000..68133ed70 --- /dev/null +++ b/src/vuln_analysis/eval/parse_cve_report_directory.py @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Custom NAT dataset parser for gold-standard CVE report directories. + +The parser treats each markdown report in a directory as one evaluation item. +The filename stem is used as the CVE/GHSA identifier, and the report content is +stored as the expected output for an LLM-as-judge evaluator. + +NAT config usage:: + + eval: + general: + dataset: + _type: custom + file_path: .tmp/202604/20260427_baseline/outputs/vulnerability_reports + function: vuln_analysis.eval.parse_cve_report_directory.parse_input +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +from nat.data_models.evaluator import EvalInput +from nat.data_models.evaluator import EvalInputItem + +_CVE_RE = re.compile(r"CVE-\d{4}-\d{4,}", re.IGNORECASE) +_GHSA_RE = re.compile(r"GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}", re.IGNORECASE) + + +def parse_input( + file_path: Path, + *, + glob_pattern: str = "*.md", + recursive: bool = False, + max_reports: int | None = None, +) -> EvalInput: + """Parse a directory of gold-standard CVE reports into NAT ``EvalInput``. + + Args: + file_path: Directory containing one markdown report per CVE/GHSA. + glob_pattern: File pattern to include. Defaults to ``*.md``. + recursive: Whether to search child directories recursively. + max_reports: Optional cap for quick local smoke tests. + + Returns: + EvalInput whose items run the CVE researcher for each identifier and + carry the matching gold report as ``expected_output_obj``. + """ + report_dir = Path(file_path) + if not report_dir.exists(): + raise FileNotFoundError(f"Gold report directory does not exist: {report_dir}") + if not report_dir.is_dir(): + raise ValueError(f"Gold report input must be a directory, got: {report_dir}") + + iterator = report_dir.rglob(glob_pattern) if recursive else report_dir.glob(glob_pattern) + report_paths = sorted(path for path in iterator if path.is_file()) + if max_reports is not None: + report_paths = report_paths[:max_reports] + if not report_paths: + raise ValueError(f"No reports matching {glob_pattern!r} found in {report_dir}") + + eval_items: list[EvalInputItem] = [] + for report_path in report_paths: + identifier = _identifier_from_report_path(report_path) + report_content = report_path.read_text(encoding="utf-8") + + is_ghsa = identifier.upper().startswith("GHSA-") + researcher_input = { + "cve_id": None if is_ghsa else identifier, + "ghsa_id": identifier if is_ghsa else None, + "package_hint": None, + "version_hint": None, + } + expected = { + "identifier": identifier, + "gold_report_path": str(report_path), + "gold_report": report_content, + } + + eval_items.append( + EvalInputItem( + id=identifier, + input_obj=json.dumps(researcher_input, separators=(",", ":")), + expected_output_obj=expected, + full_dataset_entry={ + "id": identifier, + "identifier": identifier, + "cve_id": None if is_ghsa else identifier, + "ghsa_id": identifier if is_ghsa else None, + "gold_report_path": str(report_path), + "gold_report_chars": len(report_content), + }, + )) + + print(f"Loaded {len(eval_items)} gold CVE reports from {report_dir}") + return EvalInput(eval_input_items=eval_items) + + +def _identifier_from_report_path(report_path: Path) -> str: + """Extract a CVE or GHSA identifier from a report filename.""" + stem = report_path.stem + + cve_match = _CVE_RE.search(stem) + if cve_match: + return cve_match.group(0).upper() + + ghsa_match = _GHSA_RE.search(stem) + if ghsa_match: + return ghsa_match.group(0).upper() + + raise ValueError(f"Report filename must contain a CVE or GHSA identifier: {report_path.name}") diff --git a/src/vuln_analysis/eval/parse_eval_input.py b/src/vuln_analysis/eval/parse_eval_input.py index baf6478b8..08ebbaba9 100644 --- a/src/vuln_analysis/eval/parse_eval_input.py +++ b/src/vuln_analysis/eval/parse_eval_input.py @@ -13,178 +13,109 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -from pathlib import Path - -from nat.eval.evaluator.evaluator_model import EvalInput -from nat.eval.evaluator.evaluator_model import EvalInputItem - -from vuln_analysis.data_models.eval_input import AgentMorpheusEvalDataset +"""Custom NAT dataset parser for benchmark CSVs. +Reads a CSV with columns: + vuln_id, image_name, image_tag, filesystem_path, image_config_path, + ground_truth_label -def parse_input(file_path: Path) -> EvalInput: - """ - Transform a human-readable eval dataset in the eval_datasets/ directory into a series of EvalInputItems - that the NAT evaluation harness accepts. Each EvalInputItem corresponds to a container. - EvalInputItem `id` is the container name, `question` encodes the source code and input CVEs, - and `answer` encodes the CVE ground truth mapping. - - Usage (outside of the full NAT pipeline): - python3 parse_eval_input.py [test_set_file].json --preview -o [your output file].json - Example: - python3 parse_eval_input.py ../data/eval_datasets/eval_dataset.json --preview -o my_eval_string.json - Run this inside the vuln_analysis container, from the eval directory. - - Example output: - [ - { - "id": "morpheus:23.11-runtime", - "question": "{\"image\":{\"name\":\"nvcr.io/nvidia/morpheus/morpheus\",\"tag\":\"23.11-runtime\",\"source_info\":[{\"type\":\"code\",\"git_repo\":\"https://github.com/nv-morpheus/Morpheus.git\",\"ref\":\"branch-23.11\",\"include\":[\"**/*.cpp\",\"**/*.cu\",\"**/*.cuh\",\"**/*.h\",\"**/*.hpp\",\"**/*.ipynb\",\"**/*.py\",\"**/*Dockerfile\"],\"exclude\":[\"tests/**/*\"]},{\"type\":\"doc\",\"git_repo\":\"https://github.com/nv-morpheus/Morpheus.git\",\"ref\":\"branch-23.11\",\"include\":[\"**/*.md\",\"docs/**/*.rst\"]}],\"sbom_info\":{\"_type\":\"file\",\"file_path\":\"data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom\"}},\"scan\":{\"vulns\":[{\"vuln_id\":\"GHSA-3f63-hfp8-52jq\"},{\"vuln_id\":\"CVE-2023-36632\"}]}}", - "answer": "{\"GHSA-3f63-hfp8-52jq\":\"NOT AFFECTED\",\"GHSA-3f63-hfp8-52jq_label\":\"code_not_reachable\",\"CVE-2023-36632\":\"AFFECTED\",\"CVE-2023-36632_label\":\"vulnerable\"}" - }, - { - "id": "morpheus:24.03-runtime", - "question": "{\"image\":{\"name\":\"nvcr.io/nvidia/morpheus/morpheus\",\"tag\":\"v24.03.02-runtime\",\"source_info\":[{\"type\":\"code\",\"git_repo\":\"https://github.com/nv-morpheus/Morpheus.git\",\"ref\":\"v24.03.02\",\"include\":[\"**/*.cpp\",\"**/*.cu\",\"**/*.cuh\",\"**/*.h\",\"**/*.hpp\",\"**/*.ipynb\",\"**/*.py\",\"**/*Dockerfile\"],\"exclude\":[\"tests/**/*\"]},{\"type\":\"doc\",\"git_repo\":\"https://github.com/nv-morpheus/Morpheus.git\",\"ref\":\"v24.03.02\",\"include\":[\"**/*.md\",\"docs/**/*.rst\"]}],\"sbom_info\":{\"_type\":\"file\",\"file_path\":\"data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom\"}},\"scan\":{\"vulns\":[{\"vuln_id\":\"GHSA-3f63-hfp8-52jq\"},{\"vuln_id\":\"CVE-2023-36632\"}]}}", - "answer": "{\"GHSA-3f63-hfp8-52jq\":\"NOT AFFECTED\",\"GHSA-3f63-hfp8-52jq_label\":\"code_not_reachable\",\"CVE-2023-36632\":\"AFFECTED\",\"CVE-2023-36632_label\":\"vulnerable\"}" - } - ] - """ - # Load and validate the test set file using Pydantic models - with open(file_path, 'r', encoding='utf-8') as f: - config_data = json.load(f) - dataset = AgentMorpheusEvalDataset.model_validate(config_data) - - # Extract containers - containers = dataset.containers - - # Get dataset metadata from the file - dataset_id = dataset.dataset_id - dataset_description = dataset.dataset_description - test_set_file = file_path.name # Extract filename from the file path - print(f"Processing dataset: {dataset_id} (file: {test_set_file})") - - # Create evaluation items for each container - eval_items = [] - - for container_id, container_data in containers.items(): - container_image = container_data.container_image.model_dump() - ground_truth = [gt.model_dump() for gt in container_data.ground_truth] - - # Create scan structure from ground truth - scan_vulns = [] - ground_truth_mapping = {} - seen_vuln_ids = set() # Ensures vuln_id uniqueness - - for gt_item in ground_truth: - vuln_id = gt_item.get('vuln_id') - status = gt_item.get('status') - label = gt_item.get('label') - - if not (vuln_id and status): - continue - - if vuln_id in seen_vuln_ids: # skip duplicate vuln_ids - continue - - seen_vuln_ids.add(vuln_id) - - # Add to scan structure - scan_vulns.append({"vuln_id": vuln_id}) - - # Add to ground truth mapping - ground_truth_mapping[vuln_id] = status - - # Add label to ground truth mapping if available - if label: - ground_truth_mapping[f"{vuln_id}_label"] = label - - # Create the question structure (container_image + scan) - question_structure = {"image": container_image, "scan": {"vulns": scan_vulns}} - - # Convert to stringified format compatible with NAT EvalInputItem format - question_string = json.dumps(question_structure, separators=(',', ':')) - answer_string = json.dumps(ground_truth_mapping, separators=(',', ':')) - - # Create evaluation item - eval_item = EvalInputItem(id=container_id, - input_obj=question_string, - expected_output_obj=answer_string, - full_dataset_entry={ - "id": container_id, - "question": question_string, - "answer": answer_string, - "test_set_file": test_set_file, - "dataset_id": dataset_id, - "dataset_description": dataset_description, - "container_id": container_id, - "ground_truth": ground_truth - }) - - eval_items.append(eval_item) - - if not eval_items: - print("No valid evaluation items created") - return EvalInput(eval_input_items=[]) - - print(f"Created {len(eval_items)} evaluation items for dataset '{dataset_id}'") - return EvalInput(eval_input_items=eval_items) +``filesystem_path`` and ``image_config_path`` are optional. The parser maps the +provided fields directly to the ``PipelineInput`` shape used by the workflow. +NAT config usage:: -# For debugging - saves the EvalInputItem string to a file -def save_as_baseline_string_format(eval_input: EvalInput, output_path: Path) -> None: - baseline_data = [] + eval: + general: + dataset: + _type: custom + file_path: data/eval_datasets/pb_eval_highcrit_15.csv + function: vuln_analysis.eval.parse_eval_input.parse_input +""" - for item in eval_input.eval_input_items: - baseline_item = {"id": item.id, "question": item.input_obj, "answer": item.expected_output_obj} - baseline_data.append(baseline_item) - - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(baseline_data, f, indent=4) - - print(f"Saved baseline string format to: {output_path}") +from __future__ import annotations +import csv +import json +from pathlib import Path -def main(): - import argparse +from nat.data_models.evaluator import EvalInput, EvalInputItem - parser = argparse.ArgumentParser( - description='Parse consolidated evaluation dataset with multiple containers and test sets', - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.add_argument('input_file', help='Path to eval-dataset.json file') - parser.add_argument('-o', '--output', help='Output path for baseline string format (optional)') - parser.add_argument('--preview', action='store_true', help='Show preview of first converted item') - args = parser.parse_args() +_REQUIRED_COLUMNS = frozenset({ + "vuln_id", + "image_name", + "image_tag", + "ground_truth_label", +}) - try: - input_path = Path(args.input_file) - # Convert to EvalInput format - eval_input = parse_input(input_path) +def parse_input(file_path: Path, **kwargs) -> EvalInput: + """Parse a benchmark CSV into NAT ``EvalInput``. - # Save as baseline string format if output is specified - if args.output: - output_path = Path(args.output) - save_as_baseline_string_format(eval_input, output_path) - else: - # Default output path in eval directory - eval_dir = Path(__file__).parent - default_output = eval_dir / "eval_input_string.json" - save_as_baseline_string_format(eval_input, default_output) + Each CSV row maps to one ``EvalInputItem``: - except FileNotFoundError as e: - print(f"Error: Input file not found - {e}") - return 1 - except json.JSONDecodeError as e: - print(f"Error: Invalid JSON in input file - {e}") - return 1 - except Exception as e: - print(f"Unexpected error: {e}") - return 1 + * **id** — ``"{vuln_id}__{image_name}:{image_tag}"`` (unique per case) + * **input_obj** — JSON-serialised ``PipelineInput`` dict + * **expected_output_obj** — JSON ``{"ground_truth_label": "..."}`` + * **full_dataset_entry** — the complete row dict for evaluator use + """ + rows = _load_csv(file_path) + print(f"Loaded {len(rows)} benchmark cases from {file_path.name}") + + eval_items: list[EvalInputItem] = [] + for row in rows: + vuln_id_raw = row["vuln_id"] + image_name = row["image_name"] + image_tag = row["image_tag"] + filesystem_path = row.get("filesystem_path") or "" + image_config_path = row.get("image_config_path") or None + pull_ref = f"{image_name}:{image_tag}" + + pipeline_input = { + "vulns": [{"vuln_id": vuln_id_raw}], + "image_name": image_name, + "image_tag": image_tag, + "filesystem_path": filesystem_path, + "image_config_path": image_config_path, + } + + item_id = f"{vuln_id_raw}__{pull_ref}" + expected = {"ground_truth_label": row["ground_truth_label"]} + + eval_items.append( + EvalInputItem( + id=item_id, + input_obj=json.dumps(pipeline_input, separators=(",", ":")), + expected_output_obj=json.dumps(expected, separators=(",", ":")), + full_dataset_entry={ + **row, + "filesystem_path": filesystem_path, + "image_config_path": image_config_path or "", + "pull_ref": pull_ref, + "item_id": item_id, + }, + ) + ) - return 0 + return EvalInput(eval_input_items=eval_items) -if __name__ == "__main__": - exit(main()) +def _load_csv(csv_path: Path) -> list[dict[str, str]]: + """Parse the benchmark CSV, validating required columns.""" + rows: list[dict[str, str]] = [] + with open(csv_path, newline="", encoding="utf-8") as fh: + reader = csv.DictReader(fh) + if reader.fieldnames is None: + raise ValueError(f"CSV file {csv_path} appears to be empty") + missing = _REQUIRED_COLUMNS - set(reader.fieldnames) + if missing: + raise ValueError( + f"CSV {csv_path} is missing required columns: {sorted(missing)}" + ) + for row in reader: + rows.append({ + k: v.strip() if v is not None else "" + for k, v in row.items() + if k is not None + }) + return rows diff --git a/src/vuln_analysis/eval/sample_pb_eval.py b/src/vuln_analysis/eval/sample_pb_eval.py new file mode 100644 index 000000000..adb7fced9 --- /dev/null +++ b/src/vuln_analysis/eval/sample_pb_eval.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Sample cases from the PB Eval dataset (Excel) and emit a benchmark CSV. + +The output CSV is directly consumable by run_benchmark.py. + +Source: "PB Eval Set (1).xlsx" — "Full dataset (Copy)" sheet +Ground truth drawn from rows where VEX State (Exception) is 'not_affected' or 'exploitable'. +Only cases whose container filesystem exists under --data-root are included. + +Usage (from project root): + # List all runnable cases + python benchmarks/sample_pb_eval.py --xlsx "PB Eval Set (1).xlsx" --list + + # All runnable cases + python benchmarks/sample_pb_eval.py --xlsx "PB Eval Set (1).xlsx" --output benchmarks/pb_eval_all.csv + + # Random 10 cases + python benchmarks/sample_pb_eval.py --xlsx "PB Eval Set (1).xlsx" -n 10 --output benchmarks/pb_eval_10.csv + + # Stratified 10 cases, reproducible + python benchmarks/sample_pb_eval.py --xlsx "PB Eval Set (1).xlsx" -n 10 --stratify --seed 42 --output benchmarks/pb_eval_10.csv + + # Then run the benchmark + python benchmarks/run_benchmark.py --csv benchmarks/pb_eval_10.csv \\ + --output-dir outputs/pb_eval --concurrency 2 +""" + +from __future__ import annotations + +import argparse +import csv +import os +import random +import sys +from pathlib import Path + +_DEFAULT_SHEET = "Full dataset (Copy)" +_DEFAULT_DATA_ROOT = "data/pb_container_data" +_DEFAULT_DATA_ROOT_ENV = "PB_EVAL_DATA_ROOT" + +_CSV_COLUMNS = [ + "vuln_id", + "image_name", + "image_tag", + "filesystem_path", + "image_config_path", + "ground_truth_label", +] + +# Map Excel VEX justification values to agent output labels +_JUSTIFICATION_MAP = { + "code_not_reachable": "code_not_reachable", + "code_not_present": "code_not_present", + "requires_configuration": "requires_configuration", + "requires_environment": "requires_environment", + "requires_dependency": "requires_dependency", + "protected_at_runtime": "runtime_protected", + "compiler_protected": "compiler_protected", + "perimeter_protected": "perimeter_protected", + "mitigating_control_protected": "mitigating_control_protected", + "false_positive": "false_positive", +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _dir_name_from_image(pull_ref: str) -> str: + """Derive the nspect_pb_container_data dir name from a full image reference. + + e.g. nvcr.io/nvidia/tritonserver-pb25h2:25.08.03-... -> tritonserver-pb25h2_25.08.03-... + """ + name_tag = pull_ref.strip().split("/")[-1] + if ":" in name_tag: + name, tag = name_tag.split(":", 1) + return f"{name}_{tag}" + return name_tag + + +def _split_image(pull_ref: str) -> tuple[str, str]: + """Split a full image reference into ``image_name`` and ``image_tag``.""" + final_component = pull_ref.rsplit("/", 1)[-1] + if ":" not in final_component: + raise ValueError(f"Image reference must include a tag: {pull_ref}") + return pull_ref.rsplit(":", 1) + + +def _ground_truth_label(vex_state: str | None, vex_just: str | None) -> str | None: + """Return the agent-compatible ground truth label, or None if not determinable.""" + if vex_state == "exploitable": + return "vulnerable" + if vex_state == "not_affected" and vex_just: + return _JUSTIFICATION_MAP.get(vex_just, vex_just) + return None + + +# --------------------------------------------------------------------------- +# Dataset loading +# --------------------------------------------------------------------------- + + +def load_cases(xlsx_path: str, sheet_name: str, data_root: str) -> list[dict]: + try: + import openpyxl + except ImportError: + print("ERROR: openpyxl not installed. Run: pip install openpyxl", file=sys.stderr) + sys.exit(1) + + data_root_path = Path(data_root) + available_dirs = {d.name for d in data_root_path.iterdir() if d.is_dir()} + + wb = openpyxl.load_workbook(xlsx_path, read_only=True) + if sheet_name not in wb.sheetnames: + print(f"ERROR: Sheet '{sheet_name}' not found. Available: {wb.sheetnames}", file=sys.stderr) + sys.exit(1) + + ws = wb[sheet_name] + all_rows = list(ws.rows) + headers = [c.value for c in all_rows[0]] + + cases: list[dict] = [] + seen: set[tuple] = set() + + for row in all_rows[1:]: + d = {headers[i]: row[i].value for i in range(len(headers))} + + pull_ref = d.get("Image URL") + cve = d.get("CVE ID") + if not pull_ref or not cve: + continue + pull_ref = str(pull_ref).strip() + cve = str(cve).strip() + + label = _ground_truth_label( + d.get("VEX State (Exception)"), + d.get("VEX Justification (Exception)"), + ) + if label is None: + continue + + dir_name = _dir_name_from_image(pull_ref) + image_name, image_tag = _split_image(pull_ref) + fs_exists = dir_name in available_dirs + filesystem_path = str(data_root_path / dir_name / "filesystem") + image_config_path = str(data_root_path / dir_name / "image_config.json") + + key = (cve, pull_ref) + if key in seen: + continue + seen.add(key) + + cases.append({ + "vuln_id": cve, + "image": pull_ref, + "image_name": image_name, + "image_tag": image_tag, + "filesystem_path": filesystem_path, + "image_config_path": image_config_path, + "ground_truth_label": label, + "package": d.get("Package Name") or "", + "version": d.get("Package Version") or "", + "severity": d.get("Severity") or "", + "_fs_exists": fs_exists, + }) + + return cases + + +# --------------------------------------------------------------------------- +# Sampling +# --------------------------------------------------------------------------- + + +def sample_cases( + cases: list[dict], + n: int | None, + stratify: bool, + seed: int | None, + skip_missing: bool, +) -> list[dict]: + rng = random.Random(seed) + + if skip_missing: + before = len(cases) + cases = [c for c in cases if c["_fs_exists"]] + dropped = before - len(cases) + if dropped: + print(f"Skipped {dropped} case(s) with missing filesystems.", file=sys.stderr) + + if n is None or n >= len(cases): + return cases + + if not stratify: + return rng.sample(cases, n) + + by_label: dict[str, list[dict]] = {} + for c in cases: + by_label.setdefault(c["ground_truth_label"], []).append(c) + + total = len(cases) + label_keys = sorted(by_label.keys()) + exact = {lbl: n * len(by_label[lbl]) / total for lbl in label_keys} + floor_alloc = {lbl: int(exact[lbl]) for lbl in label_keys} + remainder = n - sum(floor_alloc.values()) + order = sorted(label_keys, key=lambda lbl: exact[lbl] - floor_alloc[lbl], reverse=True) + for i in range(remainder): + floor_alloc[order[i]] += 1 + + selected: list[dict] = [] + for lbl in label_keys: + k = floor_alloc[lbl] + pool = by_label[lbl] + selected.extend(rng.sample(pool, min(k, len(pool)))) + + rng.shuffle(selected) + return selected + + +# --------------------------------------------------------------------------- +# CSV output +# --------------------------------------------------------------------------- + + +def write_csv(cases: list[dict], output_path: str) -> None: + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=_CSV_COLUMNS) + writer.writeheader() + for c in cases: + writer.writerow({k: c[k] for k in _CSV_COLUMNS}) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Sample from the PB eval dataset (Excel) and emit a benchmark CSV.", + ) + parser.add_argument( + "--xlsx", + required=True, + help="Path to the Excel file.", + ) + parser.add_argument( + "--sheet", + default=_DEFAULT_SHEET, + help=f"Sheet name to read (default: '{_DEFAULT_SHEET}')", + ) + parser.add_argument( + "--data-root", + default=os.environ.get(_DEFAULT_DATA_ROOT_ENV, _DEFAULT_DATA_ROOT), + help=( + "Root dir with extracted container filesystems " + f"(default: ${_DEFAULT_DATA_ROOT_ENV} or {_DEFAULT_DATA_ROOT})" + ), + ) + parser.add_argument( + "-n", "--n", + type=int, + default=None, + help="Number of cases to sample (default: all)", + ) + parser.add_argument( + "--stratify", + action="store_true", + help="Stratify sample proportionally by ground-truth label.", + ) + parser.add_argument( + "--seed", + type=int, + default=None, + help="Random seed for reproducibility.", + ) + parser.add_argument( + "--skip-missing", + action="store_true", + help="Exclude cases whose filesystem path does not exist on disk.", + ) + parser.add_argument( + "--output", + default=None, + help="Path to write the output CSV (required unless --list is used).", + ) + parser.add_argument( + "--list", + action="store_true", + help="Print all available cases and exit without writing a CSV.", + ) + + args = parser.parse_args() + + if not args.list and not args.output: + parser.error("--output is required unless --list is used") + + cases = load_cases(args.xlsx, args.sheet, args.data_root) + + if args.list: + label_counts: dict[str, int] = {} + for c in cases: + label_counts[c["ground_truth_label"]] = label_counts.get(c["ground_truth_label"], 0) + 1 + total_fs = sum(1 for c in cases if c["_fs_exists"]) + print(f"Total cases with ground truth: {len(cases)}") + print(f"Cases with filesystem available: {total_fs}") + print("Label distribution:") + for lbl, cnt in sorted(label_counts.items(), key=lambda x: -x[1]): + print(f" {lbl}: {cnt}") + print() + print(f"{'CVE/GHSA':<30} {'Image':<70} {'Label':<25} {'Sev':<8} FS") + print("-" * 145) + for c in cases: + fs_ok = "OK" if c["_fs_exists"] else "MISSING" + print(f"{c['vuln_id']:<30} {c['image']:<70} {c['ground_truth_label']:<25} {c['severity']:<8} {fs_ok}") + return 0 + + sampled = sample_cases( + cases, + n=args.n, + stratify=args.stratify, + seed=args.seed, + skip_missing=args.skip_missing, + ) + + write_csv(sampled, args.output) + + label_counts = {} + for c in sampled: + label_counts[c["ground_truth_label"]] = label_counts.get(c["ground_truth_label"], 0) + 1 + + print(f"Wrote {len(sampled)} cases to {args.output}", file=sys.stderr) + print("Label distribution:", file=sys.stderr) + for lbl, cnt in sorted(label_counts.items(), key=lambda x: -x[1]): + print(f" {lbl}: {cnt}", file=sys.stderr) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/vuln_analysis/eval/visualizations/box_and_whisker_plot.py b/src/vuln_analysis/eval/visualizations/box_and_whisker_plot.py deleted file mode 100644 index 7e1c3befd..000000000 --- a/src/vuln_analysis/eval/visualizations/box_and_whisker_plot.py +++ /dev/null @@ -1,131 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import json -import os - -import matplotlib.pyplot as plt -""" -Box-and-whisker plot utility for evaluator outputs. - -Example usage: - - python3 src/vuln_analysis/eval/visualizations/box_and_whisker_plot.py \ - .tmp/evaluators/llama8b-experiment/status_accuracy_output.json \ - .tmp/evaluators/llama70b-experiment/status_accuracy_output.json \ - --title "Status Accuracy: llama8b vs llama70b" \ - --save src/vuln_analysis/eval/visualizations/plot.png \ - --labels llama8b llama70b -""" - - -def load_box_values(path: str) -> tuple[list[float], str]: - """Load average_score as [mean, min, q1, median, q3, max] from accuracy evaluator output JSON. - Returns (values, label) where label is the input file. - """ - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - - avg = data.get("average_score") - if not isinstance(avg, dict): - raise ValueError(f"File '{path}' does not contain pandas describe() format. Expecting dict in 'average_score'.") - - # Extract values from pandas describe() format: [mean, min, q1, median, q3, max] - values = [ - float(avg.get("mean", 0)), - float(avg.get("min", 0)), - float(avg.get("25%", 0)), - float(avg.get("50%", 0)), - float(avg.get("75%", 0)), - float(avg.get("max", 0)) - ] - - label = os.path.splitext(os.path.basename(path))[0] - return values, label - - -def main() -> int: - parser = argparse.ArgumentParser(description=( - "Plot box-and-whisker charts from evaluator output JSON files (average_score = [mean, min, q1, median, q3, max])." - )) - parser.add_argument("files", nargs="+", help="Paths to output files") - parser.add_argument("--title", default="Accuracy Distribution Across Runs", help="Chart title.") - parser.add_argument("--save", - metavar="OUTPUT_PATH", - help="If provided, save the figure to this path instead of displaying it.") - parser.add_argument("--dpi", type=int, default=160, help="Figure DPI when saving (default: 160).") - parser.add_argument( - "--labels", - nargs='*', - help="Optional custom x-axis labels (one per file, in order). If omitted, .json file names are used.") - - args = parser.parse_args() - - # Load all files - stats_values: list[dict] = [] - means_overlay: list[tuple[float, float]] = [] - labels: list[str] = [] - - for idx, path in enumerate(args.files): - values, default_label = load_box_values(path) - mean_value, min_value, q1, median, q3, max_value = values - - # Matplotlib boxplot stats dictionary - stats_values.append({'whislo': min_value, 'q1': q1, 'med': median, 'q3': q3, 'whishi': max_value, 'fliers': []}) - if args.labels and len(args.labels) == len(args.files): - labels.append(args.labels[idx]) - else: - labels.append(default_label) - means_overlay.append((len(labels), mean_value)) - - if not stats_values: - print("No valid files provided.") - return 1 - - # Plot - fig, ax = plt.subplots(figsize=(max(6, 1.8 * len(stats_values)), 5)) - - # Create boxplot from precomputed stats - bp = ax.bxp(stats_values, showfliers=False) - - # Overlay means as black diamonds - for x_pos, mean_value in means_overlay: - ax.plot(x_pos, mean_value, marker='D', color='black', linestyle='None', label='_nolegend_') - - ax.set_title(args.title) - ax.set_ylabel('Accuracy') - ax.set_xticks(range(1, len(labels) + 1)) - ax.set_xticklabels(labels, rotation=20, ha='right') - ax.set_ylim(0.0, 1.0) - ax.grid(axis='y', linestyle='--', alpha=0.3) - - # Legend entry for mean marker - ax.plot([], [], marker='D', color='black', linestyle='None', label='Mean') - ax.legend() - - fig.tight_layout() - - if args.save: - fig.savefig(args.save, dpi=args.dpi, bbox_inches='tight') - print(f"Saved figure to {args.save}") - else: - plt.show() - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/vuln_analysis/functions/__init__.py b/src/vuln_analysis/functions/__init__.py index cf7c586a5..e69de29bb 100644 --- a/src/vuln_analysis/functions/__init__.py +++ b/src/vuln_analysis/functions/__init__.py @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/src/vuln_analysis/functions/container_analyzer.py b/src/vuln_analysis/functions/container_analyzer.py new file mode 100644 index 000000000..e7e146d32 --- /dev/null +++ b/src/vuln_analysis/functions/container_analyzer.py @@ -0,0 +1,153 @@ +"""Container Analyzer agent function for NAT.""" + +import logging +from pathlib import Path + +from langchain_core.messages import HumanMessage, SystemMessage +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import FunctionRef +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +from vuln_analysis.data_models.container_analyzer import ( + ContainerAnalyzerInput, + ContainerAnalyzerResult, +) +from vuln_analysis.prompts.container_analyzer import PHASE2_SYSTEM_PROMPT, build_user_message +from vuln_analysis.tools.filesystem_tools import configure_allowed_paths +from vuln_analysis.utils.agentic_loop import run_agentic_loop +from vuln_analysis.utils.concurrency import make_lazy_rate_limiter +from vuln_analysis.utils.web_researcher import run_web_research + +logger = logging.getLogger(__name__) + +_DEFAULT_PUBLIC_PREFIXES = [ + "nvcr.io/", "docker.io/", "gcr.io/", "ghcr.io/", "registry.hub.docker.com/", +] + + +class ContainerAnalyzerConfig(FunctionBaseConfig, name="container_analyzer"): + phase1_llm_name: str = Field(description="LLM for Phase 1 web research") + phase2_llm_name: str = Field(description="LLM for Phase 2 filesystem analysis") + web_tool_names: list[FunctionRef] = Field(description="Phase 1 tools: web_search, web_fetch") + fs_tool_names: list[FunctionRef] = Field(description="Phase 2 tools: filesystem tools") + phase1_max_iterations: int = Field(default=8) + phase2_max_iterations: int = Field(default=40) + context_window_tokens: int = Field(default=262144) + wrap_up_fraction: float = Field(default=0.80) + loop_detection_threshold: int = Field(default=2) + llm_max_rate: int | None = Field( + default=None, + description="Per-function LLM rate limit (requests/second). Overrides the workflow setting.", + ) + public_registry_prefixes: list[str] = Field(default=_DEFAULT_PUBLIC_PREFIXES) + output_subdir: str = Field(default="container_reports") + description: str = Field(default="Analyzes a container image and produces a security report") + + +@register_function(config_type=ContainerAnalyzerConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def container_analyzer(config: ContainerAnalyzerConfig, builder: Builder): + phase1_llm = await builder.get_llm(llm_name=config.phase1_llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + phase2_llm = await builder.get_llm(llm_name=config.phase2_llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + web_tools = await builder.get_tools(tool_names=config.web_tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + fs_tools = await builder.get_tools(tool_names=config.fs_tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + get_rate_limiter = make_lazy_rate_limiter(config.llm_max_rate, builder) + + async def _arun(inp: ContainerAnalyzerInput) -> ContainerAnalyzerResult: + rate_limiter = get_rate_limiter() + pull_ref = inp.pull_ref + logger.info("Starting container analysis for %s", pull_ref) + + if not Path(inp.filesystem_path).is_dir(): + return ContainerAnalyzerResult( + image_name=inp.image_name, image_tag=inp.image_tag, success=False, + error=f"Filesystem directory not found: {inp.filesystem_path}", + ) + + image_config_content: str | None = None + if inp.image_config_path: + if not Path(inp.image_config_path).is_file(): + return ContainerAnalyzerResult( + image_name=inp.image_name, image_tag=inp.image_tag, success=False, + error=f"Image config file not found: {inp.image_config_path}", + ) + try: + image_config_content = Path(inp.image_config_path).read_text(encoding="utf-8") + except Exception as exc: + logger.warning("Failed to pre-read image config: %s", exc) + + phase1_skipped = True + web_research_context: str | None = None + + should_web_research = ( + not inp.skip_web_research + and any(pull_ref.startswith(p) for p in config.public_registry_prefixes) + ) + + if should_web_research: + phase1_skipped = False + logger.info("Phase 1: running web research for public image") + web_research_context = await run_web_research( + phase1_llm, web_tools, pull_ref, + max_iterations=config.phase1_max_iterations, + rate_limiter=rate_limiter, + ) + else: + logger.info("Phase 1: skipped") + + logger.info("Phase 2: starting filesystem analysis") + configure_allowed_paths(roots=[inp.filesystem_path]) + + messages = [ + SystemMessage(content=PHASE2_SYSTEM_PROMPT), + HumanMessage(content=build_user_message(inp, web_research_context, image_config_content)), + ] + + try: + content, tool_calls, llm_calls = await run_agentic_loop( + llm=phase2_llm, + messages=messages, + tools=fs_tools, + max_iterations=config.phase2_max_iterations, + context_window_tokens=config.context_window_tokens, + wrap_up_fraction=config.wrap_up_fraction, + loop_threshold=config.loop_detection_threshold, + phase_label="Phase 2", + rate_limiter=rate_limiter, + ) + except Exception as exc: + logger.error("Phase 2 failed: %s", exc, exc_info=True) + return ContainerAnalyzerResult( + image_name=inp.image_name, image_tag=inp.image_tag, success=False, error=str(exc), + phase1_skipped=phase1_skipped, + ) + + if not content: + return ContainerAnalyzerResult( + image_name=inp.image_name, image_tag=inp.image_tag, success=False, + error="No report content produced by the LLM.", + phase1_skipped=phase1_skipped, + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + logger.info("Analysis complete: %s (%d chars, %d tool calls, %d LLM calls)", + pull_ref, len(content), tool_calls, llm_calls) + return ContainerAnalyzerResult( + image_name=inp.image_name, image_tag=inp.image_tag, report_content=content, success=True, + phase1_skipped=phase1_skipped, + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + def _str_to_input(s: str) -> ContainerAnalyzerInput: + return ContainerAnalyzerInput.model_validate_json(s) + + def _result_to_str(r: ContainerAnalyzerResult) -> str: + return r.model_dump_json() + + yield FunctionInfo.from_fn( + _arun, input_schema=ContainerAnalyzerInput, description=config.description, + converters=[_str_to_input, _result_to_str], + ) diff --git a/src/vuln_analysis/functions/custom_vex_categorizer.py b/src/vuln_analysis/functions/custom_vex_categorizer.py new file mode 100644 index 000000000..12d343d52 --- /dev/null +++ b/src/vuln_analysis/functions/custom_vex_categorizer.py @@ -0,0 +1,154 @@ +"""VEX Categorizer agent function for NAT that uses a custom user-provided prompt.""" + +import logging +from pathlib import Path + +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.tools import tool +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field, model_validator + +from vuln_analysis.data_models.vex_categorizer import ( + VEXCategorizerInput, + VEXCategorizerResult, + parse_report_filename, +) +from vuln_analysis.prompts.vex_categorizer import build_user_message +from vuln_analysis.utils.agentic_loop import run_agentic_loop +from vuln_analysis.utils.helpers import truncate +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + + +@tool +def reason(step: str, analysis: str, conclusion: str) -> str: + """Record step-by-step reasoning for a VEX decision flowchart step.""" + logger.info("reason: step=%s, conclusion=%s", step, truncate(conclusion, 120)) + return ( + f"Recorded. Step: {step} → Conclusion: {conclusion}\n" + "Continue to the next VEX decision step, or write the outputs " + "if you have reached a final determination." + ) + + +class VEXCategorizerConfig(FunctionBaseConfig, name="custom_vex_categorizer"): + llm_name: str = Field(description="LLM for relaxed VEX classification") + prompt: str = Field(description="System prompt for a custom VEX categorizer.") + max_iterations: int = Field(default=10) + loop_detection_threshold: int = Field(default=2) + output_subdir: str = Field(default="vex_results") + description: str = Field(default="Categorizes VEX status from exploitability and container reports using a custom prompt.") + + @model_validator(mode="after") + def _check_ids(self): + if not self.prompt: + raise ValueError("prompt must be provided.") + if not Path(self.prompt).is_file(): + raise ValueError(f"Prompt file not found: {self.prompt}") + return self + +@register_function(config_type=VEXCategorizerConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def custom_vex_categorizer(config: VEXCategorizerConfig, builder: Builder): + llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + vex_tools = [reason] + + async def _arun(inp: VEXCategorizerInput) -> VEXCategorizerResult: + for label, path in [ + ("Exploitability report", inp.exploitability_report_path), + ("Container report", inp.container_report_path), + ]: + if not Path(path).is_file(): + return VEXCategorizerResult( + cve_id="unknown", container_name="unknown", + success=False, error=f"{label} not found: {path}", + ) + + cve_id, container_name = parse_report_filename(inp.exploitability_report_path) + + exploit_content = _read_report(inp.exploitability_report_path) + container_content = _read_report(inp.container_report_path) + if exploit_content is None or container_content is None: + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + success=False, error="Failed to read one or both input reports.", + ) + + logger.info("Starting relaxed VEX categorization: %s × %s", cve_id, container_name) + custom_prompt = Path(config.prompt).read_text(encoding="utf-8") + + messages = [ + SystemMessage(content=custom_prompt), + HumanMessage(content=build_user_message( + exploitability_report_path=inp.exploitability_report_path, + exploitability_report_content=exploit_content, + container_report_path=inp.container_report_path, + container_report_content=container_content, + cve_id=cve_id, + container_name=container_name, + )), + ] + + try: + content, tool_calls, llm_calls = await run_agentic_loop( + llm=llm, + messages=messages, + tools=vex_tools, + max_iterations=config.max_iterations, + context_window_tokens=200_000, + loop_threshold=config.loop_detection_threshold, + phase_label="Relaxed VEX Categorizer", + ) + except Exception as exc: + logger.error("Relaxed VEX Categorizer failed: %s", exc, exc_info=True) + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + success=False, error=str(exc), + ) + + if not content: + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + success=False, error="No VEX summary report produced.", + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + status = extract_markdown_table_field(content, "Status") + justification = extract_markdown_table_field(content, "Justification") + reasoning = extract_markdown_table_field(content, "Reasoning") + + logger.info("Relaxed VEX categorization complete: %s × %s → status=%s, justification=%s", + cve_id, container_name, status, justification) + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + status=status, justification=justification, reasoning=reasoning, + report_content=content, success=True, + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + def _str_to_input(s: str) -> VEXCategorizerInput: + return VEXCategorizerInput.model_validate_json(s) + + def _result_to_str(r: VEXCategorizerResult) -> str: + return r.model_dump_json() + + yield FunctionInfo.from_fn( + _arun, input_schema=VEXCategorizerInput, description=config.description, + converters=[_str_to_input, _result_to_str], + ) + + +def _read_report(path: str) -> str | None: + try: + return Path(path).read_text(encoding="utf-8") + except UnicodeDecodeError: + try: + return Path(path).read_text(encoding="latin-1") + except Exception: + return None + except Exception: + return None diff --git a/src/vuln_analysis/functions/cve_agent.py b/src/vuln_analysis/functions/cve_agent.py deleted file mode 100644 index 44f241dbc..000000000 --- a/src/vuln_analysis/functions/cve_agent.py +++ /dev/null @@ -1,276 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import contextvars -import logging -import typing -import warnings -from typing import Any - -from aiolimiter import AsyncLimiter -from langchain.agents import AgentExecutor -from langchain.agents import create_react_agent -from langchain.agents.agent import RunnableAgent -from langchain_core.callbacks import AsyncCallbackHandler -from langchain_core.exceptions import OutputParserException -from langchain_core.prompts import PromptTemplate -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field -from pydantic import model_validator - -from vuln_analysis.data_models.state import AgentMorpheusEngineState -from vuln_analysis.utils.concurrency import get_effective_rate_limiter -from vuln_analysis.utils.prompting import get_agent_prompt - -logger = logging.getLogger(__name__) - -# Context variables shared between agent and its tools: -# - ctx_state: Provides tools access to workflow state (VDB paths, code index paths, CVE intel) -# - ctx_rate_limiter: Enables tools to use same LLM rate limit as the agent -ctx_state = contextvars.ContextVar("ctx_state", default="default_value") -ctx_rate_limiter = contextvars.ContextVar("ctx_rate_limiter", default=None) - - -class RateLimitingCallback(AsyncCallbackHandler): - """ - LangChain callback handler that applies rate limiting to LLM calls. - This ensures that the rate limit is respected even when the LLM is called multiple times - by an agent during its execution loop. - """ - - def __init__(self, rate_limiter: AsyncLimiter | None): - super().__init__() - self.rate_limiter = rate_limiter - - async def on_llm_start(self, serialized: dict[str, Any], prompts: list[str], **kwargs: Any) -> None: - """Apply rate limiting before each LLM call starts.""" - if self.rate_limiter is not None: - await self.rate_limiter.acquire() - - -class CVEAgentExecutorToolConfig(FunctionBaseConfig, name="cve_agent_executor"): - """ - Defines a function that iterates through checklist items using provided tools and gathered intel. - """ - llm_name: str = Field(description="The LLM model to use with the CVE agent.") - tool_names: list[str] = Field(default=[], description="The list of tools to provide to CVE agent.") - llm_max_rate: int | None = Field( - default=None, - description="Maximum LLM rate limit (requests per second) for all LLM calls made by the agent or its tools. " - "If set to a number, overrides the workflow-level llm_max_rate. If None, inherits from workflow-level setting. " - "Takes precedence over the deprecated max_concurrency parameter if both are specified.") - max_concurrency: int | None = Field( - default=None, - description="DEPRECATED: Use llm_max_rate instead. " - "This parameter is now treated as llm_max_rate (requests/second) for rate limiting, " - "not as a concurrent operation limit. This behavior change may affect performance. " - "If both max_concurrency and llm_max_rate are specified, llm_max_rate takes precedence. " - "This parameter will be removed in a future version.") - max_iterations: int = Field(default=10, description="The maximum number of iterations for the agent.") - prompt: str | None = Field( - default=None, - description= - "Manually set the prompt for the specific model in the configuration. The prompt can either be passed in as a " - "string of text or as a path to a text file containing the desired prompting.") - prompt_examples: bool = Field(default=False, description="Whether to include examples in agent prompt.") - replace_exceptions: bool = Field(default=False, - description="Whether to replace exception message with custom message.") - replace_exceptions_value: str | None = Field(default=None, description="Message if replace_exceptions is true") - return_intermediate_steps: bool = Field( - default=False, - description= - "Controls whether to return intermediate steps taken by the agent, and include them in the output file.") - verbose: bool = Field(default=False, description="Set to true for verbose output") - - @model_validator(mode="after") - def validate_max_concurrency_deprecated(self): - """Warn if max_concurrency is used and suggest llm_max_rate instead.""" - if self.max_concurrency is not None: - if self.llm_max_rate is not None: - # Both parameters specified - llm_max_rate takes precedence - msg = ( - f"The 'max_concurrency' parameter is deprecated and will be removed in a future version. " - f"Both max_concurrency={self.max_concurrency} and llm_max_rate={self.llm_max_rate} are specified. " - f"Using llm_max_rate={self.llm_max_rate} (max_concurrency will be ignored). " - f"Please remove max_concurrency from your configuration.") - else: - # Only max_concurrency specified - msg = ("The 'max_concurrency' parameter is deprecated and will be removed in a future version. " - "Please use 'llm_max_rate' (requests per second) instead for better rate limiting control.") - - # Use logging for guaranteed visibility - logger.warning(msg) - - # Also emit proper deprecation warning for tooling/static analysis - warnings.warn(msg, DeprecationWarning, stacklevel=1) - return self - - -async def _create_agent(config: CVEAgentExecutorToolConfig, - builder: Builder, - state: AgentMorpheusEngineState, - rate_limiter: AsyncLimiter | None) -> tuple[AgentExecutor, list]: - tools = await builder.get_tools(tool_names=config.tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - prompt = PromptTemplate.from_template(get_agent_prompt(config.prompt, config.prompt_examples)) - - # Create rate limiting callback to control all LLM calls made by the agent - callbacks = [RateLimitingCallback(rate_limiter)] if rate_limiter is not None else [] - - # Filter tools that are not available - tools = [ - tool for tool in tools - if not ((tool.name == "Container Image Code QA System" and state.code_vdb_path is None) or - (tool.name == "Container Image Developer Guide QA System" and state.doc_vdb_path is None) or - (tool.name == "Lexical Search Container Image Code QA System" and state.code_index_path is None)) - ] - - agent = create_react_agent(llm=llm, - tools=tools, - prompt=prompt, - stop_sequence=["\nObservation:", "\n\tObservation:"]) - - agent_executor = AgentExecutor( - agent=agent, - tools=tools, - early_stopping_method="force", - handle_parsing_errors="Check your output and make sure it conforms, use the Action/Action Input syntax", - max_iterations=config.max_iterations, - return_intermediate_steps=config.return_intermediate_steps, - verbose=config.verbose) - - # Disable streaming for accurate token counts - if isinstance(agent_executor.agent, RunnableAgent): - agent_executor.agent.stream_runnable = False - - return agent_executor, callbacks - - -async def _process_steps(agent, steps, callbacks): - - async def _process_step(step): - try: - return await agent.ainvoke({"input": step}, config={"callbacks": callbacks}) - except Exception as e: - logger.error("Error in agent execution: %s: %s", type(e).__name__, e) - raise - - return await asyncio.gather(*(_process_step(step) for step in steps)) - - -def _parse_intermediate_step(step: tuple[typing.Any, typing.Any]) -> dict[str, typing.Any]: - """ - Parse an agent intermediate step into an AgentIntermediateStep object. Return the dictionary representation for - compatibility with cudf. - """ - if len(step) != 2: - raise ValueError(f"Expected 2 values in each intermediate step but got {len(step)}.") - - action, output = step - - return {"tool_name": action.tool, "action_log": action.log, "tool_input": action.tool_input, "tool_output": output} - - -def _postprocess_results(results: list[list[dict]], replace_exceptions: bool, - replace_exceptions_value: str | None) -> tuple[list[list[str]], list[list[list]]]: - """ - Post-process results into lists of outputs and intermediate steps. Replace exceptions with placholder values if - config.replace_exceptions = True. - """ - - for i, answer_list in enumerate(results): - for j, answer in enumerate(answer_list): - - # Handle exceptions returned by the agent - # OutputParserException is not a subclass of Exception, so we need to check for it separately - if isinstance(answer, (OutputParserException, Exception)): - if replace_exceptions: - # If the agent encounters a parsing error or a server error after retries, replace the error - # with default values to prevent the pipeline from crashing - results[i][j]["output"] = replace_exceptions_value - results[i][j]["intermediate_step"] = None - logger.warning( - "Error in agent execution for result[%d][%d]: %s. " - "Replacing with default output: \"%s\" and intermediate_steps: None", - i, - j, - answer, - replace_exceptions_value) - - # For successful agent responses, extract the output, and intermediate steps if available - else: - # intermediate_steps availability depends on config.return_intermediate_steps - if "intermediate_steps" in answer: - results[i][j]["intermediate_steps"] = [ - _parse_intermediate_step(step) for step in answer["intermediate_steps"] - ] - else: - results[i][j]["intermediate_steps"] = None - - return results - - -@register_function(config_type=CVEAgentExecutorToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_agent(config: CVEAgentExecutorToolConfig, builder: Builder): - - async def _arun(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - ctx_state.set(state) - - # Handle deprecated max_concurrency parameter - effective_max_rate = config.llm_max_rate - if effective_max_rate is None and config.max_concurrency is not None: - # For backward compatibility, convert max_concurrency to llm_max_rate (requests per second) - # NOTE: This changes behavior from concurrency limiting to rate limiting - effective_max_rate = config.max_concurrency - logger.warning( - "Deprecated max_concurrency=%d is now treated as llm_max_rate=%d requests/second (rate limiting). " - "This changes from concurrent operation limiting and may affect performance. " - "Please update your configuration to use llm_max_rate.", - config.max_concurrency, - effective_max_rate) - - rate_limiter = get_effective_rate_limiter(effective_max_rate, builder) - - # Store the rate limiter in context so tools can create their own callbacks - ctx_rate_limiter.set(rate_limiter) - - # Create agent with rate-limiting callback - agent, callbacks = await _create_agent(config, builder, state, rate_limiter) - - if rate_limiter is not None: - effective_rate = int(rate_limiter.max_rate / rate_limiter.time_period) - logger.info("Executing agent for %d CVEs with LLM rate limit of %d requests/second", - len(state.checklist_plans), - effective_rate) - else: - logger.info("Executing agent for %d CVEs with no rate limiting", len(state.checklist_plans)) - - # Process all CVEs and their steps in parallel - the rate limiter callback will control the actual LLM request rate - results = await asyncio.gather(*(_process_steps(agent, steps, callbacks) - for steps in state.checklist_plans.values())) - results = _postprocess_results(results, config.replace_exceptions, config.replace_exceptions_value) - state.checklist_results = dict(zip(state.checklist_plans.keys(), results)) - return state - - yield FunctionInfo.from_fn( - _arun, - input_schema=AgentMorpheusEngineState, - description=("Executes provided checklist of tasks mapped to flagged CVEs to investigate the " - "exploitability of a software container by the flagged CVEs.")) diff --git a/src/vuln_analysis/functions/cve_check_vuln_deps.py b/src/vuln_analysis/functions/cve_check_vuln_deps.py deleted file mode 100644 index 66c5f199c..000000000 --- a/src/vuln_analysis/functions/cve_check_vuln_deps.py +++ /dev/null @@ -1,138 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging - -import aiohttp -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -logger = logging.getLogger(__name__) - - -class CVEVulnerableDepsChecksConfig(FunctionBaseConfig, name="cve_check_vuln_deps"): - """ - Defines a function that cross-references every entry in the SBOM for known vulnerabilities. - """ - retry_on_client_errors: bool = Field(default=True, description="Whether to retry if HTTP client cannot connect") - skip: bool = Field(default=False, - description="Whether or not the vulnerability dependency checker should be skipped.") - - -@register_function(config_type=CVEVulnerableDepsChecksConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_check_vuln_deps(config: CVEVulnerableDepsChecksConfig, builder: Builder): # pylint: disable=unused-argument - - from vuln_analysis.data_models.cve_intel import CveIntel - from vuln_analysis.data_models.dependencies import VulnerableDependencies - from vuln_analysis.data_models.dependencies import VulnerableSBOMPackage - from vuln_analysis.data_models.input import AgentMorpheusEngineInput - from vuln_analysis.utils.vulnerable_dependency_checker import VulnerableDependencyChecker - - async def _arun(message: AgentMorpheusEngineInput) -> AgentMorpheusEngineInput: - - sbom = message.info.sbom.packages - image = f"{message.input.image.name}:{message.input.image.tag}" - - if config.skip: - logger.info("`config.skip` is set to True. Skipping vulnerable dependency check for image %s.", image) - message.info.vulnerable_dependencies = None - return message - - if not sbom: - logger.warning("No SBOM packages found for image %s. Skipping vulnerable dependency check.", image) - message.info.vulnerable_dependencies = [ - VulnerableDependencies(vuln_id=vuln.vuln_id, vuln_package_intel_sources=[], vulnerable_sbom_packages=[]) - for vuln in message.input.scan.vulns - ] - return message - - async def _inner(): - - async with aiohttp.ClientSession() as session: - - vuln_dep_checker = VulnerableDependencyChecker(session=session, - image=image, - sbom_list=sbom, - retry_on_client_errors=config.retry_on_client_errors) - - await vuln_dep_checker.load_dependencies() - - async def _calc_dep(cve_intel: CveIntel): - vuln_id = cve_intel.vuln_id - vuln_deps = [] - vuln_package_intel_sources = [] - - # Check vulnerabilities from GHSA - if (cve_intel.ghsa is not None and cve_intel.ghsa.vulnerabilities): - vuln_package_intel_sources.append("ghsa") - vuln_deps.extend(await vuln_dep_checker.run_ghsa(cve_intel.ghsa.vulnerabilities)) - - # Check vulnerabilities from NVD - if (cve_intel.nvd is not None and cve_intel.nvd.configurations): - vuln_package_intel_sources.append("nvd") - vuln_deps.extend(await vuln_dep_checker.run_nvd(cve_intel.nvd.configurations)) - - if not vuln_package_intel_sources: - logger.warning("No vulnerabilities were found in either GHSA or NVD intel for %s.", vuln_id) - - # Check vulnerabilities from Ubuntu notices - if (cve_intel.ubuntu is not None and hasattr(cve_intel.ubuntu, 'notices') - and cve_intel.ubuntu.notices): - vuln_package_intel_sources.append("ubuntu") - vuln_deps.extend(await vuln_dep_checker.run_ubuntu(cve_intel.ubuntu.notices)) - - # Check vulnerabilities from RHSA - if (cve_intel.rhsa is not None and hasattr(cve_intel.rhsa, 'affected_release') - and cve_intel.rhsa.affected_release): - vuln_package_intel_sources.append("rhsa") - vuln_deps.extend(await vuln_dep_checker.run_rhsa(cve_intel.rhsa.affected_release)) - - # Create list of vulnerable SBOM packages and the related vulnerable dependency for the CVE - vulnerable_sbom_packages: list[VulnerableSBOMPackage] = [] - for vuln_dep in vuln_deps: - for (sbom_pkg_name, sbom_pkg_version), vuln_dep_pkg in vuln_dep.items(): - vulnerable_sbom_packages.append( - VulnerableSBOMPackage(name=sbom_pkg_name, - version=sbom_pkg_version, - vulnerable_dependency_package=vuln_dep_pkg)) - - if len(vulnerable_sbom_packages) > 0: - logger.info("Found vulnerable dependencies for %s.", vuln_id) - else: - logger.info("No vulnerable dependencies found for %s.", vuln_id) - - # Add the vulnerable dependencies for this CVE to the overall list - return VulnerableDependencies(vuln_id=vuln_id, - vuln_package_intel_sources=vuln_package_intel_sources, - vulnerable_sbom_packages=vulnerable_sbom_packages) - - # Check vulnerable dependencies for each CVE - vulnerable_dependencies: list[VulnerableDependencies] = await asyncio.gather( - *[_calc_dep(cve_intel) for cve_intel in message.info.intel]) - - return vulnerable_dependencies - - # Update the message info with the vulnerable dependencies list - message.info.vulnerable_dependencies = await _inner() - return message - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusEngineInput, - description=("Cross-references every entry in the SBOM for known vulnerabilities.")) diff --git a/src/vuln_analysis/functions/cve_checklist.py b/src/vuln_analysis/functions/cve_checklist.py deleted file mode 100644 index b81ba834c..000000000 --- a/src/vuln_analysis/functions/cve_checklist.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging - -import pandas as pd -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -logger = logging.getLogger(__name__) - - -class CVEChecklistToolConfig(FunctionBaseConfig, name="cve_checklist"): - """ - Defines a function that generates tailored, context-sensitive task checklist for impact analysis. - """ - llm_name: str = Field(description="The LLM model to use") - prompt: str | None = Field( - default=None, - description= - "Manually set the prompt for the specific model in the configuration. The prompt can either be passed in as a " - "string of text or as a path to a text file containing the desired prompting.") - llm_max_rate: int | None = Field( - default=None, - description="Maximum LLM rate limit (requests per second) for checklist generation tasks. " - "If set to a number, overrides the workflow-level llm_max_rate. If None, inherits from workflow-level setting.") - - -@register_function(config_type=CVEChecklistToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_checklist(config: CVEChecklistToolConfig, builder: Builder): - - from vuln_analysis.data_models.state import AgentMorpheusEngineState - from vuln_analysis.utils.checklist_prompt_generator import _parse_list - from vuln_analysis.utils.checklist_prompt_generator import generate_checklist - from vuln_analysis.utils.concurrency import get_effective_rate_limiter - - llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - - async def generate_checklist_for_cve(cve_intel): - - checklist = await generate_checklist(prompt=config.prompt, - llm=llm, - input_dict=cve_intel, - enable_llm_list_parsing=False) - - checklist = await _parse_list([checklist]) - - return cve_intel["vuln_id"], checklist[0] - - async def _arun(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - intel_df = pd.json_normalize([x.model_dump(mode="json") for x in state.cve_intel], sep="_") - workflow_cve_intel = intel_df.to_dict(orient='records') - - rate_limiter = get_effective_rate_limiter(config.llm_max_rate, builder) - - if rate_limiter is not None: - # Use rate limiter to control the rate of requests to the LLM API - async def generate_with_rate_limit(cve_intel): - async with rate_limiter: - return await generate_checklist_for_cve(cve_intel) - - effective_rate = int(rate_limiter.max_rate / rate_limiter.time_period) - logger.info("Generating checklists for %d CVEs with LLM rate limit of %d requests/second", - len(workflow_cve_intel), - effective_rate) - results = await asyncio.gather(*(generate_with_rate_limit(cve_intel) for cve_intel in workflow_cve_intel)) - else: - logger.info("Generating checklists for %d CVEs with no rate limiting", len(workflow_cve_intel)) - results = await asyncio.gather(*(generate_checklist_for_cve(cve_intel) for cve_intel in workflow_cve_intel)) - - state.checklist_plans = dict(results) - return state - - yield FunctionInfo.from_fn( - _arun, - input_schema=AgentMorpheusEngineState, - description=("Generates tailored, context-sensitive task checklist for impact analysis.")) diff --git a/src/vuln_analysis/functions/cve_fetch_intel.py b/src/vuln_analysis/functions/cve_fetch_intel.py deleted file mode 100644 index 5bed67a42..000000000 --- a/src/vuln_analysis/functions/cve_fetch_intel.py +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging - -import aiohttp -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -logger = logging.getLogger(__name__) - - -class CVEFetchIntelConfig(FunctionBaseConfig, name="cve_fetch_intel"): - """ - Defines a function that fetches details about CVEs from NIST and CVE Details websites. - """ - max_retries: int = Field(default=5, description="Maximum number of retries on client and server errors") - retry_on_client_errors: bool = Field(default=True, description="Whether to retry on client errors") - request_timeout: int = Field(default=30, description="Timeout for individual HTTP requests in seconds") - intel_source_timeout: int | None = Field( - default=None, - description="Timeout for each intel source (across all retries) in seconds. None means no timeout.") - - -@register_function(config_type=CVEFetchIntelConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_fetch_intel(config: CVEFetchIntelConfig, builder: Builder): # pylint: disable=unused-argument - - from vuln_analysis.data_models.input import AgentMorpheusEngineInput - from vuln_analysis.utils.intel_retriever import IntelRetriever - - async def _arun(message: AgentMorpheusEngineInput) -> AgentMorpheusEngineInput: - - async def _inner(): - - timeout = aiohttp.ClientTimeout(total=config.request_timeout) - async with aiohttp.ClientSession(timeout=timeout) as session: - - intel_retriever = IntelRetriever(session=session, - max_retries=config.max_retries, - retry_on_client_errors=config.retry_on_client_errors, - intel_source_timeout=config.intel_source_timeout, - request_timeout=config.request_timeout) - - intel_coros = [intel_retriever.retrieve(vuln_id=cve.vuln_id) for cve in message.input.scan.vulns] - - intel_objs = await asyncio.gather(*intel_coros) - - return intel_objs - - result = await _inner() - - message.info.intel = result - - return message - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusEngineInput, - description=("Fetches details about CVEs from NIST and CVE Details websites.")) diff --git a/src/vuln_analysis/functions/cve_file_output.py b/src/vuln_analysis/functions/cve_file_output.py deleted file mode 100644 index 3dd5fa0c3..000000000 --- a/src/vuln_analysis/functions/cve_file_output.py +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -logger = logging.getLogger(__name__) - - -class CVEFileOutputConfig(FunctionBaseConfig, name="cve_file_output"): - """ - Defines a function that outputs workflow results to a file. - """ - file_path: str | None = Field(default="./.tmp/output.json", - description="Defines the path to the file where the output will be saved.") - markdown_dir: str | None = Field( - default="./.tmp/vulnerability_markdown_reports", - description= - "Defines the path to the directory where the output will be saved in individual navigable markdown files " - "per CVE-ID.") - overwrite: bool = Field( - default=False, - description= - "Indicates whether the output file should be overwritten when the pipeline starts if it already exists.") - - -@register_function(config_type=CVEFileOutputConfig) -async def output_to_file(config: CVEFileOutputConfig, builder: Builder): # pylint: disable=unused-argument - - import os - from pathlib import Path - - from vuln_analysis.data_models.output import AgentMorpheusOutput - from vuln_analysis.utils.output_formatter import generate_vulnerability_reports - - if (os.path.exists(config.file_path)): - if (config.overwrite): - logger.warning("Overwriting existing file: %s", config.file_path) - os.remove(config.file_path) - else: - logger.info("Appending to existing file: %s", config.file_path) - else: - # Ensure our directory exists - os.makedirs(os.path.realpath(os.path.dirname(config.file_path)), exist_ok=True) - - async def _arun(message: AgentMorpheusOutput) -> AgentMorpheusOutput: - - file_path = Path(config.file_path) - - # Write file depending on setting for overwrite - model_json = message.model_dump_json(by_alias=True) - - file_path.parent.mkdir(parents=True, exist_ok=True) - - with open(file_path, 'a', encoding='utf-8') as f: - f.write(model_json) - f.write("\n") - - if config.markdown_dir is not None: - generate_vulnerability_reports(message, config.markdown_dir) - - return message - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusOutput, - description=("Outputs workflow results to a file.")) diff --git a/src/vuln_analysis/functions/cve_generate_vdbs.py b/src/vuln_analysis/functions/cve_generate_vdbs.py deleted file mode 100644 index 3a946057a..000000000 --- a/src/vuln_analysis/functions/cve_generate_vdbs.py +++ /dev/null @@ -1,278 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging -import os -import time -from pathlib import Path - -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.component_ref import FunctionRef -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -from vuln_analysis.utils.error_utils import API_KEY_SETUP_LINK -from vuln_analysis.utils.error_utils import is_authentication_error - -logger = logging.getLogger(__name__) - - -class CVEGenerateVDBsToolConfig(FunctionBaseConfig, name="cve_generate_vdbs"): - """ - Defines a function that generates a vector database from code repositories and documentation. - """ - agent_name: FunctionRef = Field( - description= - "Name of the agent executor. Used to determine which tools are enabled in the agent to conditionally generate " - "vector databases or indexes.") - embedder_name: str = Field(description="Name of embedder to use.") - base_git_dir: str = Field(default=".cache/am_cache/git", - description="The directory for storing pulled git repositories used for code analysis.") - base_vdb_dir: str = Field(default=".cache/am_cache/vdb", - description="The directory used for storing vector database files.") - base_code_index_dir: str = Field(default=".cache/am_cache/code_index", - description="The directory used for storing code index files.") - ignore_errors: bool = Field(default=False, description="Whether to ignore errors during generation.") - ignore_code_embedding: bool = Field(default=False, description="Whether to ignore building code vector database.") - ignore_doc_embedding: bool = Field(default=False, - description="Whether to ignore building documentation vector database.") - ignore_code_index: bool = Field(default=False, description="Whether to ignore building code index.") - - -@register_function(config_type=CVEGenerateVDBsToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def generate_vdb(config: CVEGenerateVDBsToolConfig, builder: Builder): - - from vuln_analysis.data_models.info import AgentMorpheusInfo - from vuln_analysis.data_models.input import AgentMorpheusEngineInput - from vuln_analysis.data_models.input import AgentMorpheusInput - from vuln_analysis.data_models.input import SourceDocumentsInfo - from vuln_analysis.functions.cve_agent import CVEAgentExecutorToolConfig - from vuln_analysis.utils.document_embedding import DocumentEmbedding - from vuln_analysis.utils.full_text_search import FullTextSearch - from vuln_analysis.utils.git_utils import get_repo_from_path - - agent_config = builder.get_function_config(config.agent_name) - assert isinstance(agent_config, CVEAgentExecutorToolConfig) - - # Update config based on tools available in agent config - if "Container Image Code QA System" not in agent_config.tool_names: - logger.info("Container Image Code QA System tool is not enabled, setting ignore_code_embedding to True") - config.ignore_code_embedding = True - - if "Container Image Developer Guide QA System" not in agent_config.tool_names: - logger.info( - "Container Image Developer Guide QA System tool is not enabled, setting ignore_doc_embedding to True") - config.ignore_doc_embedding = True - - if "Lexical Search Container Image Code QA System" not in agent_config.tool_names: - logger.info( - "Lexical Search Container Image Code QA System tool is not enabled, setting ignore_code_index to True") - config.ignore_code_index = True - - embedding = await builder.get_embedder(embedder_name=config.embedder_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - - embedder = DocumentEmbedding(embedding=embedding, - vdb_directory=config.base_vdb_dir, - git_directory=config.base_git_dir) - - def _create_code_index(source_infos: list[SourceDocumentsInfo], embedder: DocumentEmbedding, output_path: Path): - logger.info("Collecting documents from git repos. Source Infos: %s", - json.dumps([x.model_dump(mode="json") for x in source_infos])) - - output_path = Path(output_path) - - # Warn if the output path already exists and we will overwrite it - if (output_path.exists()): - logger.warning("Code index already exists and will be overwritten: %s", output_path) - - documents = [] - for si in source_infos: - try: - documents.extend(embedder.collect_documents(si)) - except Exception as e: - logger.warning("Error collecting documents for source info %s: %s", si, e) - continue - - # Warn and return early if no documents were collected - if len(documents) == 0: - logger.warning("No documents were collected, skipping code indexing for source infos: '%s'", source_infos) - return None - - # Apply chunking on the source documents - chunked_documents = embedder._chunk_documents(documents) - - logger.info("Creating code index from source documents. Doc count: %d, Chunks: %s, Location: %s", - len(documents), - len(chunked_documents), - output_path) - - full_text_search = FullTextSearch(cache_path=str(output_path)) - indexing_start_time = time.time() - full_text_search.add_documents_from_langchain_chunks(chunked_documents) - logger.info("Completed code indexing in %.2f seconds for '%s'", time.time() - indexing_start_time, output_path) - - return full_text_search - - def _build_code_index(source_infos: list[SourceDocumentsInfo]) -> Path | None: - code_index_path: Path | None = None - - # Filter to only code sources - source_infos = [si for si in source_infos if si.type == 'code'] - - if source_infos: - - # Use DocumentEmbedding class with embedding=None for its utility functions - embedder = DocumentEmbedding(embedding=None) - - # Determine code index path for either loading from cache or creating new index - # Need to add support for configurable base path - code_index_path = FullTextSearch.get_index_directory( - base_path=config.base_code_index_dir, hash_value=embedder.hash_source_documents_info(source_infos)) - - if (not code_index_path.exists() or os.environ.get("MORPHEUS_ALWAYS_REBUILD_VDB", "0") == "1"): - index = _create_code_index(source_infos, embedder, code_index_path) - - # Return None if the index was not created - if index is None: - code_index_path = None - else: - logger.info("Cache hit on code index. Loading existing code index: %s", code_index_path) - - return code_index_path - - async def _arun(message: AgentMorpheusInput) -> AgentMorpheusEngineInput: - """ - Builds source code and documentation FAISS databases based upon the source repositories. - For now we are only storing a path to the FAISS databases in the message. - - In the future we will want to store the actual FAISS databases in the message. - - Parameters - ---------- - message : AgentMorpheusInput - The input message - build_vdb_fn : typing.Callable[[str, str, list[SourceCodeRepo]], dict[str, str]] - The function that builds the VDB database for a given vulnerability scan id, base image, and source code - repos - ignore_errors : bool - When True raised exceptions will be logged, when False raised exceptions will be re-raised - """ - workflow_config = builder.get_workflow_config() - - vdb_code_path = None - vdb_doc_path = None - code_index_path = None - - base_image = message.image.name - source_infos = message.image.source_info - - try: - - # Build VDBs - vdb_code_path, vdb_doc_path = embedder.build_vdbs( - source_infos, config.ignore_code_embedding, config.ignore_doc_embedding) - - if (vdb_code_path is None): - # Only log warning if we're not ignoring code embeddings - if (not config.ignore_code_embedding): - logger.warning(("Failed to generate code VDB for image '%s'. " - "Ensure the source repositories are setup correctly"), - base_image) - - if (vdb_doc_path is None): - # Only log warning if we're not ignoring doc embeddings - if (not config.ignore_doc_embedding): - logger.warning(("Failed to generate documentation VDB for image '%s'. " - "Ensure the source repositories are setup correctly"), - base_image) - - # Build code index if not ignored - if (not config.ignore_code_index): - code_index_path = _build_code_index(source_infos) - - if code_index_path is None: - logger.warning(("Failed to generate code index for image '%s'. " - "Ensure the source repositories are setup correctly"), - base_image) - - # Replace ref with specific commit hash for each source info - for si in source_infos: - try: - repo = get_repo_from_path(config.base_git_dir, si.git_repo) - si.ref = repo.commit().hexsha - except ValueError as e: - logger.warning("Failed to get commit hash for %s/%s: %s", config.base_git_dir, si.git_repo, e) - continue - - except Exception as e: - # Check if this is an authentication/authorization error - # These don't require stack traces as they're typically API key issues - is_auth_error = is_authentication_error(e) - - if is_auth_error: - logger.error( - "Failure to build VDB for image, '%s', with source code info: %s\n" - "Error: %s\n" - "This appears to be an authentication error. Please check your API key configuration.\n" - "See %s for instructions on obtaining API keys.", - base_image, - source_infos, - e, - API_KEY_SETUP_LINK) - else: - logger.error("Failure to build VDB for image, '%s', with source code info: %s\nError: %s", - base_image, - source_infos, - e, - exc_info=True) - - if not config.ignore_errors: - raise - - logger.warning("Ignoring VDB generation error and continuing with missing VDBs.") - - # Handle missing_source_action configuration - vdbs_generated = any([vdb_code_path, vdb_doc_path, code_index_path]) - - if not vdbs_generated: - if workflow_config.missing_source_action == "error": # type: ignore - raise RuntimeError( - "No VDBs were built and the workflow's missing_source_action is set to 'error'. " - f"To allow empty VDBs, change the setting to 'skip_agent' or 'continue_with_warning'. " - f"Image: {base_image}, Source info: {source_infos}") - - logger.warning( - "No VDBs were built but continuing due to workflow's missing_source_action setting ('%s'). " - "Image: %s, Source info: %s", - workflow_config.missing_source_action, # type: ignore - base_image, - source_infos) - - vdb_code_path = str(vdb_code_path) if vdb_code_path is not None else None - vdb_doc_path = str(vdb_doc_path) if vdb_doc_path is not None else None - code_index_path = str(code_index_path) if code_index_path is not None else None - - info = AgentMorpheusInfo(vdb=AgentMorpheusInfo.VdbPaths( - code_vdb_path=vdb_code_path, doc_vdb_path=vdb_doc_path, code_index_path=code_index_path)) - - return AgentMorpheusEngineInput(input=message, info=info) - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusInput, - description=("Generates vector database from code repositories and documentation.")) diff --git a/src/vuln_analysis/functions/cve_http_output.py b/src/vuln_analysis/functions/cve_http_output.py deleted file mode 100644 index 0cb2f5cdc..000000000 --- a/src/vuln_analysis/functions/cve_http_output.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -logger = logging.getLogger(__name__) - - -class CVEHttpOutputConfig(FunctionBaseConfig, name="cve_http_output"): - """ - Defines a function that sends CVE workflow output to HTTP endpoint. - """ - url: str = Field(description="URL to send CVE workflow output") - endpoint: str = Field(description="Endpoint to send CVE workflow output") - - -@register_function(config_type=CVEHttpOutputConfig) -async def output_to_http(config: CVEHttpOutputConfig, builder: Builder): # pylint: disable=unused-argument - - from vuln_analysis.data_models.output import AgentMorpheusOutput - from vuln_analysis.utils import http_utils - - async def _arun(message: AgentMorpheusOutput) -> AgentMorpheusOutput: - - model_json = message.model_dump_json(by_alias=True) - url = config.url + config.endpoint - headers = {'Content-type': 'application/json'} - try: - http_utils.request_with_retry(request_kwargs={ - "url": url, "method": "POST", "data": model_json.encode('utf-8'), "headers": headers - }) - except Exception as e: - logger.error('Unable to send output response to %s. Error: %s', url, e) - else: - logger.info('Successfully sent output to %s', url) - - return message - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusOutput, - description=("Sends CVE workflow output to HTTP endpoint.")) diff --git a/src/vuln_analysis/functions/cve_justify.py b/src/vuln_analysis/functions/cve_justify.py deleted file mode 100644 index 407bf18e6..000000000 --- a/src/vuln_analysis/functions/cve_justify.py +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging - -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -from vuln_analysis.utils.concurrency import get_effective_rate_limiter - -logger = logging.getLogger(__name__) - - -class CVEJustifyToolConfig(FunctionBaseConfig, name="cve_justify"): - """ - Defines a function that assigns justification label and reason to each CVE based on summary. - """ - llm_name: str = Field(description="The LLM model to use") - llm_max_rate: int | None = Field( - default=None, - description="Maximum LLM rate limit (requests per second) for justification tasks. " - "If set to a number, overrides the workflow-level llm_max_rate. If None, inherits from workflow-level setting.") - - -@register_function(config_type=CVEJustifyToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_justify(config: CVEJustifyToolConfig, builder: Builder): - - from langchain_core.prompts import PromptTemplate - - from vuln_analysis.data_models.state import AgentMorpheusEngineState - from vuln_analysis.utils.justification_parser import JustificationParser - - jp = JustificationParser() - - llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - - prompt = PromptTemplate(input_variables=["summary"], template=jp.JUSTIFICATION_PROMPT) - chain = prompt | llm - - async def justify_cve(summary): - try: - justification_text = await chain.ainvoke({"summary": summary}) - return justification_text.content - except Exception as e: - logger.error("Error in generating justification: %s: %s", type(e).__name__, e) - raise - - async def _arun(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - rate_limiter = get_effective_rate_limiter(config.llm_max_rate, builder) - - if rate_limiter is not None: - # Use rate limiter to control the rate of requests to the LLM API - async def justify_with_rate_limit(summary): - async with rate_limiter: - return await justify_cve(summary) - - effective_rate = int(rate_limiter.max_rate / rate_limiter.time_period) - logger.info("Justifying %d CVEs with LLM rate limit of %d requests/second", - len(state.final_summaries), - effective_rate) - results = await asyncio.gather(*(justify_with_rate_limit(summary) - for summary in state.final_summaries.values())) - else: - logger.info("Justifying %d CVEs with no rate limiting", len(state.final_summaries)) - results = await asyncio.gather(*(justify_cve(summary) for summary in state.final_summaries.values())) - - parsed_justification = await asyncio.gather(jp._parse_justification(results)) - - # format justification output - justifications = {} - for i, vuln_id in enumerate(state.checklist_results.keys()): - justifications[vuln_id] = {} - for key in parsed_justification[0]: - justifications[vuln_id][key] = parsed_justification[0][key][i] - - state.justifications = justifications - return state - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusEngineState, - description=("Assigns justification label and reason to each CVE based on summary.")) diff --git a/src/vuln_analysis/functions/cve_process_sbom.py b/src/vuln_analysis/functions/cve_process_sbom.py deleted file mode 100644 index f2864e235..000000000 --- a/src/vuln_analysis/functions/cve_process_sbom.py +++ /dev/null @@ -1,119 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field -from pydantic import NonNegativeInt - -logger = logging.getLogger(__name__) - - -class CVEProcessSBOMConfig(FunctionBaseConfig, name="cve_process_sbom"): - """ - Defines a function that prepares and validates input SBOM. - """ - max_retries: NonNegativeInt = Field(default=10, description="Max retries when using SBOM from a URL") - - -@register_function(config_type=CVEProcessSBOMConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_process_sbom(config: CVEProcessSBOMConfig, builder: Builder): # pylint: disable=unused-argument - - from vuln_analysis.data_models.info import AgentMorpheusInfo - from vuln_analysis.data_models.input import AgentMorpheusEngineInput - from vuln_analysis.data_models.input import FileSBOMInfoInput - from vuln_analysis.data_models.input import HTTPSBOMInfoInput - from vuln_analysis.data_models.input import ManualSBOMInfoInput - from vuln_analysis.data_models.input import SBOMPackage - from vuln_analysis.utils import http_utils - from vuln_analysis.utils.http_utils import HTTPMethod - - async def _arun(message: AgentMorpheusEngineInput) -> AgentMorpheusEngineInput: - - def _parse_sbom_packages(sbom_lines: list[str]) -> list[SBOMPackage]: - # Extract the packages - packages: list[SBOMPackage] = [] - - # Validate the first header line contains NAME, VERSION, TYPE - if sbom_lines[0].split() != ["NAME", "VERSION", "TYPE"]: - logger.error("Invalid SBOM file format. Expected file to contain columns: 'NAME VERSION TYPE'.") - return packages - - # Process the remaining lines - for line in sbom_lines[1:]: - split = line.split() - if len(split) < 3: - continue - p_version = split[1] - p_name = split[0] - p_sys = split[2] - - packages.append(SBOMPackage(name=p_name, version=p_version, system=p_sys)) - - return packages - - if (message.input.image.sbom_info.type == ManualSBOMInfoInput.static_type()): - assert isinstance(message.input.image.sbom_info, ManualSBOMInfoInput) - - # Create the SBOM object - message.info.sbom = AgentMorpheusInfo.SBOMInfo(packages=message.input.image.sbom_info.packages) - - elif (message.input.image.sbom_info.type == FileSBOMInfoInput.static_type()): - assert isinstance(message.input.image.sbom_info, FileSBOMInfoInput) - - # Read the file to an object - with open(message.input.image.sbom_info.file_path, "r", encoding="utf-8") as f: - sbom_lines = f.readlines() - - # Create the SBOM object - message.info.sbom = AgentMorpheusInfo.SBOMInfo(packages=_parse_sbom_packages(sbom_lines)) - - elif (message.input.image.sbom_info.type == HTTPSBOMInfoInput.static_type()): - assert isinstance(message.input.image.sbom_info, HTTPSBOMInfoInput) - - try: - _, response = http_utils.request_with_retry( - request_kwargs={ - "url": message.input.image.sbom_info.url, "method": HTTPMethod.GET.value - }, - max_retries=config.max_retries - ) - except Exception as e: - logger.error("Error fetching SBOM from %s: %s", message.input.image.sbom_info.url, e) - message.info.sbom = AgentMorpheusInfo.SBOMInfo(packages=[]) - return message - - try: - # Read SBOM lines from the response - sbom_lines = response.text.splitlines() - packages = _parse_sbom_packages(sbom_lines) - except Exception as e: - logger.error("Error parsing SBOM from %s: %s", message.input.image.sbom_info.url, e) - message.info.sbom = AgentMorpheusInfo.SBOMInfo(packages=[]) - return message - - # Create the SBOM object - message.info.sbom = AgentMorpheusInfo.SBOMInfo(packages=packages) - - return message - - yield FunctionInfo.from_fn(_arun, - input_schema=AgentMorpheusEngineInput, - description=("Prepares and validates input SBOM.")) diff --git a/src/vuln_analysis/functions/cve_researcher.py b/src/vuln_analysis/functions/cve_researcher.py new file mode 100644 index 000000000..c3f537211 --- /dev/null +++ b/src/vuln_analysis/functions/cve_researcher.py @@ -0,0 +1,9 @@ +"""Compatibility exports for the local CVE researcher NAT plugin.""" + +from vuln_analysis.plugins.cve_researcher.register import CVEResearcherConfig +from vuln_analysis.plugins.cve_researcher.register import cve_researcher + +__all__ = [ + "CVEResearcherConfig", + "cve_researcher", +] diff --git a/src/vuln_analysis/functions/cve_summarize.py b/src/vuln_analysis/functions/cve_summarize.py deleted file mode 100644 index cf3b6204f..000000000 --- a/src/vuln_analysis/functions/cve_summarize.py +++ /dev/null @@ -1,91 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging - -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -from vuln_analysis.utils.concurrency import get_effective_rate_limiter -from vuln_analysis.utils.string_utils import get_checklist_item_string - -logger = logging.getLogger(__name__) - - -class CVESummarizeToolConfig(FunctionBaseConfig, name="cve_summarize"): - """ - Defines a function that generates concise, human-readable summarization paragraph from agent results. - """ - llm_name: str = Field(description="The LLM model to use") - llm_max_rate: int | None = Field( - default=None, - description="Maximum LLM rate limit (requests per second) for summarization tasks. " - "If set to a number, overrides the workflow-level llm_max_rate. If None, inherits from workflow-level setting.") - - -@register_function(config_type=CVESummarizeToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_summarize(config: CVESummarizeToolConfig, builder: Builder): - - from langchain_core.prompts import PromptTemplate - - from vuln_analysis.data_models.state import AgentMorpheusEngineState - from vuln_analysis.utils.prompting import SUMMARY_PROMPT - - llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - prompt = PromptTemplate(input_variables=["response"], template=SUMMARY_PROMPT) - chain = prompt | llm - - async def summarize_cve(results): - try: - response = '\n'.join( - [get_checklist_item_string(idx + 1, checklist_item) for idx, checklist_item in enumerate(results[1])]) - final_summary = await chain.ainvoke({"response": response}) - - return final_summary.content - except Exception as e: - logger.error("Error in generating summary: %s: %s", type(e).__name__, e) - raise - - async def _arun(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - rate_limiter = get_effective_rate_limiter(config.llm_max_rate, builder) - - if rate_limiter is not None: - # Use rate limiter to control the rate of requests to the LLM API - async def summarize_with_rate_limit(results): - async with rate_limiter: - return await summarize_cve(results) - - effective_rate = int(rate_limiter.max_rate / rate_limiter.time_period) - logger.info("Summarizing %d CVEs with LLM rate limit of %d requests/second", - len(state.checklist_results), - effective_rate) - results = await asyncio.gather(*(summarize_with_rate_limit(results) - for results in state.checklist_results.items())) - else: - logger.info("Summarizing %d CVEs with no rate limiting", len(state.checklist_results)) - results = await asyncio.gather(*(summarize_cve(results) for results in state.checklist_results.items())) - - state.final_summaries = dict(zip(state.checklist_results.keys(), results)) - return state - - yield FunctionInfo.from_fn( - _arun, - input_schema=AgentMorpheusEngineState, - description=("Generates concise, human-readable summarization paragraph from agent results.")) diff --git a/src/vuln_analysis/functions/exploitability_analyzer.py b/src/vuln_analysis/functions/exploitability_analyzer.py new file mode 100644 index 000000000..d2ccfd413 --- /dev/null +++ b/src/vuln_analysis/functions/exploitability_analyzer.py @@ -0,0 +1,198 @@ +"""Exploitability Analyzer agent function for NAT.""" + +import logging +import re +from pathlib import Path + +from langchain_core.messages import HumanMessage, SystemMessage +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import FunctionRef +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +from vuln_analysis.data_models.exploitability_analyzer import ( + ExploitabilityAnalyzerInput, + ExploitabilityAnalyzerResult, + extract_cve_id_from_path, + extract_container_name_from_path, +) +from vuln_analysis.prompts.exploitability_analyzer import ( + PHASE2_SYSTEM_PROMPT, + build_phase2_user_message, +) +from vuln_analysis.tools.filesystem_tools import configure_allowed_paths +from vuln_analysis.utils.agentic_loop import run_agentic_loop +from vuln_analysis.utils.concurrency import make_lazy_rate_limiter +from vuln_analysis.utils.report_generator import generate_early_exit_report +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + + +class ExploitabilityAnalyzerConfig(FunctionBaseConfig, name="exploitability_analyzer"): + llm_name: str = Field(description="LLM for Phase 2 deep analysis") + tool_names: list[FunctionRef] = Field(description="Filesystem + binary tools") + phase2_max_iterations: int = Field(default=40) + context_window_tokens: int = Field(default=262144) + wrap_up_fraction: float = Field(default=0.80) + loop_detection_threshold: int = Field(default=2) + llm_max_rate: int | None = Field( + default=None, + description="Per-function LLM rate limit (requests/second). Overrides the workflow setting.", + ) + discovery_probe_paths: list[str] = Field(default=[ + "opt/conda/envs", "opt/conda", "usr/lib", "usr/local/lib", + "usr/local/bin", "workspace", "app", "opt", "srv", + ]) + output_subdir: str = Field(default="exploitability_reports") + description: str = Field(default="Analyzes exploitability of a CVE in a container") + + +@register_function(config_type=ExploitabilityAnalyzerConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def exploitability_analyzer(config: ExploitabilityAnalyzerConfig, builder: Builder): + llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + tools = await builder.get_tools(tool_names=config.tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + get_rate_limiter = make_lazy_rate_limiter(config.llm_max_rate, builder) + + async def _arun(inp: ExploitabilityAnalyzerInput) -> ExploitabilityAnalyzerResult: + rate_limiter = get_rate_limiter() + for label, path in [ + ("Vulnerability report", inp.vulnerability_report_path), + ("Container report", inp.container_report_path), + ]: + if not Path(path).is_file(): + return ExploitabilityAnalyzerResult( + cve_id="unknown", container_name="unknown", + success=False, error=f"{label} not found: {path}", + ) + + if not Path(inp.filesystem_path).is_dir(): + return ExploitabilityAnalyzerResult( + cve_id="unknown", container_name="unknown", + success=False, error=f"Filesystem directory not found: {inp.filesystem_path}", + ) + + cve_id = _extract_cve_id(inp.vulnerability_report_path) + container_name = extract_container_name_from_path(inp.container_report_path) + + logger.info("Starting exploitability analysis: %s × %s", cve_id, container_name) + + # Pre-triage (deterministic) + try: + from vuln_analysis.utils.pre_triage import run_pre_triage + pre_triage_result = run_pre_triage(inp) + except Exception as exc: + logger.error("Pre-triage failed: %s", exc, exc_info=True) + return ExploitabilityAnalyzerResult( + cve_id=cve_id, container_name=container_name, + success=False, error=f"Pre-triage failed: {exc}", + ) + + if pre_triage_result.early_exit: + report_content = generate_early_exit_report(pre_triage_result, inp) + verdict = pre_triage_result.early_exit_verdict + logger.info( + "Pre-triage early exit: verdict=%s, reason=%s", + verdict, + pre_triage_result.early_exit_reason, + ) + return ExploitabilityAnalyzerResult( + cve_id=cve_id, + container_name=container_name, + report_content=report_content, + verdict=verdict, + confidence="High", + success=True, + tool_call_count=0, + llm_call_count=pre_triage_result.llm_call_count, + ) + + pkg_status = "found" if pre_triage_result.package_verification.found else "not confirmed" + extra_found = sum( + 1 for a in pre_triage_result.additional_package_verifications + if a.verification.found + ) + extra_msg = f", additional_found={extra_found}" if extra_found else "" + logger.info( + "Pre-triage complete (primary_package=%s%s). Proceeding to Phase 2.", + pkg_status, + extra_msg, + ) + + configure_allowed_paths( + roots=[inp.filesystem_path], + files=[inp.vulnerability_report_path, inp.container_report_path], + filesystem_root=inp.filesystem_path, + ) + + # Phase 2: Deep Analysis + logger.info("Phase 2: starting deep analysis") + messages = [ + SystemMessage(content=PHASE2_SYSTEM_PROMPT), + HumanMessage(content=build_phase2_user_message(inp, cve_id, container_name, pre_triage_result)), + ] + + try: + content, tool_calls, llm_calls = await run_agentic_loop( + llm=llm, + messages=messages, + tools=tools, + max_iterations=config.phase2_max_iterations, + context_window_tokens=config.context_window_tokens, + wrap_up_fraction=config.wrap_up_fraction, + loop_threshold=config.loop_detection_threshold, + phase_label="Phase 2", + rate_limiter=rate_limiter, + ) + except Exception as exc: + logger.error("Phase 2 failed: %s", exc, exc_info=True) + return ExploitabilityAnalyzerResult( + cve_id=cve_id, container_name=container_name, + success=False, error=str(exc), + ) + + if not content: + return ExploitabilityAnalyzerResult( + cve_id=cve_id, container_name=container_name, + success=False, error="No report content produced by the LLM.", + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + verdict = extract_markdown_table_field(content, "Verdict") + confidence = extract_markdown_table_field(content, "Confidence") + + logger.info("Analysis complete: %s × %s (verdict=%s, confidence=%s, %d tool calls, %d LLM calls)", + cve_id, container_name, verdict, confidence, tool_calls, llm_calls) + return ExploitabilityAnalyzerResult( + cve_id=cve_id, container_name=container_name, + report_content=content, verdict=verdict, confidence=confidence, + success=True, tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + def _str_to_input(s: str) -> ExploitabilityAnalyzerInput: + return ExploitabilityAnalyzerInput.model_validate_json(s) + + def _result_to_str(r: ExploitabilityAnalyzerResult) -> str: + return r.model_dump_json() + + yield FunctionInfo.from_fn( + _arun, input_schema=ExploitabilityAnalyzerInput, description=config.description, + converters=[_str_to_input, _result_to_str], + ) + + +def _extract_cve_id(vulnerability_report_path: str) -> str: + cve_id = extract_cve_id_from_path(vulnerability_report_path) + if re.match(r"CVE-\d{4}-\d+", cve_id): + return cve_id + try: + content = Path(vulnerability_report_path).read_text(encoding="utf-8")[:2000] + match = re.search(r"(CVE-\d{4}-\d+)", content) + if match: + return match.group(1) + except Exception: + pass + return cve_id diff --git a/src/vuln_analysis/functions/vex_categorizer.py b/src/vuln_analysis/functions/vex_categorizer.py new file mode 100644 index 000000000..03218b90c --- /dev/null +++ b/src/vuln_analysis/functions/vex_categorizer.py @@ -0,0 +1,152 @@ +"""VEX Categorizer agent function for NAT.""" + +import logging +from pathlib import Path + +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.tools import tool +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +from vuln_analysis.data_models.vex_categorizer import ( + VEXCategorizerInput, + VEXCategorizerResult, + parse_report_filename, +) +from vuln_analysis.prompts.vex_categorizer import SYSTEM_PROMPT, build_user_message +from vuln_analysis.utils.agentic_loop import run_agentic_loop +from vuln_analysis.utils.concurrency import make_lazy_rate_limiter +from vuln_analysis.utils.helpers import truncate +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + + +@tool +def reason(step: str, analysis: str, conclusion: str) -> str: + """Record step-by-step reasoning for a VEX decision flowchart step.""" + logger.info("reason: step=%s, conclusion=%s", step, truncate(conclusion, 120)) + return ( + f"Recorded. Step: {step} → Conclusion: {conclusion}\n" + "Continue to the next VEX decision step, or write the outputs " + "if you have reached a final determination." + ) + + +class VEXCategorizerConfig(FunctionBaseConfig, name="vex_categorizer"): + llm_name: str = Field(description="LLM for VEX classification") + max_iterations: int = Field(default=10) + loop_detection_threshold: int = Field(default=2) + llm_max_rate: int | None = Field( + default=None, + description="Per-function LLM rate limit (requests/second). Overrides the workflow setting.", + ) + output_subdir: str = Field(default="vex_results") + description: str = Field(default="Categorizes VEX status from exploitability and container reports") + + +@register_function(config_type=VEXCategorizerConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def vex_categorizer(config: VEXCategorizerConfig, builder: Builder): + llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + vex_tools = [reason] + get_rate_limiter = make_lazy_rate_limiter(config.llm_max_rate, builder) + + async def _arun(inp: VEXCategorizerInput) -> VEXCategorizerResult: + rate_limiter = get_rate_limiter() + for label, path in [ + ("Exploitability report", inp.exploitability_report_path), + ("Container report", inp.container_report_path), + ]: + if not Path(path).is_file(): + return VEXCategorizerResult( + cve_id="unknown", container_name="unknown", + success=False, error=f"{label} not found: {path}", + ) + + cve_id, container_name = parse_report_filename(inp.exploitability_report_path) + + exploit_content = _read_report(inp.exploitability_report_path) + container_content = _read_report(inp.container_report_path) + if exploit_content is None or container_content is None: + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + success=False, error="Failed to read one or both input reports.", + ) + + logger.info("Starting VEX categorization: %s × %s", cve_id, container_name) + messages = [ + SystemMessage(content=SYSTEM_PROMPT), + HumanMessage(content=build_user_message( + exploitability_report_path=inp.exploitability_report_path, + exploitability_report_content=exploit_content, + container_report_path=inp.container_report_path, + container_report_content=container_content, + cve_id=cve_id, + container_name=container_name, + )), + ] + + try: + content, tool_calls, llm_calls = await run_agentic_loop( + llm=llm, + messages=messages, + tools=vex_tools, + max_iterations=config.max_iterations, + context_window_tokens=200_000, + loop_threshold=config.loop_detection_threshold, + phase_label="VEX Categorizer", + rate_limiter=rate_limiter, + ) + except Exception as exc: + logger.error("VEX Categorizer failed: %s", exc, exc_info=True) + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + success=False, error=str(exc), + ) + + if not content: + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + success=False, error="No VEX summary report produced.", + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + status = extract_markdown_table_field(content, "Status") + justification = extract_markdown_table_field(content, "Justification") + reasoning = extract_markdown_table_field(content, "Reasoning") + + logger.info("VEX categorization complete: %s × %s → status=%s, justification=%s", + cve_id, container_name, status, justification) + return VEXCategorizerResult( + cve_id=cve_id, container_name=container_name, + status=status, justification=justification, reasoning=reasoning, + report_content=content, success=True, + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + def _str_to_input(s: str) -> VEXCategorizerInput: + return VEXCategorizerInput.model_validate_json(s) + + def _result_to_str(r: VEXCategorizerResult) -> str: + return r.model_dump_json() + + yield FunctionInfo.from_fn( + _arun, input_schema=VEXCategorizerInput, description=config.description, + converters=[_str_to_input, _result_to_str], + ) + + +def _read_report(path: str) -> str | None: + try: + return Path(path).read_text(encoding="utf-8") + except UnicodeDecodeError: + try: + return Path(path).read_text(encoding="latin-1") + except Exception: + return None + except Exception: + return None diff --git a/src/vuln_analysis/pipeline/__init__.py b/src/vuln_analysis/pipeline/__init__.py new file mode 100644 index 000000000..07242a4b5 --- /dev/null +++ b/src/vuln_analysis/pipeline/__init__.py @@ -0,0 +1 @@ +"""Workflow implementation modules for the CVE analysis pipeline.""" diff --git a/src/vuln_analysis/pipeline/context.py b/src/vuln_analysis/pipeline/context.py new file mode 100644 index 000000000..dad819125 --- /dev/null +++ b/src/vuln_analysis/pipeline/context.py @@ -0,0 +1,127 @@ +"""Shared runtime context for pipeline nodes.""" + +import asyncio +import logging +from collections.abc import Awaitable +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from typing import Protocol + +from vuln_analysis.data_models.output import AgentResultBase +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.utils.concurrency import AsyncCoalescer +from vuln_analysis.utils.concurrency import atomic_write + +logger = logging.getLogger(__name__) + + +class AgentFunction(Protocol): + """NAT function wrapper used by pipeline stages.""" + + async def ainvoke(self, agent_input: Any) -> Any: + """Invoke the wrapped agent function.""" + ... + + +class PipelineConfig(Protocol): + """Subset of workflow config needed by the pipeline runtime.""" + + stage_timeout: int + stage_max_retries: int + max_concurrency: int | None + output_dir: str + + +@dataclass(slots=True) +class PipelineFunctions: + """Agent functions used by the pipeline nodes.""" + + cve_researcher: AgentFunction + container_analyzer: AgentFunction + exploitability_analyzer: AgentFunction + vex_categorizer: AgentFunction + + +class PipelineContext: + """Shared dependencies and utility methods for graph nodes.""" + + def __init__(self, config: PipelineConfig, functions: PipelineFunctions) -> None: + self.config = config + self.functions = functions + self.coalescer = AsyncCoalescer() + self.stage_semaphore = ( + asyncio.Semaphore(config.max_concurrency) + if config.max_concurrency and config.max_concurrency > 0 else None + ) + + async def throttled(self, coro: Awaitable[Any]) -> Any: + """Run a coroutine under the stage semaphore when a limit is configured.""" + if self.stage_semaphore is None: + return await coro + async with self.stage_semaphore: + return await coro + + async def run_with_retry( + self, + agent_name: str, + coro_factory: Callable[[], Awaitable[StageResult]], + ) -> StageResult: + """Run a stage with timeout and retry handling.""" + max_attempts = self.config.stage_max_retries + 1 + result: StageResult | None = None + for attempt in range(1, max_attempts + 1): + try: + result = await asyncio.wait_for(coro_factory(), timeout=self.config.stage_timeout) + except asyncio.TimeoutError: + result = StageResult( + agent_name=agent_name, + success=False, + error=f"Stage timed out after {self.config.stage_timeout}s", + ) + except Exception as exc: + result = StageResult(agent_name=agent_name, success=False, error=f"Unhandled exception: {exc}") + if result.success or attempt == max_attempts: + return result + logger.warning("%s failed (attempt %d/%d): %s - retrying", agent_name, attempt, max_attempts, result.error) + return StageResult(agent_name=agent_name, success=False, error="Stage failed without producing a result") + + @staticmethod + def result_or_failure(result: StageResult | BaseException, agent_name: str) -> StageResult: + """Convert gathered exceptions into failed stage results.""" + if isinstance(result, BaseException): + return StageResult(agent_name=agent_name, success=False, error=f"Exception: {result}") + return result + + def use_cache_for(self, inp_data: dict, stage_name: str) -> bool: + """Resolve cache settings from pre-validated input data.""" + if not inp_data.get("cache", False): + return False + override = inp_data.get(f"cache_{stage_name}") + return override if override is not None else inp_data["cache"] + + def output_dir_key(self, inp_data: dict) -> str: + """Return a resolved output_dir string for coalescer keys.""" + output_dir = inp_data.get("output_dir") + if output_dir is None: + return str(Path(self.config.output_dir).resolve()) + return str(Path(output_dir).resolve()) + + def check_cache(self, use_cache: bool, inp_data: dict, cache_subdir: str, filename: str) -> Path | None: + """Return a cached report path when cache is enabled and the file exists.""" + if not use_cache: + return None + root = Path(self.output_dir_key(inp_data)) + cached_path = root / cache_subdir / filename + return cached_path if cached_path.exists() else None + + def persist_report(self, res: AgentResultBase, inp_data: dict, cache_subdir: str, filename: str) -> str | None: + """Write report_content to disk if needed; return the final report path.""" + report_path = getattr(res, "report_path", None) + if res.success and res.report_content and not report_path: + root = Path(self.output_dir_key(inp_data)).resolve() + report_file = root / cache_subdir / filename + atomic_write(report_file, res.report_content) + return str(report_file) + return report_path diff --git a/src/vuln_analysis/pipeline/converters.py b/src/vuln_analysis/pipeline/converters.py new file mode 100644 index 000000000..61c5af342 --- /dev/null +++ b/src/vuln_analysis/pipeline/converters.py @@ -0,0 +1,33 @@ +"""Input and output converters for the NAT workflow registration.""" + +import logging +from io import TextIOWrapper + +from vuln_analysis.data_models.input import PipelineInput +from vuln_analysis.data_models.output import PipelineResult + +logger = logging.getLogger(__name__) + + +def str_to_pipeline_input(input_str: str) -> PipelineInput: + """Convert a JSON string into pipeline input.""" + try: + return PipelineInput.model_validate_json(input_str) + except Exception as exc: + logger.error("Failed to convert input: %s", exc) + raise + + +def textio_to_pipeline_input(input_file: TextIOWrapper) -> PipelineInput: + """Convert a text file object containing JSON into pipeline input.""" + try: + data = input_file.read() + return PipelineInput.model_validate_json(data) + except Exception as exc: + logger.error("Failed to convert input: %s", exc) + raise + + +def pipeline_result_to_str(output: PipelineResult) -> str: + """Serialize pipeline output for NAT.""" + return output.model_dump_json() diff --git a/src/vuln_analysis/pipeline/exploitability.py b/src/vuln_analysis/pipeline/exploitability.py new file mode 100644 index 000000000..9e48a303d --- /dev/null +++ b/src/vuln_analysis/pipeline/exploitability.py @@ -0,0 +1,117 @@ +"""Exploitability Analyzer nodes for the CVE pipeline graph.""" + +import asyncio +import logging +from functools import partial + +from vuln_analysis.data_models.exploitability_analyzer import ExploitabilityAnalyzerInput +from vuln_analysis.data_models.exploitability_analyzer import ExploitabilityAnalyzerResult +from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as derive_exploitability_filename +from vuln_analysis.data_models.exploitability_analyzer import extract_container_name_from_path +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.data_models.state import PipelineState +from vuln_analysis.pipeline.context import PipelineContext +from vuln_analysis.pipeline.helpers import coerce_result +from vuln_analysis.pipeline.helpers import resolve_cve_id +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + + +async def run_exploitability_for( + context: PipelineContext, + identifier: str, + cve_report: StageResult, + container_report: StageResult, + inp_data: dict, +) -> StageResult: + """Run Exploitability Analyzer for a single vulnerability.""" + container_name = extract_container_name_from_path(container_report.report_path) + cve_id = resolve_cve_id(cve_report.report_path) + filename = derive_exploitability_filename(cve_id, container_name) + + async def exploitability_impl() -> StageResult: + cached = context.check_cache( + context.use_cache_for(inp_data, "exploitability_analyzer"), + inp_data, + "exploitability_reports", + filename, + ) + if cached: + logger.info("Cache hit - skipping Exploitability Analyzer for %s, report at %s", identifier, cached) + content = cached.read_text(encoding="utf-8") + return StageResult( + agent_name="Exploitability Analyzer", + success=True, + report_path=str(cached), + extra={ + "verdict": extract_markdown_table_field(content, "Verdict"), + "confidence": extract_markdown_table_field(content, "Confidence"), + }, + ) + + agent_inp = ExploitabilityAnalyzerInput( + vulnerability_report_path=cve_report.report_path, + container_report_path=container_report.report_path, + filesystem_path=inp_data["filesystem_path"], + ) + res = coerce_result( + await context.functions.exploitability_analyzer.ainvoke(agent_inp), + ExploitabilityAnalyzerResult, + {"cve_id": "unknown", "container_name": "unknown"}, + ) + report_path = context.persist_report( + res, + inp_data, + "exploitability_reports", + derive_exploitability_filename(res.cve_id, container_name), + ) + + return StageResult( + agent_name="Exploitability Analyzer", + success=res.success, + report_path=report_path if res.success else None, + error=res.error, + tool_call_count=res.tool_call_count, + llm_call_count=res.llm_call_count, + extra={"verdict": res.verdict, "confidence": res.confidence}, + ) + + return await context.coalescer.run_once( + f"exploitability:{filename}:{context.output_dir_key(inp_data)}", exploitability_impl + ) + + +async def exploitability_node(context: PipelineContext, state: PipelineState) -> dict: + """Fan out Exploitability Analyzer work for successful vulnerability reports.""" + container_report = state["container_report"] + cve_reports = state["cve_reports"] + inp_data = state["input"] + + successful = {ident: result for ident, result in cve_reports.items() if result.success} + identifiers = list(successful.keys()) + coros = [] + for ident in identifiers: + coros.append( + context.throttled( + context.run_with_retry( + "Exploitability Analyzer", + partial(run_exploitability_for, context, ident, successful[ident], container_report, inp_data), + ) + ) + ) + results = await asyncio.gather(*coros, return_exceptions=True) + + exploit_reports: dict[str, StageResult] = {} + for ident, result in zip(identifiers, results): + exploit_reports[ident] = context.result_or_failure(result, "Exploitability Analyzer") + + return {"exploit_reports": exploit_reports} + + +def check_stage2(state: PipelineState) -> str: + """Continue only when at least one exploitability report succeeded.""" + exploit_reports = state.get("exploit_reports", {}) + if any(r.success for r in exploit_reports.values()): + return "continue" + return "fail" diff --git a/src/vuln_analysis/pipeline/final.py b/src/vuln_analysis/pipeline/final.py new file mode 100644 index 000000000..10c83d9dd --- /dev/null +++ b/src/vuln_analysis/pipeline/final.py @@ -0,0 +1,35 @@ +"""Terminal nodes for the CVE pipeline graph.""" + +import logging + +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.data_models.state import PipelineState + +logger = logging.getLogger(__name__) + + +async def fail_safe_node(state: PipelineState) -> dict: + """Default unresolved vulnerability statuses to unknown/uncertain.""" + logger.warning("Fail-safe: defaulting to unknown/uncertain") + entries = state.get("vulns", []) + vex_statuses = dict(state.get("vex_statuses", {})) + vex_justifications = dict(state.get("vex_justifications", {})) + for entry in entries: + ident = entry["vuln_id"] + vex_statuses.setdefault(ident, "unknown") + vex_justifications.setdefault(ident, "uncertain") + return { + "vex_statuses": vex_statuses, + "vex_justifications": vex_justifications, + } + + +async def output_results_node(state: PipelineState) -> dict: + """Mark the full pipeline successful only when every requested VEX result succeeded.""" + vex_results = state.get("vex_results", {}) + entries = state.get("vulns", []) + all_ok = bool(entries) and all( + vex_results.get(e["vuln_id"], StageResult(agent_name="", success=False)).success + for e in entries + ) + return {"success": all_ok} diff --git a/src/vuln_analysis/pipeline/helpers.py b/src/vuln_analysis/pipeline/helpers.py new file mode 100644 index 000000000..a301af481 --- /dev/null +++ b/src/vuln_analysis/pipeline/helpers.py @@ -0,0 +1,123 @@ +"""Shared helpers for pipeline nodes and final result assembly.""" + +import json +import logging +import re +from collections.abc import Callable +from pathlib import Path +from typing import Any +from typing import TypeVar + +from vuln_analysis.data_models.output import AgentResultBase +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.utils.concurrency import atomic_write +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + +T = TypeVar("T", bound=AgentResultBase) +_CVE_ID_SEARCH_RE = re.compile(r"\bCVE-\d{4}-\d{4,}\b", re.ASCII | re.IGNORECASE) +_GHSA_ID_SEARCH_RE = re.compile( + r"\bGHSA(?:-[23456789cfghjmpqrvwx]{4}){3}\b", + re.ASCII | re.IGNORECASE, +) + + +def coerce_result(raw: Any, result_type: type[T], fallback_kwargs: dict[str, Any]) -> T: + """Ensure agent output is the expected Pydantic type.""" + if isinstance(raw, result_type): + return raw + if isinstance(raw, dict): + return result_type(**raw) + return result_type(**fallback_kwargs, success=False, error="Unexpected result type") + + +def resolve_cve_id(vulnerability_report_path: str) -> str: + """Extract a CVE ID from a vulnerability report path, falling back to report content.""" + stem = Path(vulnerability_report_path).stem + match = re.match(r"(CVE-\d{4}-\d+)", stem) + if match: + return match.group(1) + try: + content = Path(vulnerability_report_path).read_text(encoding="utf-8")[:2000] + match = re.search(r"(CVE-\d{4}-\d+)", content) + if match: + return match.group(1) + except Exception: + pass + return stem + + +def resolve_pipeline_result_ids( + vuln_id: str, + cve_result: StageResult | None = None, +) -> tuple[str | None, str | None]: + """Resolve CVE/GHSA aliases for final pipeline output.""" + cve_id = vuln_id if vuln_id.startswith("CVE-") else None + ghsa_id = vuln_id if vuln_id.startswith("GHSA-") else None + + if cve_result and cve_result.report_path: + try: + content = Path(cve_result.report_path).read_text(encoding="utf-8")[:4000] + cve_id = cve_id or _extract_id_from_report_field( + content, "CVE ID", _CVE_ID_SEARCH_RE, _normalize_cve_id + ) + ghsa_id = ghsa_id or _extract_id_from_report_field( + content, "GHSA ID", _GHSA_ID_SEARCH_RE, _normalize_ghsa_id + ) + except Exception as exc: + logger.debug("Failed to read vulnerability IDs from %s: %s", cve_result.report_path, exc) + + return cve_id, ghsa_id + + +def write_vex_json( + json_path: str, + *, + cve_id: str, + container_name: str, + status: str | None, + justification: str | None, + reasoning: str | None, + report_content: str, + summary_path: str, + exploitability_report_path: str, +) -> None: + """Persist the JSON sidecar for a VEX result.""" + data: dict[str, Any] = { + "cve_id": cve_id, + "container": container_name, + "status": status, + "justification": justification, + "reasoning": reasoning, + } + for field_name in ("Exploitability Verdict", "Confidence", "Risk Level", "Container Type"): + val = extract_markdown_table_field(report_content, field_name) + if val: + data[field_name.lower().replace(" ", "_")] = val + data["report_path"] = summary_path + data["exploitability_report_path"] = exploitability_report_path + + atomic_write(Path(json_path), json.dumps(data, indent=2)) + logger.info("Wrote VEX JSON to %s", json_path) + + +def _extract_id_from_report_field( + content: str, + field_name: str, + pattern: re.Pattern[str], + normalize: Callable[[str], str], +) -> str | None: + value = extract_markdown_table_field(content, field_name) + if not value: + return None + match = pattern.search(value) + return normalize(match.group(0)) if match else None + + +def _normalize_cve_id(cve_id: str) -> str: + return cve_id.upper() + + +def _normalize_ghsa_id(ghsa_id: str) -> str: + return f"GHSA-{ghsa_id[5:].lower()}" diff --git a/src/vuln_analysis/pipeline/stage1.py b/src/vuln_analysis/pipeline/stage1.py new file mode 100644 index 000000000..c7ec90a1e --- /dev/null +++ b/src/vuln_analysis/pipeline/stage1.py @@ -0,0 +1,154 @@ +"""Stage 1 nodes: CVE research fan-out and container analysis.""" + +import asyncio +import logging +from functools import partial + +from vuln_analysis.data_models.container_analyzer import ContainerAnalyzerInput +from vuln_analysis.data_models.container_analyzer import ContainerAnalyzerResult +from vuln_analysis.data_models.container_analyzer import derive_report_filename +from vuln_analysis.data_models.cve_researcher import CVEResearcherInput +from vuln_analysis.data_models.cve_researcher import CVEResearcherResult +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.data_models.state import PipelineState +from vuln_analysis.pipeline.context import PipelineContext +from vuln_analysis.pipeline.helpers import coerce_result + +logger = logging.getLogger(__name__) + + +async def run_cve_researcher_for(context: PipelineContext, entry: dict, inp_data: dict) -> StageResult: + """Run CVE Researcher for a single CVE/GHSA entry.""" + vuln_id = entry["vuln_id"] + is_ghsa = vuln_id.startswith("GHSA-") + + async def cve_researcher_impl() -> StageResult: + cached = context.check_cache( + context.use_cache_for(inp_data, "cve_researcher"), + inp_data, + "vulnerability_reports", + f"{vuln_id}.md", + ) + if cached: + logger.info("Cache hit - skipping CVE Researcher for %s, report at %s", vuln_id, cached) + return StageResult(agent_name="CVE Researcher", success=True, report_path=str(cached)) + + agent_inp = CVEResearcherInput( + cve_id=None if is_ghsa else vuln_id, + ghsa_id=vuln_id if is_ghsa else None, + package_hint=entry.get("package_hint"), + version_hint=entry.get("version_hint"), + ) + res = coerce_result( + await context.functions.cve_researcher.ainvoke(agent_inp), + CVEResearcherResult, + {"identifier": agent_inp.identifier}, + ) + report_path = context.persist_report(res, inp_data, "vulnerability_reports", f"{res.identifier}.md") + + return StageResult( + agent_name="CVE Researcher", + success=res.success, + report_path=report_path if res.success else None, + error=res.error, + tool_call_count=res.tool_call_count, + llm_call_count=res.llm_call_count, + ) + + return await context.coalescer.run_once( + f"cve_researcher:{vuln_id}:{context.output_dir_key(inp_data)}", cve_researcher_impl + ) + + +async def run_container_analyzer(context: PipelineContext, state: PipelineState) -> StageResult: + """Run the shared Container Analyzer stage.""" + inp_data = state["input"] + image_name = inp_data["image_name"] + image_tag = inp_data["image_tag"] + + async def container_analyzer_impl() -> StageResult: + filename = derive_report_filename(image_name, image_tag) + cached = context.check_cache( + context.use_cache_for(inp_data, "container_analyzer"), + inp_data, + "container_reports", + filename, + ) + if cached: + logger.info("Cache hit - skipping Container Analyzer, report at %s", cached) + return StageResult( + agent_name="Container Analyzer", + success=True, + report_path=str(cached), + extra={"phase1_skipped": True}, + ) + + agent_inp = ContainerAnalyzerInput( + image_name=image_name, + image_tag=image_tag, + filesystem_path=inp_data["filesystem_path"], + image_config_path=inp_data.get("image_config_path"), + skip_web_research=inp_data.get("skip_web_research", False), + ) + res = coerce_result( + await context.functions.container_analyzer.ainvoke(agent_inp), + ContainerAnalyzerResult, + {"image_name": agent_inp.image_name, "image_tag": agent_inp.image_tag}, + ) + report_path = context.persist_report(res, inp_data, "container_reports", filename) + + return StageResult( + agent_name="Container Analyzer", + success=res.success, + report_path=report_path if res.success else None, + error=res.error, + tool_call_count=res.tool_call_count, + llm_call_count=res.llm_call_count, + extra={"phase1_skipped": res.phase1_skipped}, + ) + + cache_key = f"container_analyzer:{derive_report_filename(image_name, image_tag)}:{context.output_dir_key(inp_data)}" + return await context.coalescer.run_once(cache_key, container_analyzer_impl) + + +async def stage1_parallel(context: PipelineContext, state: PipelineState) -> dict: + """Fan out CVE researchers and run the shared container analyzer.""" + inp_data = state["input"] + entries = list(state.get("vulns") or inp_data.get("vulns") or []) + + cve_tasks = [] + for entry in entries: + cve_tasks.append( + context.throttled( + context.run_with_retry( + "CVE Researcher", + partial(run_cve_researcher_for, context, entry, inp_data), + ) + ) + ) + all_tasks = cve_tasks + [ + context.run_with_retry("Container Analyzer", partial(run_container_analyzer, context, state)), + ] + results = await asyncio.gather(*all_tasks, return_exceptions=True) + + cve_reports: dict[str, StageResult] = {} + for entry, result in zip(entries, results[:len(entries)]): + identifier = entry["vuln_id"] + cve_reports[identifier] = context.result_or_failure(result, "CVE Researcher") + + container_result = context.result_or_failure(results[-1], "Container Analyzer") + return { + "vulns": entries, + "cve_reports": cve_reports, + "container_report": container_result, + } + + +def check_stage1(state: PipelineState) -> str: + """Continue only when the container report and at least one vulnerability report succeeded.""" + container = state.get("container_report") + cve_reports = state.get("cve_reports", {}) + any_cve_ok = any(r.success for r in cve_reports.values()) + if container and container.success and any_cve_ok: + return "continue" + return "fail" diff --git a/src/vuln_analysis/pipeline/vex.py b/src/vuln_analysis/pipeline/vex.py new file mode 100644 index 000000000..8063b726f --- /dev/null +++ b/src/vuln_analysis/pipeline/vex.py @@ -0,0 +1,143 @@ +"""VEX Categorizer nodes for the CVE pipeline graph.""" + +import asyncio +import logging +from functools import partial +from pathlib import Path + +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.data_models.state import PipelineState +from vuln_analysis.data_models.vex_categorizer import VEXCategorizerInput +from vuln_analysis.data_models.vex_categorizer import VEXCategorizerResult +from vuln_analysis.data_models.vex_categorizer import parse_report_filename +from vuln_analysis.pipeline.context import PipelineContext +from vuln_analysis.pipeline.helpers import coerce_result +from vuln_analysis.pipeline.helpers import write_vex_json +from vuln_analysis.utils.concurrency import atomic_write +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + + +async def run_vex_for( + context: PipelineContext, + identifier: str, + exploit_report: StageResult, + container_report: StageResult, + inp_data: dict, +) -> StageResult: + """Run VEX Categorizer for a single vulnerability.""" + cve_id, container_name = parse_report_filename(exploit_report.report_path) + stem = f"{cve_id}_{container_name}" + + async def vex_impl() -> StageResult: + cached = context.check_cache( + context.use_cache_for(inp_data, "vex_categorizer"), + inp_data, + "vex_results", + f"{stem}.md", + ) + if cached: + logger.info("Cache hit - skipping VEX Categorizer for %s, report at %s", identifier, cached) + content = cached.read_text(encoding="utf-8") + status = extract_markdown_table_field(content, "Status") + justification = extract_markdown_table_field(content, "Justification") + reasoning = extract_markdown_table_field(content, "Reasoning") + cached_json = cached.with_suffix(".json") + return StageResult( + agent_name="VEX Categorizer", + success=True, + report_path=str(cached), + extra={ + "status": status, + "justification": justification, + "reasoning": reasoning, + "json_path": str(cached_json) if cached_json.exists() else None, + }, + ) + + agent_inp = VEXCategorizerInput( + exploitability_report_path=exploit_report.report_path, + container_report_path=container_report.report_path, + ) + res = coerce_result( + await context.functions.vex_categorizer.ainvoke(agent_inp), + VEXCategorizerResult, + {"cve_id": "unknown", "container_name": "unknown"}, + ) + + summary_path = res.summary_report_path + json_path = res.json_path + + if res.success and res.report_content and not summary_path: + root = Path(context.output_dir_key(inp_data)).resolve() + summary_file = root / "vex_results" / f"{stem}.md" + json_file = root / "vex_results" / f"{stem}.json" + atomic_write(summary_file, res.report_content) + summary_path = str(summary_file) + json_path = str(json_file) + write_vex_json( + json_path, + cve_id=res.cve_id, + container_name=res.container_name, + status=res.status, + justification=res.justification, + reasoning=res.reasoning, + report_content=res.report_content, + summary_path=summary_path, + exploitability_report_path=exploit_report.report_path, + ) + + return StageResult( + agent_name="VEX Categorizer", + success=res.success, + report_path=summary_path, + error=res.error, + tool_call_count=res.tool_call_count, + llm_call_count=res.llm_call_count, + extra={ + "status": res.status, + "justification": res.justification, + "reasoning": res.reasoning, + "json_path": json_path, + }, + ) + + return await context.coalescer.run_once(f"vex:{stem}:{context.output_dir_key(inp_data)}", vex_impl) + + +async def vex_node(context: PipelineContext, state: PipelineState) -> dict: + """Fan out VEX Categorizer work for successful exploitability reports.""" + exploit_reports = state["exploit_reports"] + container_report = state["container_report"] + inp_data = state["input"] + + successful = {ident: result for ident, result in exploit_reports.items() if result.success} + identifiers = list(successful.keys()) + coros = [] + for ident in identifiers: + coros.append( + context.throttled( + context.run_with_retry( + "VEX Categorizer", + partial(run_vex_for, context, ident, successful[ident], container_report, inp_data), + ) + ) + ) + results = await asyncio.gather(*coros, return_exceptions=True) + + vex_results: dict[str, StageResult] = {} + vex_statuses: dict[str, str | None] = {} + vex_justifications: dict[str, str | None] = {} + for ident, result in zip(identifiers, results): + stage_result = context.result_or_failure(result, "VEX Categorizer") + vex_results[ident] = stage_result + extra = stage_result.extra or {} + vex_statuses[ident] = extra.get("status") + vex_justifications[ident] = extra.get("justification") + + return { + "vex_results": vex_results, + "vex_statuses": vex_statuses, + "vex_justifications": vex_justifications, + } diff --git a/src/vuln_analysis/pipeline/workflow.py b/src/vuln_analysis/pipeline/workflow.py new file mode 100644 index 000000000..0d5f676bc --- /dev/null +++ b/src/vuln_analysis/pipeline/workflow.py @@ -0,0 +1,185 @@ +"""Pipeline graph construction and top-level response handling.""" + +import asyncio +import logging +import os +import time +from functools import partial +from pathlib import Path + +from vuln_analysis.data_models.input import PipelineInput +from vuln_analysis.data_models.output import CVEResult +from vuln_analysis.data_models.output import PipelineResult +from vuln_analysis.data_models.state import PipelineState +from vuln_analysis.data_models.container_analyzer import compose_pull_ref +from vuln_analysis.pipeline.context import PipelineConfig +from vuln_analysis.pipeline.context import PipelineContext +from vuln_analysis.pipeline.context import PipelineFunctions +from vuln_analysis.pipeline.exploitability import check_stage2 +from vuln_analysis.pipeline.exploitability import exploitability_node +from vuln_analysis.pipeline.final import fail_safe_node +from vuln_analysis.pipeline.final import output_results_node +from vuln_analysis.pipeline.helpers import resolve_pipeline_result_ids +from vuln_analysis.pipeline.stage1 import check_stage1 +from vuln_analysis.pipeline.stage1 import stage1_parallel +from vuln_analysis.pipeline.vex import vex_node +from vuln_analysis.utils.image_extraction import ImageMaterializationError +from vuln_analysis.utils.image_extraction import materialize_image_bounded + +logger = logging.getLogger(__name__) + + +class PipelineWorkflow: + """Compiled graph and response function for the CVE pipeline.""" + + def __init__(self, config: PipelineConfig, functions: PipelineFunctions) -> None: + self.config = config + self.context = PipelineContext(config, functions) + extraction_concurrency = int(os.environ.get("VULN_ANALYSIS_MAX_CONCURRENT_EXTRACTIONS", "2")) + self.extraction_semaphore = asyncio.Semaphore(extraction_concurrency) + logger.info("Image extraction concurrency limit: %d", extraction_concurrency) + self.graph = build_pipeline_graph(self.context) + + async def response(self, inp: PipelineInput) -> PipelineResult: + """Run the pipeline graph and assemble the public result model.""" + start_time = time.monotonic() + entries = inp.vulns + inp_dict = inp.model_dump() + pull_ref = compose_pull_ref(inp.image_name, inp.image_tag) + if not inp.filesystem_path: + try: + materialized = await self._materialize_image(pull_ref) + except ImageMaterializationError as exc: + return self._image_materialization_failure_result(inp, pull_ref, start_time, exc) + inp_dict["filesystem_path"] = str(materialized.rootfs_path) + inp_dict["image_config_path"] = str(materialized.config_path) + inp_dict["image_digest"] = materialized.digest + + initial_state: PipelineState = { + "input": inp_dict, + "vulns": [entry.model_dump() for entry in entries], + "container_report": None, + "cve_reports": {}, + "exploit_reports": {}, + "vex_results": {}, + "vex_statuses": {}, + "vex_justifications": {}, + "success": False, + } + result_state = await self.graph.ainvoke(initial_state) + elapsed = time.monotonic() - start_time + + cve_reports = result_state.get("cve_reports", {}) + exploit_reports = result_state.get("exploit_reports", {}) + vex_results = result_state.get("vex_results", {}) + vex_statuses = result_state.get("vex_statuses", {}) + vex_justifications = result_state.get("vex_justifications", {}) + + cve_result_list: list[CVEResult] = [] + for entry in entries: + ident = entry.vuln_id + vex_res = vex_results.get(ident) + cve_res = cve_reports.get(ident) + exploit_res = exploit_reports.get(ident) + error = next( + (result.error for result in (cve_res, exploit_res, vex_res) if result and not result.success + and result.error), + None, + ) + cve_id, ghsa_id = resolve_pipeline_result_ids(ident, cve_res) + cve_result_list.append( + CVEResult( + vuln_id=ident, + cve_id=cve_id, + ghsa_id=ghsa_id, + success=bool(vex_res and vex_res.success), + cve_researcher=cve_res, + exploitability_analyzer=exploit_res, + vex_categorizer=vex_res, + vex_status=vex_statuses.get(ident), + vex_justification=vex_justifications.get(ident), + error=error, + ) + ) + + return PipelineResult( + image_name=inp.image_name, + image_tag=inp.image_tag, + success=result_state.get("success", False), + container_analyzer=result_state.get("container_report"), + elapsed_seconds=elapsed, + results=cve_result_list, + ) + + async def _materialize_image(self, pull_ref: str): + cache_root = Path(os.environ.get("VULN_ANALYSIS_IMAGE_CACHE", ".cache/vuln_analysis/images")) + return await materialize_image_bounded( + self.extraction_semaphore, + pull_ref, + cache_root, + use_cache=True, + ) + + @staticmethod + def _image_materialization_failure_result( + inp: PipelineInput, + pull_ref: str, + start_time: float, + exc: ImageMaterializationError, + ) -> PipelineResult: + logger.error("Image materialization failed for %s: %s", pull_ref, exc) + failed_results: list[CVEResult] = [] + for entry in inp.vulns: + cve_id, ghsa_id = resolve_pipeline_result_ids(entry.vuln_id) + failed_results.append( + CVEResult( + vuln_id=entry.vuln_id, + cve_id=cve_id, + ghsa_id=ghsa_id, + success=False, + error=str(exc), + ) + ) + return PipelineResult( + image_name=inp.image_name, + image_tag=inp.image_tag, + success=False, + elapsed_seconds=time.monotonic() - start_time, + results=failed_results, + ) + + +def build_pipeline_graph(context: PipelineContext): + """Build and compile the LangGraph pipeline.""" + from langgraph.graph import END + from langgraph.graph import START + from langgraph.graph import StateGraph + + graph_builder = StateGraph(PipelineState) + graph_builder.add_node("stage1_parallel", partial(stage1_parallel, context)) + graph_builder.add_node("exploitability", partial(exploitability_node, context)) + graph_builder.add_node("vex_categorizer", partial(vex_node, context)) + graph_builder.add_node("fail_safe", fail_safe_node) + graph_builder.add_node("output_results", output_results_node) + + graph_builder.add_edge(START, "stage1_parallel") + graph_builder.add_conditional_edges( + "stage1_parallel", + check_stage1, + { + "continue": "exploitability", + "fail": "fail_safe", + }, + ) + graph_builder.add_conditional_edges( + "exploitability", + check_stage2, + { + "continue": "vex_categorizer", + "fail": "fail_safe", + }, + ) + graph_builder.add_edge("vex_categorizer", "output_results") + graph_builder.add_edge("fail_safe", "output_results") + graph_builder.add_edge("output_results", END) + return graph_builder.compile() diff --git a/src/vuln_analysis/plugins/__init__.py b/src/vuln_analysis/plugins/__init__.py new file mode 100644 index 000000000..e161c8ddf --- /dev/null +++ b/src/vuln_analysis/plugins/__init__.py @@ -0,0 +1,2 @@ +"""Local NAT plugin implementations for vulnerability analysis.""" + diff --git a/src/vuln_analysis/plugins/cve_researcher/__init__.py b/src/vuln_analysis/plugins/cve_researcher/__init__.py new file mode 100644 index 000000000..0e6ac6551 --- /dev/null +++ b/src/vuln_analysis/plugins/cve_researcher/__init__.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Local CVE researcher NAT plugin.""" + +from . import register # noqa: F401 +from .agent import CVEResearcherAgent +from .models import CVEResearcherAgentState +from .models import parse_research_prompt +from .register import CVEResearcherAgentConfig +from .register import CVEResearcherConfig +from .register import cve_researcher +from .register import cve_researcher_agent # noqa: F401 + +__all__ = [ + "CVEResearcherAgent", + "CVEResearcherAgentConfig", + "CVEResearcherAgentState", + "CVEResearcherConfig", + "cve_researcher", + "cve_researcher_agent", + "parse_research_prompt", +] diff --git a/src/vuln_analysis/plugins/cve_researcher/agent.py b/src/vuln_analysis/plugins/cve_researcher/agent.py new file mode 100644 index 000000000..ada5da4c3 --- /dev/null +++ b/src/vuln_analysis/plugins/cve_researcher/agent.py @@ -0,0 +1,382 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""DeepAgents CVE researcher.""" + +from __future__ import annotations + +import logging +from collections.abc import Mapping +from collections.abc import Sequence +from typing import Any + +from deepagents import create_deep_agent +from langchain.agents.middleware import ModelRetryMiddleware +from langchain_core.messages import AIMessage +from langchain_core.messages import BaseMessage +from langchain_core.messages import HumanMessage +from langchain_core.messages import ToolMessage +from langchain_core.tools import BaseTool +from langgraph.store.memory import InMemoryStore + +from aiq_agent.agents.deep_researcher.custom_middleware import EmptyContentFixMiddleware +from aiq_agent.agents.deep_researcher.custom_middleware import ToolNameSanitizationMiddleware +from aiq_agent.agents.deep_researcher.custom_middleware import ToolResultPruningMiddleware +from aiq_agent.agents.deep_researcher.custom_middleware import ToolRetryMiddleware +from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepAgentsRuntime +try: + from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepResearchSandboxConfig as SandboxConfig + from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepResearchSkillsConfig as SkillsConfig +except ImportError: # pragma: no cover - compatibility with older AIQ revisions + from aiq_agent.agents.deep_researcher.deepagents_runtime import SandboxConfig + from aiq_agent.agents.deep_researcher.deepagents_runtime import SkillsConfig +from aiq_agent.common import LLMProvider +from aiq_agent.common import LLMRole + +from .models import CVEResearcherAgentState +from .prompts import build_system_prompt +from .prompts import build_user_message + +logger = logging.getLogger(__name__) + +_DEEPAGENTS_BUILTIN_TOOL_NAMES = ( + "task", + "write_todos", + "write_file", + "read_file", + "edit_file", + "ls", + "glob", + "grep", +) + + +def _message_content(message: Any) -> str: + if isinstance(message, BaseMessage): + content = message.content + elif isinstance(message, dict): + content = message.get("content", "") + else: + content = getattr(message, "content", "") + + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for item in content: + if isinstance(item, str): + parts.append(item) + elif isinstance(item, dict): + text = item.get("text") or item.get("content") + if text: + parts.append(str(text)) + return "\n".join(parts) + return str(content or "") + + +def _write_file_tool_content(message: Any) -> str: + tool_calls = getattr(message, "tool_calls", None) + if not tool_calls and isinstance(message, dict): + tool_calls = message.get("tool_calls") + if not tool_calls: + return "" + + best_content = "" + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + if tool_call.get("name") != "write_file": + continue + args = tool_call.get("args") or {} + content = args.get("content") if isinstance(args, dict) else None + if isinstance(content, str) and len(content) > len(best_content): + best_content = content + return best_content.strip() + + +def _extract_final_content(result: Any) -> str: + if isinstance(result, dict): + structured = result.get("structured_response") + if structured: + return str(structured).strip() + + messages = result.get("messages") or [] + for message in reversed(messages): + if isinstance(message, AIMessage) or ( + isinstance(message, dict) and message.get("role") in {"assistant", "ai"} + ): + content = _message_content(message).strip() + if content: + return content + file_content = _write_file_tool_content(message) + if file_content: + return file_content + + for message in reversed(messages): + content = _message_content(message).strip() + if content: + return content + + return _message_content(result).strip() + + +def _count_agent_activity(result: Any) -> tuple[int, int]: + messages = result.get("messages") if isinstance(result, dict) else getattr(result, "messages", None) + if not isinstance(messages, Sequence) or isinstance(messages, (str, bytes)): + return 0, 0 + + tool_call_count = 0 + assistant_tool_calls = 0 + llm_call_count = 0 + + for message in messages: + if isinstance(message, AIMessage): + llm_call_count += 1 + assistant_tool_calls += len(message.tool_calls or []) + elif isinstance(message, ToolMessage): + tool_call_count += 1 + elif isinstance(message, dict): + role = message.get("role") + if role in {"assistant", "ai"}: + llm_call_count += 1 + assistant_tool_calls += len(message.get("tool_calls") or []) + elif role == "tool": + tool_call_count += 1 + + if not tool_call_count: + tool_call_count = assistant_tool_calls + return tool_call_count, llm_call_count + + +def _build_research_subagents( + tools: Sequence[BaseTool], + llm: Any, + middleware: Sequence[Any] | None = None, +) -> list[dict[str, Any]]: + """Create focused DeepAgents subagents for context-isolated CVE research.""" + + web_tools = [tool for tool in tools if tool.name in {"web_search", "web_fetch"}] + all_tools = list(tools) + subagent_middleware = list(middleware or []) + + return [ + { + "name": "advisory-researcher", + "description": ( + "Finds primary CVE, GHSA, vendor, NVD, OSV, and patch sources; " + "extracts affected packages, vulnerable ranges, fixed versions, " + "and vulnerable functions or APIs." + ), + "system_prompt": ( + "You are a vulnerability advisory researcher. Use exact quoted " + "identifier searches, prefer vendor advisories and patch pages, " + "and return concise findings with source URLs. Focus on affected " + "packages, version ranges, first fixed versions per branch, and " + "the exact vulnerable function or API." + ), + "tools": all_tools, + "model": llm, + "middleware": subagent_middleware, + }, + { + "name": "exploit-intel-researcher", + "description": ( + "Researches exploit availability, PoCs, CISA KEV status, EPSS, " + "attack prerequisites, and observed exploitation for the CVE." + ), + "system_prompt": ( + "You are an exploit intelligence researcher. Search for public " + "PoCs, exploit writeups, KEV status, EPSS references, and attack " + "preconditions. Return concise findings with confidence and URLs." + ), + "tools": web_tools or all_tools, + "model": llm, + "middleware": subagent_middleware, + }, + ] + + +class CVEResearcherAgent: + """DeepAgents-backed CVE vulnerability researcher.""" + + def __init__( + self, + llm_provider: LLMProvider | Any | None = None, + tools: Sequence[BaseTool] | None = None, + *, + llm: Any | None = None, + max_iterations: int = 2, + verbose: bool = True, + callbacks: list[Any] | None = None, + skills: SkillsConfig | None = None, + sandbox: SandboxConfig | None = None, + include_middleware: bool = True, + job_id: str | None = None, + ) -> None: + if llm_provider is not None and not isinstance(llm_provider, LLMProvider): + llm = llm_provider + llm_provider = None + + if llm_provider is None: + if llm is None: + raise ValueError("CVEResearcherAgent requires llm_provider or llm.") + llm_provider = LLMProvider() + llm_provider.set_default(llm) + llm_provider.configure(LLMRole.ORCHESTRATOR, llm) + llm_provider.configure(LLMRole.RESEARCHER, llm) + + self.llm_provider = llm_provider + self.tools = list(tools) if tools else [] + self.max_iterations = max_iterations + self.verbose = verbose + self.callbacks = callbacks or [] + self.include_middleware = include_middleware + self.deepagents_runtime = DeepAgentsRuntime(skills=skills, sandbox=sandbox, job_id=job_id) + self.tools_info = [ + { + "name": getattr(tool, "name", str(tool)), + "description": getattr(tool, "description", "No description available"), + } + for tool in self.tools + ] + + valid_tool_names = [tool.name for tool in self.tools] + valid_tool_names.extend(_DEEPAGENTS_BUILTIN_TOOL_NAMES) + self.middleware = ( + [ + EmptyContentFixMiddleware(), + ToolNameSanitizationMiddleware(valid_tool_names=valid_tool_names), + ToolRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0), + ToolResultPruningMiddleware(keep_last_n=10, max_chars=3000), + ModelRetryMiddleware(max_retries=10, backoff_factor=2.0, initial_delay=1.0), + ] + if include_middleware + else [] + ) + + if self.verbose: + logger.info("CVE researcher tools configured: %d", len(self.tools)) + + def _normalize_state( + self, + state: CVEResearcherAgentState, + ) -> CVEResearcherAgentState: + state = CVEResearcherAgentState.model_validate(state) + messages = list(state.messages) + user_message = HumanMessage(content=build_user_message(state)) + + if messages and isinstance(messages[-1], HumanMessage): + messages[-1] = user_message + else: + messages.append(user_message) + + return state.model_copy( + update={ + "messages": messages, + "tools_info": state.tools_info or self.tools_info, + } + ) + + def _build_agent(self, state: CVEResearcherAgentState) -> Any: + create_agent_kwargs: dict[str, Any] = {"backend": self.deepagents_runtime.backend} + skill_sources = self.deepagents_runtime.skill_sources_for("cve-researcher") + if skill_sources is not None: + create_agent_kwargs["skills"] = skill_sources + + agent = create_deep_agent( + model=self.llm_provider.get(LLMRole.ORCHESTRATOR), + tools=self.tools, + system_prompt=build_system_prompt(), + subagents=_build_research_subagents( + self.tools, + self.llm_provider.get(LLMRole.RESEARCHER), + self.middleware, + ), + store=InMemoryStore(), + context_schema=CVEResearcherAgentState, + middleware=self.middleware, + name="cve-researcher", + **create_agent_kwargs, + ) + return agent.with_config({"recursion_limit": max(2, self.max_iterations * 2)}) + + @staticmethod + def _result_mapping(result: Any) -> dict[str, Any]: + if isinstance(result, dict): + return dict(result) + if hasattr(result, "model_dump"): + return result.model_dump() + return dict(result) + + @staticmethod + def _replace_last_message_content(messages: list[Any], content: str) -> list[Any]: + if not messages: + return [AIMessage(content=content)] + + updated = list(messages) + last_message = updated[-1] + if hasattr(last_message, "model_copy"): + updated[-1] = last_message.model_copy(update={"content": content}) + elif isinstance(last_message, Mapping): + updated[-1] = {**last_message, "content": content} + else: + updated.append(AIMessage(content=content)) + return updated + + async def run( + self, + state: CVEResearcherAgentState, + ) -> CVEResearcherAgentState: + """Run CVE research and return updated CVE researcher state.""" + + state = self._normalize_state(state) + prepare_state = getattr(self.deepagents_runtime, "prepare_state", None) + if callable(prepare_state): + state = prepare_state(state) + else: + prepared_files = self.deepagents_runtime.prepare_state_files(dict(state.files)) + if prepared_files != state.files: + state = state.model_copy(update={"files": prepared_files}) + identifier = state.identifier + logger.info("Starting CVE research for %s", identifier) + + agent = self._build_agent(state) + result = await agent.ainvoke( + state, + config={"callbacks": self.callbacks} if self.callbacks else None, + ) + content = _extract_final_content(result) + tool_calls, llm_calls = _count_agent_activity(result) + + data = self._result_mapping(result) + messages = list(data.get("messages") or state.messages) + if content: + messages = self._replace_last_message_content(messages, content) + data.update( + { + "messages": messages, + "cve_id": state.cve_id, + "ghsa_id": state.ghsa_id, + "package_hint": state.package_hint, + "version_hint": state.version_hint, + "tools_info": state.tools_info, + "report_content": content or None, + "tool_call_count": tool_calls, + "llm_call_count": llm_calls, + } + ) + + if content: + logger.info( + "CVE research complete: %s (%d chars, %d tool calls, %d LLM calls)", + identifier, + len(content), + tool_calls, + llm_calls, + ) + return CVEResearcherAgentState.model_validate(data) + + +__all__ = [ + "CVEResearcherAgent", + "_build_research_subagents", +] diff --git a/src/vuln_analysis/plugins/cve_researcher/models/__init__.py b/src/vuln_analysis/plugins/cve_researcher/models/__init__.py new file mode 100644 index 000000000..55eff9e2c --- /dev/null +++ b/src/vuln_analysis/plugins/cve_researcher/models/__init__.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Data models for the CVE researcher agent.""" + +from __future__ import annotations + +import re + +_CVE_RE = re.compile(r"(?[@a-zA-Z0-9][a-zA-Z0-9@._+/:~-]*)`?", + re.IGNORECASE, +) +_VERSION_HINT_RE = re.compile( + r"\b(?:installed\s+|affected\s+|detected\s+)?(?:version|ver|tag)" + r"\s*(?:is|=|:)?\s*" + r"`?(?P[a-zA-Z0-9][a-zA-Z0-9._+~:-]*)`?", + re.IGNORECASE, +) +_PACKAGE_VERSION_RE = re.compile( + r"\b(?P@?[a-zA-Z][a-zA-Z0-9._+/-]*)" + r"(?:==|=|@|:|\s+v(?:ersion)?\s+|\s+)" + r"(?Pv?\d+(?:[._]\d+)+(?:[-+~._a-zA-Z0-9]*)?)\b", + re.IGNORECASE, +) +_STOPWORDS = { + "a", + "about", + "affected", + "against", + "analyze", + "and", + "for", + "in", + "installed", + "package", + "pkg", + "please", + "research", + "the", + "this", + "version", + "vulnerability", + "with", +} + + +def _clean_hint(value: str) -> str: + cleaned = value.strip().strip("`'\" \t\r\n,;.)]}") + cleaned = cleaned.removeprefix("(").removeprefix("[").removeprefix("{") + if cleaned.lower().endswith(".md"): + cleaned = cleaned[:-3] + return cleaned + + +def _normalize_ghsa(value: str) -> str: + parts = value.split("-") + return "-".join([parts[0].upper(), *(part.lower() for part in parts[1:])]) + + +def _looks_like_version(value: str) -> bool: + cleaned = _clean_hint(value) + return bool(_VERSION_RE.fullmatch(cleaned)) or cleaned.lower() == "latest" + + +def _identifierless_prompt(prompt: str) -> str: + without_ids = _CVE_RE.sub(" ", prompt) + without_ids = _GHSA_RE.sub(" ", without_ids) + return without_ids.replace("_", " ") + + +def _prompt_tokens(prompt: str) -> list[str]: + return [ + _clean_hint(match.group(1)) + for match in _HINT_TOKEN_RE.finditer(_identifierless_prompt(prompt)) + if _clean_hint(match.group(1)) + ] + + +def _is_package_candidate(value: str) -> bool: + lowered = value.lower() + return ( + lowered not in _STOPWORDS + and not _looks_like_version(value) + and not _CVE_RE.fullmatch(value) + ) + + +def parse_research_prompt(prompt: str) -> dict[str, str]: + """Extract CVE/GHSA identifiers plus package and version hints from a prompt.""" + + parsed: dict[str, str] = {} + + cve_match = _CVE_RE.search(prompt) + if cve_match: + parsed["cve_id"] = cve_match.group(0).upper() + + ghsa_match = _GHSA_RE.search(prompt) + if ghsa_match: + parsed["ghsa_id"] = _normalize_ghsa(ghsa_match.group(0)) + + package_match = _PACKAGE_HINT_RE.search(prompt) + if package_match: + parsed["package_hint"] = _clean_hint(package_match.group("package")) + + version_match = _VERSION_HINT_RE.search(prompt) + if version_match: + parsed["version_hint"] = _clean_hint(version_match.group("version")) + + tokens = _prompt_tokens(prompt) + + if "package_hint" not in parsed or "version_hint" not in parsed: + for token in tokens: + if ":" not in token or token.startswith(("http:", "https:")): + continue + package, version = token.rsplit(":", 1) + if package and version and _looks_like_version(version): + parsed.setdefault("package_hint", _clean_hint(package)) + parsed.setdefault("version_hint", _clean_hint(version)) + break + + if "package_hint" not in parsed or "version_hint" not in parsed: + for match in _PACKAGE_VERSION_RE.finditer(_identifierless_prompt(prompt)): + package = _clean_hint(match.group("package")) + version = _clean_hint(match.group("version")) + if _is_package_candidate(package) and _looks_like_version(version): + parsed.setdefault("package_hint", package) + parsed.setdefault("version_hint", version) + break + + if "version_hint" not in parsed: + for token in tokens: + if _looks_like_version(token): + parsed["version_hint"] = token + break + + if "package_hint" not in parsed and "version_hint" in parsed: + version = parsed["version_hint"] + for index, token in enumerate(tokens): + if token != version: + continue + for candidate in reversed(tokens[:index]): + if _is_package_candidate(candidate): + parsed["package_hint"] = candidate + break + if "package_hint" in parsed: + break + + return parsed + + +from .state import CVEResearcherAgentState # noqa: E402 + +__all__ = [ + "CVEResearcherAgentState", + "parse_research_prompt", +] diff --git a/src/vuln_analysis/plugins/cve_researcher/models/state.py b/src/vuln_analysis/plugins/cve_researcher/models/state.py new file mode 100644 index 000000000..5fb9a25a0 --- /dev/null +++ b/src/vuln_analysis/plugins/cve_researcher/models/state.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""State models for the CVE researcher agent.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Annotated +from typing import Any + +from langchain_core.messages import AnyMessage +from langchain_core.messages import BaseMessage +from langchain_core.messages import HumanMessage +from langgraph.graph.message import add_messages +from pydantic import BaseModel +from pydantic import Field +from pydantic import model_validator + +from aiq_agent.knowledge import AvailableDocument + + +def _merge_dict_state(left: dict[str, Any] | None, right: dict[str, Any] | None) -> dict[str, Any]: + if not left: + return right or {} + if not right: + return left + merged = dict(left) + merged.update(right) + return merged + + +def _message_content(message: Any) -> str: + if isinstance(message, BaseMessage): + content = message.content + elif isinstance(message, Mapping): + content = message.get("content", "") + else: + content = getattr(message, "content", "") + + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for item in content: + if isinstance(item, str): + parts.append(item) + elif isinstance(item, Mapping): + text = item.get("text") or item.get("content") + if text: + parts.append(str(text)) + return "\n".join(parts) + return str(content or "") + + +def _latest_message_content(messages: list[AnyMessage]) -> str: + for message in reversed(messages): + content = _message_content(message).strip() + if content: + return content + return "" + + +class CVEResearcherAgentState(BaseModel): + """State for the CVE researcher DeepAgents workflow.""" + + messages: Annotated[list[AnyMessage], add_messages] = Field(default_factory=list) + data_sources: list[str] | None = None + user_info: dict[str, Any] | None = None + tools_info: list[dict[str, Any]] | None = None + todos: list[dict[str, Any]] = Field(default_factory=list) + files: Annotated[dict[str, Any], _merge_dict_state] = Field(default_factory=dict) + subagents: list[dict[str, Any]] = Field(default_factory=list) + available_documents: list[AvailableDocument] | None = None + cve_id: str | None = None + ghsa_id: str | None = None + package_hint: str | None = None + version_hint: str | None = None + report_content: str | None = None + tool_call_count: int = Field(default=0, ge=0) + llm_call_count: int = Field(default=0, ge=0) + + @model_validator(mode="before") + @classmethod + def _coerce_input(cls, data: Any) -> Any: + if isinstance(data, str): + data = {"messages": [HumanMessage(content=data)]} + + if isinstance(data, Mapping): + from . import parse_research_prompt + + values = dict(data) + message_content = _latest_message_content(values.get("messages") or []) + if message_content: + parsed = parse_research_prompt(message_content) + for key, value in parsed.items(): + if not values.get(key): + values[key] = value + return values + + return data + + @model_validator(mode="after") + def _populate_identifiers(self) -> CVEResearcherAgentState: + from . import parse_research_prompt + + message_content = _latest_message_content(self.messages) + if message_content: + parsed = parse_research_prompt(message_content) + self.cve_id = self.cve_id or parsed.get("cve_id") + self.ghsa_id = self.ghsa_id or parsed.get("ghsa_id") + self.package_hint = self.package_hint or parsed.get("package_hint") + self.version_hint = self.version_hint or parsed.get("version_hint") + + if not self.cve_id and not self.ghsa_id: + raise ValueError("At least one CVE or GHSA identifier must be provided.") + return self + + @property + def identifier(self) -> str: + """Return the primary vulnerability identifier.""" + + return self.cve_id or self.ghsa_id or "" + + +__all__ = [ + "CVEResearcherAgentState", +] diff --git a/src/vuln_analysis/plugins/cve_researcher/prompts.py b/src/vuln_analysis/plugins/cve_researcher/prompts.py new file mode 100644 index 000000000..79df18e43 --- /dev/null +++ b/src/vuln_analysis/plugins/cve_researcher/prompts.py @@ -0,0 +1,416 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Prompt construction for the CVE researcher.""" + +from __future__ import annotations + +from .models import CVEResearcherAgentState + +_REPORT_FINALIZER = ( + "You are the final synthesis agent for CVE vulnerability research. Use the " + "available research tools and any delegated subagent results to produce the " + "complete vulnerability report requested by the user. Do not stop at a plan " + "or a summary of work performed. Delegate advisory, version, fix, and patch " + "research to advisory-researcher when useful. Delegate exploit, PoC, CISA " + "KEV, EPSS, and exploitation-prerequisite research to exploit-intel-researcher " + "when useful. Synthesize the final report yourself." +) + + +SYSTEM_PROMPT = r"""You are a **CVE Vulnerability Researcher**. Your mission is to research a given +CVE (or GHSA) identifier and produce a comprehensive vulnerability report by +searching the web and fetching advisory pages. + +You have three tools available: +- **web_search(query)** – Search the web. Returns summarized results. Best for + NVD, MITRE, CISA KEV, GitHub advisory index. +- **web_fetch(url)** – Fetch a web page as markdown. Best for vendor advisory + pages, GitHub commits/PRs, GitLab advisories, Wiz.io. +- **osv_lookup(cve_id, version, package_name?)** – Check whether a specific + package version is affected by a CVE using OSV.dev data. Returns + AFFECTED/NOT AFFECTED with the nearest unaffected version. Use this in Step 2 + to verify fix versions. + +--- + +## CRITICAL INSTRUCTION + +**Always attempt to identify the specific vulnerable function or API.** This is +the single most important field for downstream analysis. Without it (e.g., +`tarfile.extractall()`, `torch.load()`, `PackageIndex.download()`), the +downstream Exploitability Analyzer cannot determine if the vulnerability is +reachable in a specific container. Dig into patches, commits, and code changes +to find the exact function. + +--- + +## WebSearch / WebFetch Best Practices + +### WebSearch Tips +1. **Always use exact-match quotes** for vulnerability identifiers: + - Good: `"CVE-2025-12345"` or `"GHSA-xxxx-yyyy-zzzz"` + - Bad: `CVE-2025-12345` (returns unrelated results) +2. **WebSearch summaries are often more reliable** than WebFetch for pages that + render poorly (NVD, GitHub). The search result summary frequently contains + structured CVE data. +3. **Run multiple searches in parallel** when gathering initial intelligence – call multiple web_search tools at once. + +### WebFetch Tips +1. **Pages that render well** (prefer WebFetch): + - Vendor-specific security advisory pages: `github.com/[vendor]/[repo]/security/advisories/GHSA-xxx` + - Wiz.io vulnerability pages + - GitLab advisory pages + - GitHub commit and PR pages +2. **Pages that render poorly** (prefer WebSearch): + - `github.com/advisories/GHSA-xxx` – returns minimal content + - `nvd.nist.gov/vuln/detail/CVE-xxx` – partial content with UI artifacts + - CISA KEV catalog pages + - `cve.org/CVERecord?id=CVE-xxx` – may have rendering issues +3. **If WebFetch returns minimal content**, fall back to WebSearch with specific queries. +4. **Verify that referenced patches were actually adopted.** When you fetch a GitHub PR + or commit as evidence of a fix, confirm it was merged/released — not just proposed or + closed. A closed-but-not-merged PR, a reverted commit, or a superseded patch does NOT + constitute a fix. If a PR was closed in favor of another, follow the reference to the + successor and verify that one instead. + +--- + +## Research Workflow + +### Step 0: Handle GHSA Identifiers + +If the input includes a GHSA ID instead of (or in addition to) a CVE: +1. Search for `"GHSA-xxxx-yyyy-zzzz" security advisory` to find the associated CVE ID. +2. Search for the vendor-specific advisory page: `"GHSA-xxxx-yyyy-zzzz" site:github.com/[vendor]` +3. WebFetch the vendor page at + `github.com/[vendor]/[repo]/security/advisories/GHSA-xxx` (NOT the generic + `github.com/advisories/` page). +4. Once you have the CVE ID, proceed with the standard workflow. + +**Note:** NVD only accepts CVE IDs, not GHSA IDs. Always convert GHSA → CVE before querying NVD. + +### Step 1: Gather CVE Details (Run Searches in Parallel) + +#### 1.1 Core CVE Information +Run these searches **simultaneously** (multiple web_search calls at once): +- `"CVE-YYYY-NNNNN" site:nvd.nist.gov OR site:cve.org` +- `"CVE-YYYY-NNNNN" vulnerability details` +- `"CVE-YYYY-NNNNN" CVSS CWE affected versions` + +Extract: description, CVSS score/vector, CWE ID, affected versions, fixed versions. + +**IMPORTANT — Per-branch fix versions:** Many projects (CPython, OpenSSL, curl, Go, +Node.js, etc.) maintain multiple active release branches and backport security fixes to +each. When you find a fix version, always check whether the fix was also backported to +older maintained branches: +- Search: `"CVE-YYYY-NNNNN" release notes OR changelog` for each active branch +- Check the upstream project's release page (e.g., python.org/downloads, openssl.org) +- Look at GitHub/GitLab backport PRs (often titled "[3.12] backport …" or similar) +- Document the **first fixed version per branch** in the Affected Version Ranges table. + Do NOT write a single "fixed in X.Y.Z and later" when there are backports — always + list each branch separately with its exact vulnerable range and first fixed version. + +**CRITICAL — NVD/CPE "all versions below X" ranges:** Pay close attention to the NVD CPE +configuration and the CVE JSON `versions` array. If you see a range like +`"version": "0", "lessThan": "3.13.11"` or CPE `versions up to (excluding) 3.13.11`, +this means **ALL versions from 0 to < 3.13.11 are affected** — including 3.12.x, 3.11.x, +3.10.x, etc. Do NOT list only the branches that have upstream fixes. You MUST include a +catch-all row in the Affected Version Ranges table for older branches that have no +upstream fix. Use `No upstream fix` in the First Fixed Version column. Example: +`| <= 3.12 (all older branches) | All versions | No upstream fix |` +Distro vendors (Red Hat, Ubuntu, SUSE) may independently backport fixes to these older +branches, but from an upstream perspective, those versions remain unpatched. + +#### 1.2 Vendor Advisories +- `"CVE-YYYY-NNNNN" [vendor-name] advisory OR security bulletin` +- For GitHub-hosted projects, fetch the vendor's advisory page at + `github.com/[vendor]/[repo]/security/advisories/GHSA-xxx` +- Alternative sources that render well: GitLab advisories, Wiz.io, Vulert + +**CRITICAL — Multiple affected packages:** A single CVE often affects multiple +packages or modules. For example, a runc CVE (CVE-2025-52881) also lists +`github.com/opencontainers/selinux` as a separately affected package with its +own vulnerable version range and fix version. When fetching the GHSA vendor +advisory, check the "Affected versions" / "Affected packages" section for ALL +listed packages — not just the primary one. Also check GitLab advisories and +Snyk which may list additional affected packages. Document every affected +package in the **Affected Packages** table in the report. + +#### 1.3 Exploit Intelligence (Run Searches in Parallel) +- `"CVE-YYYY-NNNNN" exploit OR PoC OR proof-of-concept` +- `"CVE-YYYY-NNNNN" site:github.com exploit` +- `"CVE-YYYY-NNNNN" CISA KEV known exploited` + +Document: public exploit/PoC availability, active exploitation (CISA KEV), EPSS scores, exploitation prerequisites. + +### Step 2: Verify Fix Versions + +After gathering CVE details in Step 1, verify the fix versions you found before +proceeding. This step catches off-by-one version errors that are common with +multi-branch projects. + +**When to verify:** Always verify when the affected project maintains multiple +release branches (CPython, OpenSSL, curl, Node.js, Go, Linux kernel, etc.). +For single-branch packages with a clear vendor-stated fix version, you may skip +this step. + +**How to verify:** +1. For each branch where you identified a fix version, call: + `osv_lookup(cve_id="CVE-YYYY-NNNNN", version="")` + - If the tool returns **AFFECTED**, your fix version is wrong. The tool will + suggest the nearest unaffected version — use that instead. + - If the tool returns **NOT AFFECTED**, your fix version is confirmed. + - If the tool returns **AFFECTED (inferred)**, it means the branch has + pre-release versions marked as affected but OSV does not track release + versions for this branch. There is no known fix for this branch. Do NOT + claim a fix version exists — instead, use `No upstream fix` or verify + independently via upstream release notes or a merged backport PR. + - If the tool returns **UNCERTAIN (INCOMPLETE DATA)**, OSV has no version + data for this branch at all. Fall back to distro security trackers or + upstream changelogs. + - If the tool returns **NO OSV DATA**, fall back to checking a distro security + tracker (Ubuntu, Debian, or Red Hat) or the upstream project changelog. + +2. **Temporal sanity check:** Verify that each claimed fix version was released + AFTER the CVE disclosure date. A version released before disclosure cannot + contain the fix unless it was a silent pre-disclosure fix (rare — note it + explicitly if so). + +Revise your fix versions before proceeding to Step 3. + +### Step 3: Analyze Technical Details + +#### 3.1 Identify the Vulnerable Component +- Search: `"CVE-YYYY-NNNNN" vulnerable function OR affected API OR root cause` +- Search: `"CVE-YYYY-NNNNN" [package-name] patch OR commit OR fix` +- WebFetch GitHub commit/PR pages to see the actual code changes +- Extract: specific vulnerable functions/methods, vulnerable code patterns, root cause, what the fix changes + +#### 3.2 Understand the Attack Vector +From CVE details, document: Attack Vector, Attack Complexity, Privileges +Required, User Interaction, Scope, Impact (CIA). + +#### 3.3 Determine Exploitation Requirements +Research prerequisites, trigger conditions, and exploitation difficulty. + +### Step 4: Produce the Vulnerability Report + +After gathering all intelligence, produce the complete report as your final +response. The report MUST follow the exact template below. **Output the full +markdown report as your final message — do not summarize or abbreviate.** + +**Commit hash rule:** Only include commit hashes that you have fetched and +verified via `web_fetch`. If you cannot find the commit hash for a branch, +write "N/A" — never fabricate commit hashes. + +--- + +## Output Format Template + +Use this exact markdown structure for the report: + +``` +# CVE Analysis: CVE-YYYY-NNNNN + +## Overview + +| Field | Value | +|-------|-------| +| **CVE ID** | CVE-YYYY-NNNNN | +| **GHSA ID** | GHSA-xxxx-yyyy-zzzz (if applicable) | +| **Affected Package** | [name] | +| **Vulnerable Versions** | [version range — see Affected Version Ranges below for per-branch details] | +| **Fixed In** | [first fixed version per branch — see Affected Version Ranges below] | +| **CVSS v3.1** | [score] [severity] | +| **CVSS Vector** | [vector string] | +| **CWE** | [CWE-ID] - [weakness name] | +| **Disclosure Date** | [date] | + +### Affected Version Ranges + +List EACH affected branch separately so downstream agents can match an installed +version to the correct branch. Always use ">=" and "<" notation for unambiguous +machine-readable ranges. Every fix version MUST be verified via Step 2 (osv_lookup) +before finalizing. + +Case A — Backported fixes across branches: + +| Branch | Vulnerable Range | First Fixed Version | +|--------|-----------------|---------------------| +| X.Y | >= X.Y.0, < X.Y.Z | X.Y.Z | +| X.W | >= X.W.0, < X.W.V | X.W.V | + +Case B — No backport to older branches: + +| Branch | Vulnerable Range | First Fixed Version | +|--------|-----------------|---------------------| +| X.Y | >= X.Y.0, < X.Y.Z | X.Y.Z | +| **<= X.W (all older branches)** | **All versions** | **No upstream fix** | + +The catch-all row is CRITICAL when the NVD/CPE range covers older branches. + +### Affected Packages + +A single CVE may affect multiple packages or modules (e.g., a container +runtime CVE may also affect a vendored dependency that received a separate +fix). List ALL affected packages here. The downstream Exploitability Analyzer +searches for each one independently, so missing a package causes false +negatives. + +Check the GHSA "Affected versions" section, GitLab advisories, and Snyk — +they often list multiple packages with separate version ranges. + +| Package | Ecosystem | Vulnerable Versions | Fixed In | +|---------|-----------|---------------------|----------| +| [full canonical package name] | [Go / PyPI / npm / Maven / crate / C/deb / etc.] | [version range] | [fixed version] | + +If only one package is affected, this table will have a single row. Always +include at least the primary package from the Overview table. + +## Description + +[Concise description of what the vulnerability is and how it works] + +## Technical Details + +### Vulnerable Component + +- **File:** [path/to/file] +- **Function:** [function name] + +### Root Cause + +[Detailed explanation of what causes the vulnerability] + +### Attack Vector Analysis + +| Factor | Value | Explanation | +|--------|-------|-------------| +| **Attack Vector** | Network / Adjacent / Local / Physical | [brief explanation] | +| **Attack Complexity** | Low / High | [brief explanation] | +| **Privileges Required** | None / Low / High | [brief explanation] | +| **User Interaction** | None / Required | [brief explanation] | +| **Scope** | Unchanged / Changed | [brief explanation] | +| **Confidentiality Impact** | None / Low / High | [brief explanation] | +| **Integrity Impact** | None / Low / High | [brief explanation] | +| **Availability Impact** | None / Low / High | [brief explanation] | + +## Exploit Intelligence + +| Indicator | Status | +|-----------|--------| +| **Public Exploit** | Yes/No | +| **PoC Available** | Yes/No | +| **Active Exploitation (CISA KEV)** | Yes/No | +| **EPSS Percentile** | [value]% | +| **EPSS Probability** | [value]% | + +### Exploitation Requirements + +- **Prerequisites:** [required conditions] +- **Privileges Required:** [none / low / high] +- **User Interaction:** [none / required] +- **Exploitation Difficulty:** [Low / Medium / High] + +## Impact + +[Description of the potential impact if exploited] + +## Remediation + +### Patch + +[Fixed version and patch reference] + +### Workarounds + +[Available mitigations if immediate patching is not possible] + +### Recommendation + +[Patch / Mitigate / Monitor with justification] + +## References + +- [NVD Entry](link) +- [GitHub Security Advisory](link) +- [Vendor Advisory](link) +- [Other relevant links] + +## Credits + +- **Reporter:** [name/organization] +- **Other credits:** [if applicable] +``` + +--- + +## Key Sources Reference + +### Sources That WebFetch Well +| Source | URL Pattern | Information | +|--------|-------------|-------------| +| GitHub Vendor Advisory | `github.com/[vendor]/[repo]/security/advisories/GHSA-xxx` | Detailed advisory, patches | +| GitHub Commits | `github.com/[vendor]/[repo]/commit/[hash]` | Patch details | +| GitLab Advisory | `advisories.gitlab.com/pkg/[ecosystem]/[package]/CVE-xxx` | Structured CVE data | +| Wiz.io | `wiz.io/vulnerability-database/cve/cve-xxx` | Analysis, mitigation | +| Vulert | `vulert.com/vuln-db/CVE-xxx` | Package-specific details | + +### Sources to Use WebSearch For (Don't WebFetch) +| Source | URL Pattern | Why WebSearch is Better | +|--------|-------------|------------------------| +| NVD | `nvd.nist.gov/vuln/detail/CVE-xxx` | Page renders with UI artifacts; search summaries contain CVSS/CWE | +| GitHub Advisory Index | `github.com/advisories/GHSA-xxx` | Returns minimal content; use vendor-specific page instead | +| CISA KEV | `cisa.gov/known-exploited-vulnerabilities-catalog` | Search for KEV status directly | +| MITRE | `cve.org/CVERecord?id=CVE-xxx` | May have rendering issues | + +### Distribution-Specific Sources +| Source | URL Pattern | Information | +|--------|-------------|-------------| +| Red Hat | `access.redhat.com/security/cve/CVE-xxx` | RHEL-specific analysis | +| Ubuntu | `ubuntu.com/security/CVE-xxx` | Ubuntu-specific patches | +| Debian | `security-tracker.debian.org/tracker/CVE-xxx` | Debian package status | +""" + + +def build_system_prompt() -> str: + """Return the CVE researcher system prompt with final synthesis guardrails.""" + + return f"{SYSTEM_PROMPT}\n\n{_REPORT_FINALIZER}" + + +def build_user_message(state: CVEResearcherAgentState) -> str: + """Construct the user message for a CVE/GHSA research run.""" + + parts: list[str] = [] + if state.cve_id and state.ghsa_id: + parts.append(f"Research vulnerability **{state.cve_id}** (also known as **{state.ghsa_id}**).") + elif state.cve_id: + parts.append(f"Research vulnerability **{state.cve_id}**.") + else: + parts.append( + f"Research vulnerability **{state.ghsa_id}**. " + "This is a GitHub Security Advisory ID; start by finding the associated CVE ID." + ) + + if state.package_hint or state.version_hint: + hints: list[str] = [] + if state.package_hint: + hints.append(f"affected package is **{state.package_hint}**") + if state.version_hint: + hints.append(f"installed version is **{state.version_hint}**") + parts.append(f"Scanner hint: {', '.join(hints)}. Use this information to focus your search.") + + parts.append( + "Once you have gathered all intelligence, produce the full vulnerability " + "report as the final response using the required template. Do not " + "summarize or abbreviate the markdown report." + ) + return "\n\n".join(parts) + + +__all__ = [ + "SYSTEM_PROMPT", + "build_system_prompt", + "build_user_message", +] diff --git a/src/vuln_analysis/plugins/cve_researcher/register.py b/src/vuln_analysis/plugins/cve_researcher/register.py new file mode 100644 index 000000000..dcd86fe74 --- /dev/null +++ b/src/vuln_analysis/plugins/cve_researcher/register.py @@ -0,0 +1,357 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""NAT registration for the local CVE researcher plugin.""" + +from __future__ import annotations + +import logging +from typing import Any + +from langchain_core.messages import AIMessage +from langchain_core.tools import BaseTool +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import FunctionGroupRef +from nat.data_models.component_ref import FunctionRef +from nat.data_models.component_ref import LLMRef +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +try: + from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepResearchSandboxConfig as SandboxConfig + from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepResearchSkillsConfig as SkillsConfig +except ImportError: # pragma: no cover - compatibility with older AIQ revisions + from aiq_agent.agents.deep_researcher.deepagents_runtime import SandboxConfig + from aiq_agent.agents.deep_researcher.deepagents_runtime import SkillsConfig +from aiq_agent.common import LLMProvider +from aiq_agent.common import LLMRole +from aiq_agent.common import VerboseTraceCallback +from aiq_agent.common import all_mapped_tools_filtered_out +from aiq_agent.common import filter_tools_by_sources +from aiq_agent.common import format_user_facing_tool_error +from aiq_agent.common import is_verbose +from aiq_agent.common import validate_tool_availability + +from vuln_analysis.data_models.cve_researcher import CVEResearcherInput +from vuln_analysis.data_models.cve_researcher import CVEResearcherResult + +from .agent import CVEResearcherAgent +from .models import CVEResearcherAgentState + +logger = logging.getLogger(__name__) + + +class CVEResearcherConfig(FunctionBaseConfig, name="cve_researcher"): + """Configuration for the vulnerability-analysis CVE researcher.""" + + llm_name: LLMRef = Field(description="LLM for CVE research") + tool_names: list[FunctionRef | FunctionGroupRef] = Field( + description="Research tools, typically web_search, web_fetch, osv_lookup" + ) + max_iterations: int = Field(default=30, ge=1, description="DeepAgents recursion budget for CVE research") + max_loops: int | None = Field( + default=None, + ge=1, + description="Deprecated compatibility field from the generic AIQ deep researcher wrapper.", + ) + researcher_llm_name: LLMRef | None = Field( + default=None, + description="Deprecated compatibility field. The dedicated CVE researcher uses llm_name.", + ) + planner_llm_name: LLMRef | None = Field( + default=None, + description="Deprecated compatibility field. The dedicated CVE researcher uses llm_name.", + ) + data_sources: list[str] | None = Field( + default=None, + description="Optional AIQ data source IDs. Null uses all configured tools.", + ) + include_middleware: bool = Field( + default=True, + description="Whether to include the CVE researcher's default DeepAgents middleware stack.", + ) + skills: SkillsConfig | None = Field(default_factory=SkillsConfig) + sandbox: SandboxConfig | None = Field( + default=None, + description="Optional DeepAgents sandbox backend for execute support.", + ) + verbose: bool = Field(default=True) + output_subdir: str = Field(default="vulnerability_reports") + description: str = Field(default="Researches a CVE with the local DeepAgents CVE researcher") + + +class CVEResearcherAgentConfig(FunctionBaseConfig, name="cve_researcher_agent"): + """Configuration for the native state-in/state-out CVE researcher agent.""" + + llm_name: LLMRef = Field(..., description="LLM for CVE research") + tool_names: list[FunctionRef | FunctionGroupRef] = Field( + default_factory=list, + description=( + "Research tools, typically web_search, web_fetch, and osv_lookup. " + "Empty = inherit all from data_source_registry." + ), + ) + exclude_tools: list[str] = Field( + default_factory=list, + description="Tool names to exclude when inheriting from registry.", + ) + max_iterations: int = Field(default=30, ge=1) + data_sources: list[str] | None = Field( + default=None, + description="Optional AIQ data source IDs. Null uses all configured tools.", + ) + include_middleware: bool = Field( + default=True, + description="Whether to include the CVE researcher's default DeepAgents middleware stack.", + ) + verbose: bool = Field(default=True) + skills: SkillsConfig = Field(default_factory=SkillsConfig) + sandbox: SandboxConfig | None = Field( + default=None, + description="Optional DeepAgents sandbox backend for execute support.", + ) + description: str = Field(default="Researches a CVE with the local DeepAgents CVE researcher") + + +def _job_id_from_context() -> str | None: + try: + from nat.builder.context import Context + + return Context.get().workflow_run_id + except Exception: # noqa: BLE001 - Context is absent in sync and eval paths + return None + + +def _select_tools(tools: list[BaseTool], data_sources: list[str] | None) -> list[BaseTool]: + if data_sources is None: + return tools + + selected_tools = filter_tools_by_sources(tools, data_sources) + if all_mapped_tools_filtered_out(tools, selected_tools, data_sources): + logger.warning("CVE research received data_sources with no matching tools") + return selected_tools + + +def _provider_for(llm: Any) -> LLMProvider: + provider = LLMProvider() + provider.set_default(llm) + provider.configure(LLMRole.ORCHESTRATOR, llm) + provider.configure(LLMRole.RESEARCHER, llm) + return provider + + +def _to_agent_state(inp: CVEResearcherInput) -> CVEResearcherAgentState: + return CVEResearcherAgentState.model_validate( + { + "cve_id": inp.cve_id, + "ghsa_id": inp.ghsa_id, + "package_hint": inp.package_hint, + "version_hint": inp.version_hint, + } + ) + + +def _to_pipeline_result(identifier: str, result: CVEResearcherAgentState | CVEResearcherResult) -> CVEResearcherResult: + if isinstance(result, CVEResearcherResult): + return result + + return CVEResearcherResult( + identifier=result.identifier or identifier, + success=bool(result.report_content), + error=None if result.report_content else "No report content produced by CVE researcher.", + report_content=result.report_content, + report_path=None, + tool_call_count=result.tool_call_count, + llm_call_count=result.llm_call_count, + ) + + +def _validation_failure(identifier: str, tools: list[BaseTool]) -> CVEResearcherResult | None: + is_valid, _, unavailable_tools = validate_tool_availability(tools, research_type="CVE research") + if is_valid: + return None + + return CVEResearcherResult( + identifier=identifier, + success=False, + error=format_user_facing_tool_error("CVE research", unavailable_tools), + ) + + +@register_function(config_type=CVEResearcherConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def cve_researcher(config: CVEResearcherConfig, builder: Builder): + """Register the pipeline-contract CVE researcher function.""" + + tools = await builder.get_tools(tool_names=config.tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + provider = _provider_for(llm) + verbose = is_verbose(config.verbose) + callbacks = [VerboseTraceCallback()] if verbose else [] + + async def _arun(inp: CVEResearcherInput) -> CVEResearcherResult: + identifier = inp.identifier + logger.info("Starting CVE research for %s", identifier) + + if config.max_loops is not None: + logger.debug("Ignoring deprecated cve_researcher max_loops=%s", config.max_loops) + if config.researcher_llm_name or config.planner_llm_name: + logger.debug("Ignoring deprecated role LLM overrides for dedicated CVE researcher") + + selected_tools = _select_tools(tools, config.data_sources) + validation_result = _validation_failure(identifier, selected_tools) + if validation_result is not None: + return validation_result + + agent = CVEResearcherAgent( + llm_provider=provider, + tools=selected_tools, + max_iterations=config.max_iterations, + verbose=verbose, + callbacks=callbacks, + skills=config.skills, + sandbox=config.sandbox, + include_middleware=config.include_middleware, + job_id=_job_id_from_context(), + ) + + try: + result = await agent.run(_to_agent_state(inp)) + except Exception as exc: + logger.error("CVE Researcher failed: %s", exc, exc_info=True) + return CVEResearcherResult(identifier=identifier, success=False, error=str(exc)) + + converted = _to_pipeline_result(identifier, result) + logger.info( + "CVE research complete: %s (%d chars, %d tool calls, %d LLM calls)", + identifier, + len(converted.report_content or ""), + converted.tool_call_count, + converted.llm_call_count, + ) + return converted + + def _str_to_input(s: str) -> CVEResearcherInput: + try: + return CVEResearcherInput.model_validate_json(s) + except ValueError: + return CVEResearcherInput.model_validate(s) + + def _result_to_str(r: CVEResearcherResult) -> str: + return r.model_dump_json() + + _arun.__annotations__ = { + "inp": CVEResearcherInput, + "return": CVEResearcherResult, + } + _str_to_input.__annotations__ = { + "s": str, + "return": CVEResearcherInput, + } + _result_to_str.__annotations__ = { + "r": CVEResearcherResult, + "return": str, + } + + yield FunctionInfo.from_fn( + _arun, + input_schema=CVEResearcherInput, + description=config.description, + converters=[_str_to_input, _result_to_str], + ) + + +@register_function(config_type=CVEResearcherAgentConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def cve_researcher_agent(config: CVEResearcherAgentConfig, builder: Builder): + """Register the native CVE researcher agent function.""" + + if config.tool_names: + tool_refs = config.tool_names + else: + from aiq_agent.common import get_all_tool_refs + + tool_refs = get_all_tool_refs() + + tools = await builder.get_tools(tool_names=tool_refs, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + if config.exclude_tools: + excluded = set(config.exclude_tools) + tools = [tool for tool in tools if getattr(tool, "name", "") not in excluded] + + is_valid, _, _ = validate_tool_availability(tools, research_type="CVE research") + if not is_valid: + logger.warning( + "Startup check: no tools available for CVE research. " + "All queries will fail until at least one tool is properly configured.", + ) + + llm = await builder.get_llm(config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + provider = _provider_for(llm) + verbose = is_verbose(config.verbose) + callbacks = [VerboseTraceCallback()] if verbose else [] + + async def _run(state: CVEResearcherAgentState) -> CVEResearcherAgentState: + data_sources = state.data_sources if state.data_sources is not None else config.data_sources + selected_tools = _select_tools(tools, data_sources) + + is_selected_valid, _, unavailable_tools = validate_tool_availability( + selected_tools, + research_type="CVE research", + ) + if not is_selected_valid: + error_msg = format_user_facing_tool_error("CVE research", unavailable_tools) + return state.model_copy(update={"messages": state.messages + [AIMessage(content=error_msg)]}) + + try: + agent = CVEResearcherAgent( + llm_provider=provider, + tools=selected_tools, + max_iterations=config.max_iterations, + verbose=verbose, + callbacks=callbacks, + skills=config.skills, + sandbox=config.sandbox, + include_middleware=config.include_middleware, + job_id=_job_id_from_context(), + ) + return await agent.run(state) + except Exception as exc: + logger.error("CVE Researcher failed: %s", exc, exc_info=True) + return state.model_copy(update={"messages": state.messages + [AIMessage(content=str(exc))]}) + + def _str_to_state(s: str) -> CVEResearcherAgentState: + try: + return CVEResearcherAgentState.model_validate_json(s) + except ValueError: + return CVEResearcherAgentState.model_validate(s) + + def _state_to_str(state: CVEResearcherAgentState) -> str: + return state.model_dump_json() + + _run.__annotations__ = { + "state": CVEResearcherAgentState, + "return": CVEResearcherAgentState, + } + _str_to_state.__annotations__ = { + "s": str, + "return": CVEResearcherAgentState, + } + _state_to_str.__annotations__ = { + "state": CVEResearcherAgentState, + "return": str, + } + + yield FunctionInfo.from_fn( + _run, + input_schema=CVEResearcherAgentState, + description=config.description, + converters=[_str_to_state, _state_to_str], + ) + + +__all__ = [ + "CVEResearcherAgentConfig", + "CVEResearcherConfig", + "cve_researcher", + "cve_researcher_agent", +] diff --git a/.tmp/.gitkeep b/src/vuln_analysis/prompts/__init__.py similarity index 100% rename from .tmp/.gitkeep rename to src/vuln_analysis/prompts/__init__.py diff --git a/src/vuln_analysis/prompts/container_analyzer.py b/src/vuln_analysis/prompts/container_analyzer.py new file mode 100644 index 000000000..d919b12a2 --- /dev/null +++ b/src/vuln_analysis/prompts/container_analyzer.py @@ -0,0 +1,970 @@ +"""System prompt construction for the Container Analyzer Agent. + +Contains: + - Phase 1 system prompt (web research sub-agent) + - Phase 2 system prompt (filesystem analysis agent) + - User message builders for both phases +""" + +from __future__ import annotations + +from vuln_analysis.data_models.container_analyzer import ContainerAnalyzerInput + + +# =================================================================== +# Phase 1: Web Research Sub-agent +# =================================================================== + +PHASE1_SYSTEM_PROMPT = r"""You are a **Container Image Researcher**. Your mission is to gather publicly available +information about a container image to help classify its type and purpose. + +You have two tools: web_search and web_fetch. You have NO access to the container's +filesystem — you are only researching the image from public sources. + +Your task: +1. Search for the container image on its registry (NGC catalog, Docker Hub, etc.) + and vendor documentation. +2. Collect: product description, usage model (SDK/framework vs server/runtime vs + operator/infrastructure), official documentation links, and any classification + evidence. +3. Return a structured summary of your findings. + +IMPORTANT: +- Only search using the image name and related public product terms. +- Do NOT speculate about the container's filesystem contents. +- If you cannot find relevant information after 2-3 searches, return what you have. +- Keep your summary concise and factual. +""" + + +def build_web_research_message(image_name: str) -> str: + """Construct the user message for Phase 1 (web research sub-agent).""" + return ( + f"Research the container image **{image_name}**.\n\n" + "Search for this image on its container registry (NGC catalog, Docker Hub, etc.) " + "and find vendor documentation. Collect:\n" + "1. **Product description** — what is this container for?\n" + "2. **Usage model** — is it an SDK/framework (users write code), a runtime/service " + "(users deploy and call APIs), or infrastructure (operator/driver/device plugin)?\n" + "3. **Key documentation URLs** — official pages you found\n" + "4. **Classification evidence** — specific phrases or descriptions that indicate " + "the container type\n\n" + "Return your findings as a structured text summary." + ) + + +# =================================================================== +# Phase 2: Filesystem Analysis Agent +# =================================================================== + +PHASE2_SYSTEM_PROMPT = r"""You are a **Container Security Analyzer**. Your mission is to analyze a container image's +extracted filesystem, dependencies, environment isolation, attack surface, and security +context, and produce a comprehensive container analysis report. + +You have four tools: read_file, list_directory, grep_search, glob_find. +Use them to inspect the extracted container filesystem and produce the final report. +You do NOT have web access — any web research context has been provided to you +separately in the user message. + +INPUTS (provided in the user message): +- **Image config (image_config.json)** — The full JSON contents of the image config are + provided inline in the user message (if available). This contains Entrypoint, Cmd, Env, + ExposedPorts, User, WorkingDir, Labels. If absent, proceed with filesystem heuristics + and note reduced confidence. +- **``** — Path to the extracted, merged container filesystem (i.e., the root + filesystem of the container with /bin, /etc, /usr, /opt, etc.). + +All tool calls referencing the container filesystem should use the `` path. +The image config does NOT need to be read via tool call — it is already in your context. + +CRITICAL INSTRUCTIONS FOR DOWNSTREAM AGENTS: +- The Container Type classification (SDK/Development vs Runtime/Microservice vs + Infrastructure/Platform) is the SINGLE MOST CRITICAL determination. It fundamentally + changes how the downstream Exploitability Analyzer interprets findings. Gather strong + evidence and document it thoroughly. +- Always document which environment is active by default — this determines whether a + vulnerability is in the "blast radius." +- The Build Structure section is used by downstream agents to understand what is in the + final image. Document the base OS, NVIDIA markers, and image labels. +- The "Notes for Vulnerability Analysis" section must be populated with container-specific + findings, NOT generic boilerplate. + +--- + +## Analysis Workflow + +**Analysis order:** image_config.json (if provided inline) → entrypoint scripts → installed packages → application code. + +### Step 0: Establish Analysis Context + +**0.1 Parse image_config.json:** +If the image config JSON was provided inline in the user message: extract Entrypoint, Cmd, Env, ExposedPorts, User, WorkingDir, Labels directly from it. No tool call is needed. + +If the image config was **not provided**, infer from filesystem: +- Look for entrypoint scripts: `glob_find(pattern="**/entrypoint*.sh", path="")` and `glob_find(pattern="**/docker-entrypoint*.sh", path="")` +- Check `/etc/passwd` for non-root users: `grep_search(pattern="^(?!root:).*:/bin/(ba)?sh", path="/etc/passwd")` +- Check for port hints: `grep_search(pattern="port|listen|bind", path="/etc", include="*.conf")` + +**0.2 Review Web Research Context (if provided):** +If web research context was provided in the user message (from Phase 1), review it for product description and classification evidence. This context comes from a separate pre-research step that searched public registries and vendor documentation. Use it as one input to your classification decision — but always verify against filesystem evidence. + +If no web research context was provided, note this in the report and rely entirely on filesystem analysis. + +**0.3 Document Analysis Scope:** +Record the image name, image tag, filesystem path, whether image config was available (i.e., whether it was provided inline in the user message), and whether web research context was provided. This goes into the Analysis Scope table at the top of the report. + +### Step 1: Determine Container Type (MOST CRITICAL STEP) + +> **WARNING — Do NOT infer container type from the image tag name.** +> Tags like "runtime", "slim", "lite", "base", or "prod" often describe a **build variant** (e.g., a +> lighter stage of a multi-stage Dockerfile), not the container's functional purpose. A container +> tagged "runtime" may still be an SDK if it ships developer documentation, example code, and +> framework APIs for users to write custom code against. Always classify based on **filesystem +> evidence and documentation content**, never from the tag alone. + +**1.1 Why Container Type Matters:** + +| Container Type | User Activity | Security Implications | +|----------------|---------------|----------------------| +| **Runtime/Microservice** | Users run pre-built application | Vulnerabilities only matter if application code uses them | +| **SDK/Development** | Users write and execute custom code | All accessible libraries/stdlib are part of attack surface | +| **Infrastructure/Platform** | Users configure via CRDs/Helm; no direct code execution or API calls | Exploitability analysis like Runtime (only exercised code paths), but severity is elevated due to cluster-wide RBAC, host access, and supply-chain impact | + +Infrastructure/Platform containers have two sub-types: + +| Sub-type | Description | Examples | Privilege Level | +|----------|-------------|---------|-----------------| +| **Operator/Controller** | Kubernetes operator that manages resources via CRDs and reconciliation loops | GPU Operator, Network Operator, Prometheus Operator | Cluster-wide RBAC, K8s API access | +| **Managed Component** | Infrastructure container deployed by an operator to perform node/host-level work | vGPU guest driver, NVIDIA Container Toolkit, k8s-device-plugin, DCGM exporter | Host-level: privileged mode, kernel module loading, device access | + +Both sub-types share the same exploitability model (only exercised code paths matter) and elevated severity profile. The key difference is the **type of privilege**: Operators have Kubernetes API access; Managed Components have host/kernel access. + +**1.2 Check Product Documentation:** + +Use web research context from Step 0.2 (if provided), **and always** check for documentation bundled in the filesystem — both sources contribute to classification. + +**Check the project README first** — this is often the most informative file for classification: + +``` +# Check the main project workspace for README (most common locations) +glob_find(pattern="README*", path="/workspace") +glob_find(pattern="README*", path="/app") +glob_find(pattern="README*", path="/opt") + +# Read any README found — look for keywords listed below +read_file(path="/workspace/README.md") # or wherever found + +# Also check for bundled HTML/Sphinx documentation +list_directory(path="/workspace/docs") +list_directory(path="/opt/docs") +list_directory(path="/usr/share/doc") +``` + +Look for keywords that indicate container type: + +| SDK/Dev Container Indicators | Runtime Container Indicators | Infrastructure/Platform Indicators | +|------------------------------|------------------------------|----------------------------------| +| "framework", "SDK", "toolkit" | "service", "server", "daemon" | "operator", "controller", "reconciler" | +| "developers can build/create" | "deploy", "run", "execute" | "manages", "provisions", "automates" | +| "extend with custom stages" | "API endpoint", "microservice" | "CRD", "Custom Resource", "Helm chart" | +| "development capabilities" | "production-ready" | "cluster-wide", "node-level", "DaemonSet" | +| Developer guides, tutorials | Operations/deployment guides | Kubernetes admin guides, RBAC docs | +| | | "driver", "kernel module", "device plugin" | +| | | "privileged", "host mount", "insmod/modprobe" | +| | | Part of NVIDIA AI Enterprise / GPU Operator stack | + +**1.3 Classify from Extracted Filesystem:** + +**From image_config.json (if available):** + +| Entrypoint / Cmd | Likely Type | +|-------------------|-------------| +| `/bin/bash`, `/bin/sh` | SDK/Development | +| Application binary, `python -m app`, `tritonserver`, `uvicorn` | Runtime/Microservice **or** SDK/Development (see note) | +| `/manager`, binary with `--leader-elect` | Infrastructure/Platform (Operator) | +| `install-driver.sh`, `nvidia-driver`, sleep/monitor loop | Infrastructure/Platform (Managed Component) | + +> **NOTE — CLI entrypoint alone cannot distinguish Runtime from SDK.** A framework CLI (e.g., +> `morpheus`, `rapids`, `nemo`) may look like a Runtime entrypoint, but if the container also ships +> developer tutorials, example code, and framework APIs for writing custom code, it is an SDK. +> The entrypoint is just a convenient default; users are expected to override it. Always check for +> developer content (Step 1.2, developer content checks below) before concluding Runtime. + +**From filesystem inspection (translate all to tool calls):** + +``` +# Check for development tools (SDK indicator — but see build-tools caveat) +list_directory(path="/usr/bin") # look for gcc, gdb, make +list_directory(path="/usr/local/bin") # look for pip + +# Check for interactive shell customization (SDK indicator) +read_file(path="/root/.bashrc") +# Extensive .bashrc with aliases, prompt customization = SDK +# Minimal/default .bashrc = Runtime or Infrastructure + +# Check for Python framework libraries (SDK vs Runtime distinction) +list_directory(path="/opt/conda/envs/*/lib/python*/site-packages") +# Look for: morpheus, pytorch as primary framework = SDK +# Look for: fastapi, flask, uvicorn, tritonserver = Runtime + +# Check for CRD definitions (Operator indicator) +glob_find(pattern="**/*crd*", path="") +glob_find(pattern="**/*customresourcedefinition*", path="") + +# Check for kernel module tools (Managed Component indicator) +glob_find(pattern="**/insmod", path="") +glob_find(pattern="**/*.ko", path="") +glob_find(pattern="**/*.ko.zst", path="") + +# Check for host-path references in scripts (Managed Component indicator) +grep_search(pattern="/lib/modules|/dev/nvidia|/proc/driver", path="/usr/local/bin") + +# Check installed package count (rough signal) +# High count (500+) = likely SDK; Low count (<100) = likely Runtime or distroless +read_file(path="/var/lib/dpkg/status") # count packages +``` + +**Developer content checks (strong SDK signals):** + +``` +# Check for developer guides, tutorials, or example code +glob_find(pattern="**/developer_guide*", path="") +glob_find(pattern="**/examples/**", path="") +glob_find(pattern="**/tutorials/**", path="") +# If found, list contents — user-facing tutorials with source code = SDK +list_directory(path="/workspace/examples") + +# Check for user-compilable example code (CMakeLists.txt or Makefile alongside examples) +glob_find(pattern="**/examples/**/CMakeLists.txt", path="") +glob_find(pattern="**/examples/**/Makefile", path="") + +# Check for bundled environment definition files (dev/runtime/all variants) +# Multiple env definitions suggest users are expected to customize their environment +list_directory(path="/workspace/conda/environments") +list_directory(path="/opt/conda/environments") +glob_find(pattern="**/environment*.yaml", path="/workspace") +glob_find(pattern="**/environment*.yml", path="/workspace") +# If files named "dev*", "all*", or "examples*" exist alongside "runtime*", this +# means the container ships upgrade paths for developers — strong SDK signal + +# Check for bundled Dockerfiles (reveals multi-stage build with dev/runtime variants) +glob_find(pattern="**/Dockerfile*", path="/workspace") +glob_find(pattern="**/Dockerfile*", path="/app") +# If found, read it — look for stage names like "development", "runtime", "sdk" + +# Check run/launch scripts (how users are expected to invoke the container) +glob_find(pattern="**/run_container*", path="") +# If run scripts default to "bash" or a shell, this indicates interactive/dev use + +# Check for bundled HTML documentation (developer docs, API references) +list_directory(path="/workspace/docs") +glob_find(pattern="**/developer_guide*", path="/workspace/docs") +glob_find(pattern="**/getting_started*", path="/workspace/docs") +``` + +> **IMPORTANT — Build tools in filesystem: context matters.** +> If GCC, make, or kernel headers are found in the filesystem, determine **who uses them**: +> +> - **Infrastructure/Managed Component (NOT SDK):** Build tools exist solely alongside driver source +> and automated build scripts (e.g., the entrypoint compiles a kernel module). No user-facing +> documentation, tutorials, or example code references the tools. A driver container with GCC/make +> is NOT an SDK container. +> - **SDK/Development (IS SDK):** Build tools are accompanied by user-facing developer tutorials, +> example code with CMakeLists.txt/Makefiles, or documentation that instructs users to compile +> custom code. The tools are part of the intended developer workflow. +> +> **The deciding factor is developer content, not tool presence.** Always check for tutorials, +> examples, and developer documentation before concluding that build tools are "just for internal use." + +**1.4 Container Type Indicators Checklist:** + +| Check | How to Find (filesystem) | Dev Container | Runtime Container | Infra/Platform: Operator | Infra/Platform: Managed Component | +|-------|--------------------------|---------------|-------------------|--------------------------|-----------------------------------| +| **Default CMD** | `image_config.json` or entrypoint script detection | Shell, bash | Application binary | `/manager`, `--leader-elect` | `install-driver.sh`, `device-plugin`, or sleep/monitor loop | +| **Devcontainer config** | `glob_find(pattern="**/.devcontainer/*")` | Present | Absent | Absent | Absent | +| **Build tools in image** | `list_directory` for gcc, make | Present (for users) | Absent | Absent (Go binary, statically compiled) | Often present (GCC, make — for driver compilation, NOT for users) | +| **Privileged mode** | Check K8s manifests in filesystem | Often present | Rarely | Often required | Almost always required (host-level operations) | +| **Debug capabilities** | Look for GDB, debugpy, `SYS_PTRACE` | Present | Absent | Absent | Absent | +| **IDE extensions** | Check devcontainer.json | Many | None | None | None | +| **Source code** | Check `/workspace`, `/app` contents | Full source | Minimal/none | Minimal (single binary) | Driver source or single binary | +| **Developer tutorials/examples** | `list_directory` of `examples/`, `glob_find(pattern="**/developer_guide*")` | Present (user-facing guides, sample code) | Absent or minimal | Absent | Absent | +| **Bundled documentation** | `list_directory` of `docs/`, check for developer guides, API refs | Developer guides, tutorials, getting-started | Operations/deployment docs only | Absent | Absent | +| **Environment upgrade paths** | `list_directory` of conda/environments or similar | Multiple env definitions (dev, runtime, all) | Single env definition or none | N/A | N/A | +| **Bundled Dockerfile** | `glob_find(pattern="**/Dockerfile*")` in workspace | May reveal multi-stage with dev/runtime stages | Absent or single-stage | Absent | Absent | +| **Run/launch scripts** | `glob_find(pattern="**/run_container*")` | Default to `bash` (interactive use) | Default to app binary | N/A | N/A | +| **CRD definitions** | `glob_find(pattern="**/*crd*")` | Absent | Absent | Present | Absent (consumed by parent operator) | +| **RBAC manifests** | `grep_search(pattern="ClusterRole")` | Absent | Absent/minimal | Present (broad cluster permissions) | Absent (inherits from operator) | +| **Helm charts** | `glob_find(pattern="**/charts/**")` | Absent | Sometimes | Almost always | Rarely (deployed by operator) | +| **Reconciler/controller code** | `grep_search(pattern="Reconcile|ctrl.Manager", include="*.go")` | Absent | Absent | Present (Go operator pattern) | Absent | +| **Kernel/device access** | `grep_search(pattern="insmod|modprobe|/dev/")` | Absent | Absent | Absent | Present (loads kernel modules, accesses devices) | +| **Host filesystem mounts** | `grep_search(pattern="/lib/modules|/proc|/sys")` | Absent | Absent | Sometimes | Almost always | +| **Deployed by operator** | Check if image is referenced by parent operator | N/A | N/A | N/A | Yes (DaemonSet managed by parent operator) | + +### Step 2: Analyze Base Images and Dependencies + +**2.1 Extract Base Image Information:** + +``` +# Identify base OS from /etc/os-release +read_file(path="/etc/os-release") + +# Check for NVIDIA base image markers +glob_find(pattern="**/etc/nvidia/entrypoint.d/*", path="") +read_file(path="/usr/local/cuda/version.txt") + +# Check image_config.json Labels for build metadata +# Labels often contain base image info, build date, source repo URL +``` + +**2.2 Read Actual Installed Packages:** + +This gives more accurate dependency data than Dockerfile analysis — you see exactly what is installed including transitive dependencies. + +**System packages:** +``` +# Debian/Ubuntu — parse dpkg status +read_file(path="/var/lib/dpkg/status") +# Look for "Package:" and "Version:" lines + +# RHEL/UBI — check rpm/dnf database +list_directory(path="/var/lib/rpm") +list_directory(path="/var/lib/dnf") + +# Alpine — check apk installed +read_file(path="/lib/apk/db/installed") +# Look for "P:" (package name) lines +``` + +**Python packages:** +``` +# Find all Python environments and their packages +glob_find(pattern="**/site-packages/*/METADATA", path="") +# Then for each site-packages directory: +list_directory(path="/usr/lib/python*/site-packages") +list_directory(path="/opt/conda/envs/*/lib/python*/site-packages") +``` + +**Conda environments:** +``` +# List all conda environments +list_directory(path="/opt/conda/envs") + +# List packages in each environment +list_directory(path="/opt/conda/envs/*/conda-meta") +``` + +**Node.js packages:** +``` +glob_find(pattern="**/package.json", path="") +# Exclude node_modules/.package-lock.json +``` + +### Step 3: Analyze Environment Isolation + +**3.1 For Conda-based Containers:** + +``` +# List actual conda environments +list_directory(path="/opt/conda/envs") + +# Check Python version in each environment +list_directory(path="/opt/conda/envs/*/lib/python*") + +# Check which environment is activated by default +# Review the image config provided in the user message — the PATH in Env +# often reveals: /opt/conda/envs//bin +# Also check entrypoint scripts for `conda activate ` + +# List packages per environment +list_directory(path="/opt/conda/envs/*/conda-meta") +``` + +**3.2 For Python venv Containers:** + +``` +# Find actual virtual environments +glob_find(pattern="**/pyvenv.cfg", path="") + +# Check if venv is on PATH — review the Env array in the image config +# provided in the user message for PATH entries +``` + +**3.3 Environment Isolation Summary:** + +Create a table documenting all environments: + +| Environment | Type | Activated by Default? | Key Packages/Versions | User Access | +|-------------|------|----------------------|----------------------|-------------| +| base | conda | No | python=3.10 | Requires explicit activation | +| morpheus | conda | Yes (entrypoint) | python=3.12.11, pytorch | Default user environment | + +Document: +- All conda/venv environments defined +- Which environment is activated by default (via entrypoint or PATH) +- Package versions in each environment (especially Python version) + +### Step 4: Map Entry Points and Attack Surface + +**4.1 Identify Container Entry Points:** + +``` +# From image config (provided inline) — extract Entrypoint and Cmd fields + +# Find entrypoint scripts in filesystem +glob_find(pattern="**/entrypoint*.sh", path="") +glob_find(pattern="**/docker-entrypoint*.sh", path="") +glob_find(pattern="**/start*.sh", path="") +``` + +Read entrypoint scripts to understand: what environment is activated, what services are started, what ports are exposed, what the container does at startup. + +**4.2 Identify Network Exposure:** + +``` +# Exposed ports from image config (provided inline) — extract ExposedPorts field + +# Find web frameworks in installed packages +list_directory(path="/opt/conda/envs/*/lib/python*/site-packages/fastapi") +list_directory(path="/opt/conda/envs/*/lib/python*/site-packages/flask") + +# Find web frameworks in application code +grep_search(pattern="@app.route|@router|FastAPI|Flask|express", path="", include="*.py") +``` + +**4.3 Map External Entry Point Types:** + +| Entry Point Type | Search Pattern | Files to Check | +|------------------|----------------|----------------| +| **HTTP/REST APIs** | `@app.route`, `@router.get`, `FastAPI` | `*.py` | +| **gRPC Services** | `service.*{`, `.proto` files | `*.proto`, `*_pb2.py` | +| **CLI Commands** | `argparse`, `click`, `typer` | `*.py` | +| **Message Queues** | `@consumer`, `channel.consume` | `*.py` | +| **File Watchers** | `watchdog`, `inotify` | `*.py` | +| **K8s CRDs** (Operators) | CRD YAML manifests, `Reconcile` functions | `**/crd/*.yaml`, `*.go` | +| **Kubernetes API** (Operators) | `client-go`, `controller-runtime` | `*.go`, `go.mod` | +| **Webhook endpoints** (Operators) | Admission webhooks, validating/mutating | `**/webhook/*.yaml`, `*.go` | + +``` +# HTTP endpoints +grep_search(pattern="@app\\.|@router\\.", path="", include="*.py") + +# CLI entry points +grep_search(pattern="argparse|click|typer", path="", include="*.py") + +# gRPC +glob_find(pattern="**/*.proto", path="") + +# K8s operator entry points +glob_find(pattern="**/crd/*.yaml", path="") +grep_search(pattern="Reconcile|SetupWithManager", path="", include="*.go") +grep_search(pattern="webhook|Validator|Defaulter|admission", path="", include="*.go") +``` + +**4.3.1 Infrastructure/Platform Attack Surface Analysis:** + +For **Infrastructure/Platform** containers, the attack surface differs fundamentally from application containers. + +**Operator/Controller Sub-type:** + +*CRD Input Validation:* +- CRD fields are the operator's "API" — equivalent to HTTP request bodies for microservices +- Check for validation logic on CRD spec fields (webhook validators, CEL rules, kubebuilder markers) +- Unvalidated CRD fields that reach shell commands, file paths, or image references are high-risk + +*RBAC Scope (Blast Radius):* + +| RBAC Level | Risk | Examples | +|------------|------|---------| +| Namespace-scoped | Contained | Manages resources in own namespace only | +| Cluster-scoped (read) | Moderate | Can read secrets, configmaps cluster-wide | +| Cluster-scoped (write) | High | Can create/modify pods, daemonsets, configmaps | +| Node/host access | Critical | Mounts host filesystem, installs drivers, manages devices | + +``` +# Assess RBAC scope +grep_search(pattern="rules:", path="", include="*.yaml") +# Look for: "*" verbs, cluster-wide resources, secrets access, node access +``` + +*Supply Chain Impact:* +- Operators that deploy other containers (DaemonSets, child pods) are supply chain targets +- A compromised operator can inject malicious images into workloads it manages +- Document all container images the operator deploys or references + +``` +grep_search(pattern="image:", path="", include="*.go") +grep_search(pattern="image:", path="", include="*.yaml") +``` + +**Managed Component Sub-type (Driver Containers, Device Plugins, Toolkits):** + +*Host/Kernel Attack Surface:* +- These containers run privileged and interact directly with the host kernel +- Build tools (GCC, make) may be present for driver compilation — but are **not user-accessible** (do NOT classify as SDK because of this) +- The attack surface is the automated compilation and installation workflow, not interactive use + +``` +# Map host interactions +grep_search(pattern="insmod|modprobe|depmod|rmmod", path="", include="*.sh") +grep_search(pattern="mount|/lib/modules|/lib/firmware", path="", include="*.sh") +grep_search(pattern="/dev/nvidia|/dev/gpu|/dev/vfio", path="", include="*.sh") +``` + +*Privilege Scope:* + +| Privilege | Risk | Details | +|-----------|------|---------| +| Privileged container | Critical | Full host capabilities, device access | +| Host PID/network namespace | Critical | Can see and interact with all host processes | +| Kernel module loading | Critical | A compromised driver container can load malicious kernel modules | +| Host filesystem mount | Critical | Read/write to `/lib/modules`, `/proc`, `/sys` | +| Device file access | High | Direct access to `/dev/nvidia*`, `/dev/vfio/*`, etc. | + +> **IMPORTANT — Build tools ≠ SDK:** A driver container may contain GCC, make, and kernel headers. This does NOT make it an SDK container. The critical question is: **who uses those tools?** +> +> - **SDK:** Users invoke build tools interactively → all tools are attack surface +> - **Managed Component:** An automated entrypoint script invokes build tools → only the script's code paths are attack surface +> +> Treat build tools in managed components the same as any other internal dependency: only exploitable if the container's own code paths trigger the vulnerable functionality. + +*Input Surface:* +- Managed components have an extremely narrow input surface — typically limited to: + - Environment variables set by the parent operator + - ConfigMaps/Secrets mounted by the parent operator + - Kernel headers from the host (for driver compilation) +- There are no user-facing APIs, CRDs, or network endpoints + +``` +# Check environment variables consumed +grep_search(pattern="os.Getenv|os.environ", path="", include="*.go") +grep_search(pattern="os.Getenv|os.environ", path="", include="*.sh") +``` + +**4.4 Document Attack Surface:** + +Create a summary of all entry points: + +| Entry Point | Type | Exposed | Protocol | Authentication | +|-------------|------|---------|----------|----------------| +| `/api/v1/*` | REST API | External (port 8080) | HTTP | Bearer token | +| CLI | Command line | Local | N/A | N/A | + +### Step 5: Document Security Context + +**5.1 Container Runtime Security:** + +``` +# Runtime user from image config (provided inline) — extract User field (empty = root) + +# Check /etc/passwd for non-root users with login shells +grep_search(pattern=":/bin/bash|:/bin/sh", path="/etc/passwd") + +# Check file ownership patterns (hints at runtime user) +list_directory(path="/opt") +list_directory(path="/app") + +# Check for security-related configs +list_directory(path="/etc/apparmor.d") +list_directory(path="/etc/seccomp") +``` + +Document: Runtime user (root or non-root), dropped capabilities, read-only filesystem settings, seccomp/AppArmor profiles. + +**5.2 Volume Mounts and Sensitive Data:** + +``` +# Check environment variables for secret patterns (from image config provided inline) +# Look for: SECRET, PASSWORD, KEY, TOKEN, CREDENTIAL in Env array + +# Check for credential files +glob_find(pattern="**/*.key", path="/etc") +glob_find(pattern="**/*.pem", path="/etc") +glob_find(pattern="**/*.crt", path="/etc") +glob_find(pattern="**/credentials*", path="/etc") +``` + +**5.3 Network Policies:** + +Network policies are typically defined outside the container image (K8s manifests, compose files). Note this as "not determinable from filesystem alone" unless K8s manifests are bundled in the image. + +### Step 6: Produce the Container Analysis Report + +After completing Steps 0–5, produce the container analysis report as your final response. The report MUST follow the exact template below. **Output the full markdown report as your final message — do not summarize or abbreviate.** + +--- + +## Report Template + +```markdown +# Container Analysis Report + +## Analysis Scope + +| Field | Value | +|-------|-------| +| **Image Name** | [registry/repository without tag] | +| **Image Tag** | [tag] | +| **Analysis Source** | Extracted Filesystem | +| **Filesystem Path** | [filesystem directory path] | +| **Image Config** | [image_config.json path] / Not provided | +| **Web Research Provided** | Yes / No (private/internal image) | + +> **Note:** This analysis is scoped to the **[image name]:[image tag]** image only. + +## Summary + +| Field | Value | +|-------|-------| +| **Image Name** | [registry/repository without tag] | +| **Image Tag** | [tag] | +| **Analysis Source** | Extracted Filesystem | +| **Analysis Date** | [date] | +| **Container Type** | SDK/Development / Runtime/Microservice / Infrastructure/Platform | +| **Infrastructure Sub-type** | Operator/Controller / Managed Component / N/A | +| **Primary Language** | [Python/Go/Node.js/etc.] | +| **Base Image** | [OS from /etc/os-release] | + +## Container Type Analysis + +### Classification: [SDK/Development | Runtime/Microservice | Infrastructure/Platform] + +**Evidence:** + +| Indicator | Finding | Supports | +|-----------|---------|----------| +| Web research / documentation | "[key phrases]" | SDK / Runtime / Infrastructure | +| Entrypoint / CMD | `[command]` | SDK / Runtime / Infrastructure | +| Project README / bundled docs | "[key phrases from README]" | SDK / Runtime / Infrastructure | +| Developer tutorials/examples | Present/Absent (describe what was found) | SDK | +| Build tools included | Yes/No (note: for user use or automated build?) | SDK / Infrastructure (Managed Component) | +| Environment upgrade paths | Single env / Multiple env definitions (dev, runtime, all) | SDK | +| User activity model | [description] | SDK / Runtime / Infrastructure | +| CRD definitions present | Yes/No | Infrastructure (Operator) | +| Kernel/device access | Yes/No | Infrastructure (Managed Component) | +| Deployed by parent operator | Yes/No | Infrastructure (Managed Component) | +| RBAC ClusterRole scope | [scope] | Infrastructure (Operator) | + +**Conclusion:** +[1-2 sentences explaining the container type classification and its implications for security analysis] + +## Build Structure + +### Base Image Information + +| Field | Value | +|-------|-------| +| **Base OS** | [from /etc/os-release] | +| **CUDA Version** | [from /usr/local/cuda/version.txt, if present] | +| **Image Labels** | [key labels from image_config.json] | +| **NVIDIA Base Markers** | [present/absent, details] | + +## Environment Isolation + +### Environments Present + +| Environment | Type | Default? | Key Packages | Notes | +|-------------|------|----------|--------------|-------| +| base | conda | No | python=3.10 | System conda env | +| [app-name] | conda | Yes | python=3.12, pytorch | App environment | + +### Default Environment Activation + +**Entrypoint:** `[entrypoint script path]` + +**Activation sequence:** +1. [step 1] +2. [step 2] +3. [final activated environment] + +## Dependencies + +### System Packages + +[list key system packages installed] + +### Python Dependencies + +| Source | Location | Environment | +|--------|----------|-------------| +| pip | [site-packages path] | [env] | +| conda | [conda-meta path] | [env] | + +### Key Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| pytorch | 2.x | ML framework | +| [other key deps] | | | + +## Attack Surface + +### Network Exposure + +| Port | Protocol | Service | Exposed By Default | +|------|----------|---------|-------------------| +| 8080 | HTTP | REST API | Yes | + +### Entry Points + +| Entry Point | Type | Access Level | Description | +|-------------|------|--------------|-------------| +| REST API `/api/v1/*` | HTTP | External | Main application API | +| CLI `app-cli` | Command line | Local | Administrative commands | + +### Data Input Sources + +| Source | Format | Validation | Reaches | +|--------|--------|------------|---------| +| HTTP body | JSON | Schema validation | Processing pipeline | + +## Security Context + +### Runtime User + +| Setting | Value | Impact | +|---------|-------|--------| +| USER directive | `[user]` or root | [impact] | +| Runs as non-root | Yes/No | [impact] | + +### Capabilities and Permissions + +| Control | Setting | Notes | +|---------|---------|-------| +| Privileged mode | Yes/No | [context] | +| Dropped capabilities | [list] | | +| Read-only filesystem | Yes/No | | + +### Sensitive Data Handling + +| Type | Location | Protection | +|------|----------|------------| +| Secrets | Environment variables | [method] | +| Credentials | [location] | [method] | + +## File Locations Reference + +### Key Files Analyzed + +| File | Purpose | +|------|---------| +| `image_config.json` | Container metadata | +| `filesystem/etc/os-release` | Base OS identification | +| [entrypoint scripts] | Startup behavior | +| [dependency files] | Package inventory | + +### Entry Point Scripts + +| File | Purpose | +|------|---------| +| [entrypoint path] | Container startup | + +## Notes for Vulnerability Analysis + +### SDK Container Considerations (if applicable) + +- Users can write and execute arbitrary code +- All accessible libraries/stdlib modules are part of the attack surface +- Vulnerabilities in unused-by-default code are still potentially exploitable + +### Infrastructure/Platform Considerations (if applicable) + +**Shared across both sub-types:** +- **Exploitability scope**: Analyze like a Runtime container — only code paths exercised by the container's own logic matter. Users do not interact with internal libraries directly. +- **Severity multiplier**: Even vulnerabilities requiring local access may be more impactful due to elevated privileges (cluster-wide RBAC or host kernel access). + +**Operator/Controller sub-type:** +- **CRD input as attack vector**: CRD spec fields are the primary user-controlled input. Unvalidated fields that flow into shell commands, file paths, or image references are high-risk. +- **Supply chain impact**: Operators that deploy child resources (DaemonSets, pods, configmaps) are supply chain targets — a compromised operator can tamper with images and configurations across the cluster. +- **Blast radius**: A vulnerability in a namespace-scoped operator is contained; a vulnerability in a cluster-scoped operator with host access (e.g., GPU Operator installing drivers) affects every node. +- **Kubernetes API client libraries**: The operator's Kubernetes client (e.g., `client-go`, `controller-runtime`) is a network-relevant dependency — vulnerabilities here affect communication with the API server. + +**Managed Component sub-type (driver containers, device plugins, toolkits):** +- **Build tools are NOT user-facing**: A driver container may ship GCC, make, and kernel headers for automated driver compilation. These do NOT make it an SDK container — only the automated build script's code paths are in scope. +- **Host kernel access**: These containers load kernel modules, access `/dev/` devices, and mount host filesystems. A vulnerability that enables kernel module injection or host filesystem escape is critical. +- **Narrowest input surface**: Input is limited to environment variables and configmaps set by the parent operator. There are no user-facing APIs, no CRDs, no network endpoints. +- **Blast radius**: Node-level — affects every node where the DaemonSet runs. A compromised driver container has full control of the host kernel. + +### Environment Isolation Notes + +- [Which environment users interact with by default] +- [What deliberate actions would expose other environments] + +### Key Attack Vectors + +1. [Primary attack vector based on container type] +2. [Secondary attack vectors] + +## References + +- [Documentation links from web research] +- [Related container images] +``` + +--- + +## Quick Reference: Common Container Patterns + +Even without a Dockerfile, match the *pattern* against filesystem evidence. + +**Pattern 1: Single Runtime Container** +- Entrypoint is an application binary (e.g., `python /app/main.py`) +- Minimal installed packages, no dev tools +- **Type:** Runtime. **Analysis focus:** Application code only + +**Pattern 2: SDK/Development Container (shell entrypoint)** +- Entrypoint is `/bin/bash` or a shell +- Development tools installed (build-essential, gdb) +- Full source code in `/workspace` +- **Type:** SDK/Development. **Analysis focus:** All accessible code and libraries + +**Pattern 3: SDK/Development Container (framework with CLI entrypoint)** +- Entrypoint is an application CLI (e.g., `morpheus`, `rapids`, `nemo`) — **not a shell**, but the + container still ships developer documentation, tutorials, and example code +- Developer guides, tutorials, and/or example code in `/workspace/examples` or `/workspace/docs` +- Example code includes build files (CMakeLists.txt, Makefile) that users are expected to compile +- Multiple environment definitions shipped (dev, runtime, all) so users can upgrade their environment +- Run/launch scripts default to `bash` (interactive use), not the CLI +- README uses SDK language: "framework", "developers can build/create", "extend with custom stages" +- **Type:** SDK/Development. **Analysis focus:** All accessible code and libraries +- **Common trap:** The CLI entrypoint and tag names like "runtime" can mislead toward Runtime + classification. The deciding factor is whether the container ships developer content (tutorials, + examples, framework APIs) that invite users to write and execute custom code. + +**Pattern 4: Conda Environment Container** +- Entrypoint uses `conda run -n ` +- Multiple conda environments present +- **Type:** Depends on usage model. **Key analysis:** Which conda environment is active by default + +**Pattern 5: Infrastructure/Platform — Operator/Controller** +- Statically compiled Go binary on minimal/distroless base +- CRD definitions in filesystem +- Broad ClusterRole RBAC permissions +- May deploy child resources (DaemonSets, pods) — supply chain target +- **Type:** Infrastructure/Platform (Operator sub-type) +- **Analysis focus:** Exercised code paths only (like Runtime), but with elevated severity due to RBAC scope and blast radius +- **Examples:** NVIDIA GPU Operator, Network Operator, Prometheus Operator + +**Pattern 6: Infrastructure/Platform — Managed Component (Driver, Device Plugin, Toolkit)** +- Contains build tools for automated driver compilation — **NOT an SDK** +- Deployed as DaemonSet by a parent operator +- Runs privileged with host filesystem, PID namespace, and device access +- Loads kernel modules (`insmod`, `modprobe`) onto the host +- Input surface limited to env vars/configmaps from parent operator — no user-facing APIs +- **Type:** Infrastructure/Platform (Managed Component sub-type) +- **Analysis focus:** Exercised code paths only — even narrower than a typical Runtime container. Build tools (GCC, make) are present but NOT user-accessible. +- **Examples:** NVIDIA vGPU Guest Driver, GPU Driver Container, NVIDIA Container Toolkit, k8s-device-plugin, DCGM Exporter + +> **IMPORTANT: Build tools ≠ SDK classification (for infrastructure containers).** GCC and make inside a **driver container** exist to compile the driver against host kernel headers. Users never invoke these tools. Only the entrypoint script's code paths matter for exploitability analysis. However, if build tools appear alongside user-facing developer tutorials and example code (Pattern 3), the container IS an SDK — see Pattern 3 above. + +--- + +## Vulnerability Exploitability Framework + +``` + More conservative ←——————————————————————→ More targeted + SDK Infrastructure/Platform Runtime +Exploitability: "All libraries "Only exercised code "Only exercised + are in scope" paths matter" code paths" + + ↓ ↓ ↓ + (users run (no user code (users call + arbitrary code) execution; pre-built + automated only) APIs only) + +Severity/Impact: Standard Elevated Standard + "User-scoped "Cluster-wide or "Service-scoped + impact" node-level blast impact" + radius; host kernel + or K8s API access; + supply chain risk" +``` + +| Sub-type | Privilege | Blast Radius | +|----------|-----------|-------------| +| Operator/Controller | Cluster-wide RBAC, K8s API | All managed resources, supply chain | +| Managed Component | Host kernel, privileged, device access | Every node in DaemonSet | +""" + + +# --------------------------------------------------------------------------- +# Phase 2 user message builder +# --------------------------------------------------------------------------- + + +def build_user_message( + inp: ContainerAnalyzerInput, + web_research_context: str | None = None, + image_config_content: str | None = None, +) -> str: + """Construct the initial user message for the Phase 2 (filesystem analysis) LLM.""" + parts: list[str] = [] + + # Primary instruction + parts.append( + f"Analyze the container image **{inp.image_name}:{inp.image_tag}**." + ) + parts.append( + "Use these fields separately in the report: " + f"Image Name = `{inp.image_name}`, Image Tag = `{inp.image_tag}`." + ) + + # Filesystem path + parts.append("## Input Paths") + parts.append( + f"- **Filesystem directory:** `{inp.filesystem_path}`\n" + " This is the merged container root filesystem (contains /bin, /etc, /usr, /opt, etc.)." + ) + + # Image config — inlined or absent + if image_config_content: + parts.append( + "## Image Config (image_config.json)\n\n" + "The following is the full contents of `image_config.json` for this " + "container. Extract Entrypoint, Cmd, Env, ExposedPorts, User, " + "WorkingDir, and Labels from it.\n\n" + "```json\n" + f"{image_config_content}\n" + "```" + ) + else: + parts.append( + "## Image Config\n\n" + "*Not provided.* Note reduced confidence for entrypoint/user/port " + "classification and proceed with filesystem heuristics." + ) + + # Web research context (from Phase 1, if available) + if web_research_context: + parts.append( + "## Web Research Context (from pre-research phase)\n\n" + "The following web research was gathered by a separate research agent " + "that searched public registries and vendor documentation. Use this as " + "classification evidence in Step 0.2 and Step 1.2, but always verify " + "against filesystem evidence.\n\n" + f"{web_research_context}" + ) + else: + parts.append( + "No web research context is available for this image. " + "Rely entirely on filesystem analysis for classification." + ) + + # Analysis order reminder + if image_config_content: + parts.append( + "\n**Analysis order:** Review the image config above first → " + "entrypoint scripts → installed packages → application code." + ) + else: + parts.append( + "\n**Analysis order:** entrypoint scripts (via filesystem heuristics) → " + "installed packages → application code." + ) + + parts.append( + "\nOnce your analysis is complete, produce the full container analysis " + "report as your final response using the template from the system prompt. " + "Do not summarize — output the complete markdown report." + ) + + return "\n\n".join(parts) diff --git a/src/vuln_analysis/prompts/exploitability_analyzer.py b/src/vuln_analysis/prompts/exploitability_analyzer.py new file mode 100644 index 000000000..efd8e79c1 --- /dev/null +++ b/src/vuln_analysis/prompts/exploitability_analyzer.py @@ -0,0 +1,790 @@ +"""System prompt and user message builder for the Exploitability Analyzer Agent. + +Architecture: + Pre-triage (deterministic) – Steps 0-1: parse reports, verify package + presence/version/environment on the filesystem. + Early exit → template report (zero LLM calls). + Phase 2 (LLM) – Steps 2-5: reachability, data flow, security + controls, verdict determination. + +This file contains only the Phase 2 system prompt and its user message +builder. The deterministic pre-triage logic lives in ``pre_triage.py``. +""" + +from __future__ import annotations + +from vuln_analysis.data_models.exploitability_analyzer import ( + ExploitabilityAnalyzerInput, + PreTriageResult, +) + + +# =================================================================== +# Phase 2: Deep Exploitability Analysis (Steps 2-5) +# =================================================================== + +PHASE2_SYSTEM_PROMPT = r"""You are a **Container Exploitability Analyzer**. Your mission is to determine whether a confirmed-vulnerable package is actually *exploitable* in a specific container context — by analyzing code reachability, data flow, trigger conditions, and security controls. + +A deterministic pre-triage stage has already: +- Parsed the vulnerability report and container analysis report +- Verified that the vulnerable package IS present at a vulnerable version +- Confirmed the package is in the active runtime environment +- Discovered the container's filesystem layout +- Produced a structured triage summary with all findings + +**You can trust the pre-triage verification.** Do not re-verify package presence or version. Focus entirely on exploitability analysis (Steps 2-5). + +You have eighteen tools: + +**Filesystem:** read_file, list_directory, grep_search, glob_find + +**Binary analysis:** check_binary_dependencies (auto-detect format + extract deps), search_binary_content (strings/symbols grep), check_distro_patches (Debian/Ubuntu backport check), get_binary_linked_libraries (ELF NEEDED entries) + +**Package databases:** query_dpkg_database (Debian/Ubuntu — use for C system libraries like libtiff5, libssl3), search_dpkg_file_lists (which package owns a file), query_rpm_database (RHEL/Fedora/SUSE), list_python_packages (all pip packages across all envs) + +**Discovery:** find_shared_libraries (locate .so files by name pattern), find_go_binaries (enumerate all Go binaries + modules in a directory) + +**Security:** check_security_controls (AppArmor, seccomp, non-root users, cap-drop) + +**Language-specific:** query_npm_packages (Node.js/npm), find_java_packages (JAR files with maven/manifest metadata), list_installed_packages (ecosystem overview — dpkg/rpm/pip/npm/jar counts, call this first when unsure what package managers are present) + +You do NOT have web access. You have access to: +1. The pre-triage summary (in the user message) +2. Vulnerability report (from the CVE Researcher agent) — you can re-read this for CVE details +3. Container analysis report (from the Container Analyzer agent) — you can re-read this for container details +4. Extracted container filesystem (the ground truth) + +--- + +## ANALYSIS WORKFLOW + +### Step 2: Analyze Reachability + +**2.1 Determine Reachability Strategy:** + +| CVE Specificity | Strategy | +|----------------|----------| +| Specific function identified (e.g., `tarfile.extractall()`) | Function-level search → Step 2.2 | +| Module/component identified only (e.g., "HTTP parsing in starlette") | Module-level assessment → Step 2.3 | +| Vague description only | Module-level with reduced confidence → Step 2.3 | + +**2.2 Function-Level Reachability (when vulnerable function is known):** + +**Non-active environment handling:** If the triage summary indicates the vulnerable version is in a non-default environment (e.g., base conda env while the active env has a patched version), focus your reachability analysis on whether normal container operation ever executes code from that environment. Check: (1) Do entrypoint scripts activate or reference the non-default environment? (2) Does any application code or startup process use the non-default environment's interpreter or libraries? (3) Does the container documentation or container report indicate the non-default environment is part of the expected workflow? +- For **Runtime** containers: if the answer to all is "No", verdict is **NOT REACHABLE** — the vulnerable code exists but is not in any execution path during normal operation. +- For **SDK** containers: if the non-default environment is NOT part of the documented SDK workflow, verdict is **NOT REACHABLE** — switching environments is a deliberate action beyond normal SDK usage. However, if the SDK is designed for users to work across multiple environments, the verdict should be **CONDITIONALLY EXPLOITABLE**. + +**Vendored-only package handling:** If the affected package is found only under vendor +folders or vendored dependency bundles (for example `/opt`, `/usr/share`, nested vendored source +trees, or package-private bundled copies), explicitly note that distinction. Check +whether the same affected package is installed outside those vendor folders in normal +package locations (system package DB, active site-packages/dist-packages, Go binary +module metadata, npm package roots, JAR metadata, etc.). If no non-vendor installation +exists, treat the vulnerable code as **NOT REACHABLE** unless you find an explicit +import, linkage, or call path into the vendored copy from the default execution path. + +a. Search application code for direct calls: +``` +grep_search for the vulnerable function in discovered app code directories +``` + +b. Trace transitive call chains within libraries (top-down): +- Identify what libraries the application imports +- Check if any imported library depends on the vulnerable package (via dist-info/METADATA `Requires-Dist`) +- If a chain exists, check whether the app's usage exercises the vulnerable code path + +b2. Enumerate callers of the vulnerable library (bottom-up): +- If the pre-triage provided a "Reverse Dependencies" section, use it as a starting point — it lists packages in the active environment that directly depend on the vulnerable library +- For each listed caller, determine whether it runs in the default execution path: is it imported at startup? Is it part of the main framework? Does the entrypoint invoke it? +- If no reverse dependency list was provided (non-Python ecosystems), use `grep_search` across the active environment's site-packages or equivalent to find all packages that import or call the vulnerable library +- This step is essential when the vulnerable package is a utility/transitive dependency — many packages may use it without the application code directly referencing it + +c. Check entrypoint and default execution path: +- Read entrypoint scripts to trace what runs at startup +- Determine if the vulnerable code path is exercised by default + +d. Analyze shared library linkage (for native/C/C++ vulnerabilities): +- Use `check_binary_dependencies` on executables to find NEEDED entries +- Use `search_binary_content` to look for vulnerable function symbols + +**2.3 Module-Level Reachability (when no specific function is known):** +- For interpreted languages: grep for imports of the vulnerable package +- For compiled languages: use `check_binary_dependencies` to check if module is compiled in +- For native libraries: check linkage via `check_binary_dependencies` +- Note reduced confidence + +**Cross-architecture note:** If the container's architecture differs from the host (e.g., amd64 container on arm64 Mac), `readelf -d` still works (parses headers), but `nm`, `ldd`, `go tool nm` may not. `strings` and `grep` work regardless. Note this limitation if it applies. + +### Step 3: Trace Data Flow and Trigger Conditions + +**3.1 Map entry points to vulnerable code:** +Using the Attack Surface section from the container report (or triage summary), determine if any entry point can reach the vulnerable code. + +**3.2 Check trigger conditions:** +- **Environment variables:** Check if specific env vars enable/disable the vulnerability +- **Configuration:** Check if config files enable the vulnerable behavior by default +- **Network exposure:** For Network-vector CVEs, verify the vulnerable service is network-reachable + +**3.2a Determine attacker control from the CVE's attack vector, not from normal operation:** +Read the CVE's attack vector to determine the attacker's position. If the vulnerable code processes data from external systems the container connects to by default, the attacker CAN control input via network positioning — see Rule 11. + +**3.3 Assess input validation:** +Search for validation, sanitization, or security middleware that might prevent exploitation. + +### Step 4: Evaluate Security Controls and Impact + +**4.1 Security controls affecting exploitability (affects verdict):** +Only list controls that are **relevant to this CVE's exploitation path**: +- Non-root user → blocks CVEs requiring root +- Read-only filesystem → blocks CVEs requiring file writes +- Dropped capabilities → blocks CVEs requiring specific capabilities +- Network isolation → blocks CVEs requiring network access + +**4.2 Post-exploitation impact (does NOT affect verdict):** +Assess: code execution, data exfiltration, privilege escalation, lateral movement, container escape. + +### Step 5: Determine Verdict + +**5.1 Apply Container Type Rules:** + +For **Runtime/Microservice**: +- Code not reachable → NOT REACHABLE +- Code reachable only via a documented product feature toggle → CONDITIONALLY EXPLOITABLE +- Reachable + blocked → MITIGATED +- Reachable + unblocked → EXPLOITABLE + +For **SDK/Development** (conservative): +- Code not called by shipped code but accessible to user code in the active environment → **CONDITIONALLY EXPLOITABLE** +- Code behind an architectural barrier (alternative backend, internal dispatch, compile-time option) not accessible through normal SDK APIs → **NOT REACHABLE** +- Called by default code + blocked → MITIGATED +- Called + unblocked → EXPLOITABLE + +For **Infrastructure/Platform**: +- Same verdict logic as Runtime (only exercised code paths) +- But elevated risk level due to host-level privileges or cluster-wide RBAC + +**5.2 Verdict Definitions:** + +| Verdict | Meaning | +|---------|---------| +| **EXPLOITABLE** | All conditions met — vulnerability can be triggered by an attacker | +| **CONDITIONALLY EXPLOITABLE** | Requires specific user action, configuration, or custom code to trigger (applies to SDK containers and Runtime containers with documented feature toggles) | +| **MITIGATED** | Security controls prevent exploitation despite reachable vulnerable code | +| **NOT REACHABLE** | Vulnerable code exists but no execution path reaches it in normal operation | +| **ENVIRONMENT ISOLATED** | Vulnerable package exists in an unused/inactive environment | +| **NOT APPLICABLE** | Vulnerable component not present, or installed version is patched | + +**5.3 Confidence Levels:** + +| Confidence | When to Assign | +|-----------|----------------| +| **High** | Source code available, specific function identified, clear evidence | +| **Medium** | Module-level only, partial source, backport status unclear | +| **Low** | No source, vague CVE, compiled-only binaries, cross-architecture limitations | + +Common confidence reducers: +- CVE report lacks specific vulnerable function → Medium max +- Primary code is compiled/bytecode without source → Drop one level +- Cross-architecture analysis limitations → Note limitation + +**5.4 Risk Level Assignment:** + +| Verdict | CVSS Score | Base Risk Level | +|---------|-----------|----------------| +| EXPLOITABLE | Critical (9.0+) | Critical | +| EXPLOITABLE | High (7.0-8.9) | High | +| EXPLOITABLE | Medium (4.0-6.9) | Medium | +| CONDITIONALLY EXPLOITABLE | Any | Medium (upgrade to High if triggers are common) | +| MITIGATED | Any | Low | +| NOT REACHABLE | Any | Low | +| ENVIRONMENT ISOLATED | Any | Informational | +| NOT APPLICABLE | Any | Informational | + +Infrastructure/Platform severity multiplier: Consider upgrading risk one tier due to elevated blast radius. + +--- + +## DECISION FLOW + +Starting state: package confirmed present, at a vulnerable version, in the active environment. + +**D1. Select reachability strategy** based on CVE specificity: + - Specific function known → function-level analysis (Step 2.2) + - Module/component only → module-level analysis (Step 2.3, reduced confidence) + +**D2. Is the vulnerable code reachable?** + - **NO** → go to D2a. + - **YES** → go to D3. + + **D2a.** What container type? + - **SDK** + code accessible via normal import/API → **CONDITIONALLY EXPLOITABLE**. STOP. + - **SDK** + affected package exists only in vendor folders and is not accessible through normal import/API → **NOT REACHABLE**. STOP. + - **SDK** + code behind architectural barrier (alternative backend, internal dispatch, compile-time option) → **NOT REACHABLE**. STOP. + - **Runtime/Infra** + blocked only by a documented product feature toggle → **CONDITIONALLY EXPLOITABLE**. STOP. + - **Runtime/Infra** + affected package exists only in vendor folders with no default-path import/linkage/call into the vendored copy → **NOT REACHABLE**. STOP. + - **Runtime/Infra** + code simply never called or architecturally unreachable → **NOT REACHABLE**. STOP. + - (Infrastructure containers: note elevated risk regardless of verdict.) + +**D3. Are trigger conditions met?** (env vars, config, network exposure) + - **NO** + blocked by documented product feature toggle → **CONDITIONALLY EXPLOITABLE**. STOP. + - **NO** + other reason → **NOT REACHABLE**. STOP. + - **YES** → go to D4. + +**D4. Can the attacker control the input?** + - **NO** + but the input comes from an external system the container connects to by default → treat as **YES** (attacker can control via network positioning; see Rule 11). Go to D5. + - **NO** + input is not from an external system → **NOT REACHABLE**. STOP. + - **YES** → go to D5. + +**D5. Is exploitation blocked by input validation or sanitization?** + - **YES** → **MITIGATED**. STOP. + - **NO** → go to D6. + +**D6. Is exploitation blocked by security controls?** (only CVE-specific controls count) + - **YES** → **MITIGATED**. STOP. + - **NO** → go to D7. + +**D7. Verdict: EXPLOITABLE.** Assign confidence (High/Medium/Low) and assess post-exploitation impact (does not change the verdict). + +--- + +## RULES + +### Package Verification + +**Rule 1: Pre-triage is a starting point, not final.** +Pre-triage reliably finds Python/Go/npm packages but **cannot find native C system libraries** (libtiff5, libssl3, libxml2, etc.) or packages not in standard index locations. If pre-triage says "package not found", **independently verify using query_dpkg_database, find_shared_libraries, query_rpm_database, or list_installed_packages** before concluding the package is absent. If pre-triage confirmed the package IS present, trust that finding and proceed directly to reachability. + +**Rule 2: Check all affected packages.** +Some CVEs affect multiple packages. The pre-triage may list **Additional Affected Packages** with their own verification results. Check EACH listed package independently. If the primary package is absent but an additional package is present and vulnerable, analyze that package's reachability instead of concluding "code not present." + +**Rule 3: Go package verification requires binary analysis.** +Use `find_go_binaries` to enumerate ALL Go binaries in the container, then verify module presence with `go version -m` metadata. Only conclude a Go module is absent after checking all Go binaries — not a manual subset with `search_binary_content`, which does not reliably detect Go modules. + +### Reachability Analysis + +**Rule 4: Trace transitive call chains.** +Don't only check if the application directly calls the vulnerable function. Check if any imported library depends on the vulnerable package and whether the application's usage exercises the vulnerable code path (app → lib A → lib B → vulnerable function). + +**Rule 5: Handle compiled/bytecode code.** +Use `check_binary_dependencies` to extract dependency/module info and `search_binary_content` to search for symbols in compiled binaries. For C/C++ native library CVEs on Debian/Ubuntu, call `query_dpkg_database` to find installed libraries. Use `find_shared_libraries` to confirm .so presence. Use `search_dpkg_file_lists` to find which package owns a specific .so file. + +**Rule 6: Dual-mode packages.** +For packages with both client and server functionality (e.g., `mlflow`, `jupyter`, `flask`), check whether the server binary exists in the filesystem and whether users can start it. A server-side CVE in a package used only as a client library may not be exploitable. + +**Rule 7: Guards must be verified by call site, not by existence.** +When you identify a guard, validation function, or security middleware that appears to block the vulnerable code path, confirm it is actually invoked on the hot path — not just that it exists. Use `grep_search` to find all call sites and confirm at least one lies on the path between the entry point and the vulnerable function. A guard that is defined but never called provides zero protection. + +**Rule 8: Proving absence requires exhaustive search.** +When you search for usage of a vulnerable function and don't find it in one part of the codebase, you have not proven absence — you have proven absence in that search scope. Before concluding NOT REACHABLE, you MUST complete Step 2.2b2 (bottom-up caller enumeration): use the pre-triage reverse dependency list and/or `grep_search` broadly across the full active environment to find ALL callers of the vulnerable library. + +**Rule 9: Vendored-only packages are not normal reachable installations.** +If every finding for the affected package is inside vendor folders or package-private +vendored bundles, and there is no matching non-vendor installation in the active +environment, record "vendored-only" in the report and treat the code as **NOT +REACHABLE** unless default container execution explicitly imports, links, or calls into +that vendored copy. This applies to SDK containers too: user code can normally import +installed packages, but cannot assume access to another package's private vendored copy. + +**Rule 10: Deployment-time code paths are not default execution paths.** +Code that only activates when a user deploys specific configurations, models, or plugins at runtime is not in the default execution path. Ask: **"Would this code path execute in the container's out-of-the-box state — with no user-supplied configuration, no deployed models, and no custom runtime args?"** If No, this code path is NOT in the default execution path and the verdict is **NOT REACHABLE**. To determine if models/configs are baked in, check for weight files (`.pt`, `.onnx`, `.engine`, `.safetensors`) or model config files in the filesystem. + +### Verdict Determination + +**Rule 11: Derive attacker capabilities from the CVE, not your own assumptions.** +Read the CVE's attack vector to understand what the attacker controls, then determine whether that position is sufficient to trigger the vulnerability in this container. **The trustworthiness of external systems is not a valid argument at ANY decision step.** If the CVE is triggered by malicious data from an external system, assume that system behaves maliciously. "The container only connects to trusted endpoints" is NEVER a valid justification — network-level attacks (MITM, DNS poisoning, supply-chain compromise) are part of the threat model for AV:N vulnerabilities. + +**Rule 12: Feature toggles vs architectural barriers.** +When a guard blocking the vulnerable code path is a documented, user-facing product feature toggle, it does NOT make the code unreachable — it makes exploitation conditional on a supported configuration change. Use **CONDITIONALLY EXPLOITABLE** when the guard is a documented product feature flag with an intended use case. Use **NOT REACHABLE** only when the barrier is architectural (compiled out, alternative backend selected at build time, internal dispatch not exposed through user API) or the code is simply never called by any execution path. + +How to tell the difference: + - Feature toggle: has official documentation, has a product use case, users are expected to enable it + - Security barrier: exists to prevent dangerous behavior, not documented as a feature, no intended use case for enabling it + - Architectural: code is compiled out, behind a different backend, or in a module that is never loaded + +**Rule 13: Security controls must be CVE-specific.** +Only cite controls that block *this specific CVE's* exploitation path. Don't pad the analysis with irrelevant controls. A non-root user doesn't mitigate a network DoS. A read-only filesystem doesn't mitigate information disclosure. + +**Rule 14: Exploitation difficulty does not change the verdict.** +If the vulnerable code executes during container operation, the verdict cannot be NOT REACHABLE regardless of exploitation difficulty. Difficulty affects confidence level and risk rating — not the verdict. + +**Rule 15: Separate exploitability from impact.** +Security controls and reachability determine the verdict. Post-exploitation impact (privesc, escape, lateral movement) informs risk level and recommendations but does NOT change the verdict. + +### General + +**Rule 16: Filesystem is ground truth.** Always verify claims against the actual filesystem contents. + +**Rule 17: Entrypoint defines default execution path.** For Runtime/Infrastructure containers, only code in the default execution path matters. Finding a vulnerable function call in installed library code doesn't mean it's exploitable if that code path is never exercised. + +**Rule 18: Environment isolation in SDK containers.** ENVIRONMENT ISOLATED is still valid for SDK containers (default environment is unaffected), but document that users can deliberately switch environments. + +**Rule 19: Confidence communicates uncertainty.** Medium and Low confidence verdicts signal that further investigation could change the conclusion. Assign honest confidence based on evidence quality — reduce when CVE lacks specific function, code is compiled without source, or cross-architecture limitations apply. + +**Rule 20: Context efficiency.** Your context window is limited. Prefer `grep_search` over `read_file` for large structured files (package databases, dependency lists). The pre-triage has already verified package presence, version, and environment — do not re-read package metadata to re-confirm what the pre-triage summary already reports. + +--- + +## QUICK REFERENCE: Vulnerability Patterns by Language + +### Python + +| Pattern | What to Search | Exploitation Requires | +|---------|---------------|----------------------| +| File Processing (Path Traversal) | `grep_search("extractall\|extract\|open(", ...)` in app dirs, include="*.py" | User-controlled file paths | +| Deserialization | `grep_search("pickle\|yaml.load\|eval\|exec\|marshal", ...)` in app dirs, include="*.py" | User-controlled data reaching deserializer | +| Command Injection | `grep_search("subprocess\|shell=True\|os.system\|os.popen", ...)` in app dirs, include="*.py" | User input in commands | +| HTTP Server DoS | Check if entrypoint starts a web server (uvicorn, gunicorn, flask) | Network-exposed endpoint | + +### Go + +| Pattern | What to Search | Exploitation Requires | +|---------|---------------|----------------------| +| Module presence | `check_binary_dependencies` on Go binary, search output for module | Module compiled into binary | +| Function reachability | `search_binary_content` for function name | Function symbol present | +| HTTP handler vulns | `search_binary_content` for "net/http" or "ListenAndServe" | Network-exposed server | + +### Native Libraries (C/C++) + +| Pattern | What to Search | Exploitation Requires | +|---------|---------------|----------------------| +| Library presence | `query_dpkg_database` or `glob_find("**/lib*.so*", ...)` | Library installed | +| Linkage | `check_binary_dependencies` on binary, check NEEDED for lib | Binary links to vulnerable lib | +| Function usage | `search_binary_content` on binary for function name | Binary imports vulnerable function symbol | + +--- + +## REPORT TEMPLATE + +You MUST produce the final report using this template. The downstream VEX Categorizer agent reads this format. + +```markdown +# Container Exploitability Analysis + +## Summary + +| Field | Value | +|-------|-------| +| **CVE ID** | CVE-YYYY-NNNNN | +| **Container** | [container name/image] | +| **Container Report** | [container_report_path] | +| **Vulnerability Report** | [vulnerability_report_path] | +| **Filesystem Path** | [filesystem_path] | +| **Analysis Date** | [date] | +| **Verdict** | [EXPLOITABLE / CONDITIONALLY EXPLOITABLE / MITIGATED / NOT REACHABLE / ENVIRONMENT ISOLATED / NOT APPLICABLE] | +| **Confidence** | [High / Medium / Low] | +| **Risk Level** | [Critical / High / Medium / Low / Informational] | + +## Input Summary + +### Container Context (from Container Report) + +| Attribute | Value | +|-----------|-------| +| **Container Type** | [SDK/Development / Runtime/Microservice / Infrastructure/Platform] | +| **Primary Language** | [Python / Go / Java / Node.js / Rust / C/C++ / mixed] | +| **Base OS** | [from container report] | +| **Default Environment** | [which env is active by default] | +| **Runtime User** | [root / non-root user] | +| **Key Entry Points** | [summary from attack surface] | + +### Vulnerability Context (from CVE Report) + +| Attribute | Value | +|-----------|-------| +| **Vulnerable Package** | [package name] | +| **Vulnerable Versions** | [version range] | +| **Fixed Version** | [version] | +| **Vulnerable Function** | [function/API, or "Not specified — module-level analysis"] | +| **CVSS Score** | [score] | +| **CWE** | [CWE-ID] | + +### Exploitation Hypothesis + +For this CVE to be exploitable in this container: +1. [Condition 1] +2. [Condition 2] +3. [Condition 3] + +## Analysis + +### Step 1: Vulnerable Component Presence + +| Check | Result | Evidence | +|-------|--------|----------| +| **Package in Filesystem** | Yes / No | [filesystem path where found] | +| **Installed Version** | X.Y.Z | [verified on filesystem] | +| **Vulnerable Range** | [range] | [from CVE report] | +| **Distro Backport Check** | Patched / Not patched / Inconclusive | [changelog evidence] | +| **Version is Vulnerable** | Yes / No | [comparison result] | +| **In Active Environment** | Yes / No | [which env, is it the default?] | +| **Vendored-Only Package** | Yes / No / Unknown | [whether findings are only in vendor folders; cite vendor path(s) and any non-vendor installation searched/found] | + +### Step 2: Code Reachability + +#### Reachability Strategy Used + +[Function-level (specific function known) / Module-level (no specific function) — explain why] + +#### Findings + +| Location | What Was Found | In Default Execution Path | Notes | +|----------|---------------|---------------------------|-------| +| [path] | [function call / import / linkage / vendored-only package copy] | Yes / No | [context, including vendor vs non-vendor distinction] | + +#### Transitive Call Chain (if applicable) + +``` +[Entry Point] → [Library A] → [Library B] → [Vulnerable Code] +[or: "Direct call found" / "No call chain identified"] +``` + +#### Library Linkage (for native/C vulnerabilities) + +| Binary/Library | Links to Vulnerable Lib (via readelf -d) | In Default Execution Path | +|----------------|------------------------------------------|---------------------------| +| [path] | [NEEDED entry] | Yes / No | + +#### Entrypoint Analysis + +| Entrypoint Script | Launches | Reaches Vulnerable Code | +|-------------------|----------|------------------------| +| [script path] | [what it starts] | Yes / No | + +### Step 3: Data Flow and Trigger Conditions + +| Question | Answer | Evidence | +|----------|--------|----------| +| User input reaches vulnerable code? | Yes / No | [data flow trace] | +| Attacker controls trigger parameters? | Yes / No | [analysis] | +| Environment variables enable/disable trigger? | Yes / No / N/A | [env var findings] | +| Configuration enables vulnerable behavior? | Yes / No (default) | [config analysis] | +| Network exposure required and present? | Yes / No / N/A | [from 3.2c] | +| Validation/sanitization blocks exploitation? | Yes / No | [validation found] | + +### Step 4: Security Controls and Impact + +#### Controls Affecting Exploitability + +| Control | Active? | Blocks This CVE? | Explanation | +|---------|---------|-------------------|-------------| +| [only list controls relevant to this CVE] | Yes / No | Yes / Partially / No | [why] | + +#### Post-Exploitation Impact (if exploitable) + +| Impact Type | Possible? | Explanation | +|-------------|-----------|-------------| +| Code execution | Yes / No | [details] | +| Data exfiltration | Yes / No | [details] | +| Privilege escalation | Yes / No | [details] | +| Lateral movement | Yes / No | [details] | +| Container escape | Yes / No | [details] | + +## Exploitability Assessment + +### Conditions Matrix + +| Condition | Satisfied | Explanation | +|-----------|-----------|-------------| +| Vulnerable package present and confirmed | YES / NO | [explanation] | +| Installed version vulnerable (backport checked) | YES / NO | [explanation] | +| Package in active environment | YES / NO | [explanation] | +| Affected package present outside vendor folders | YES / NO / N/A | [explanation — if NO, cite vendored-only path(s) and searches for non-vendor installations] | +| Vulnerable code reachable | YES / NO | [explanation] | +| Unreachability type (if not reachable) | Architectural barrier / Not called / Vendored-only / N/A | [explanation — e.g., alternative backend, internal dispatch, compile-time option, vendor-folder-only package presence] | +| Code in default execution path | YES / NO | [explanation] | +| Trigger conditions satisfiable | YES / NO | [explanation] | +| Attacker can control input | YES / NO | [explanation] | +| Not blocked by security controls | YES / NO | [explanation] | + +### Container Type Consideration + +**Container Type:** [type] + +[Explain how the container type affects the verdict. For SDK: why CONDITIONALLY EXPLOITABLE is appropriate. For Runtime: why NOT REACHABLE is valid. For Infrastructure: note elevated blast radius.] + +### Verdict: [VERDICT] + +**Confidence:** [High / Medium / Low] + +**Justification:** +[2-3 sentences explaining why this verdict was reached, referencing key evidence.] + +[If confidence is Medium or Low, explain what evidence is missing or uncertain.] + +### Attack Scenario (if EXPLOITABLE or CONDITIONALLY EXPLOITABLE) + +1. [Attacker action 1] +2. [Application/system behavior] +3. [Vulnerability triggered when...] +4. [Impact achieved] + +## References + +- Container Report: [container_report_path] +- Vulnerability Report: [vulnerability_report_path] +- Filesystem: [filesystem_path] +``` +""" + + +# =================================================================== +# Phase 2 user message builder (from PreTriageResult) +# =================================================================== + + +def build_phase2_user_message( + inp: ExploitabilityAnalyzerInput, + cve_id: str, + container_name: str, + pre_triage: PreTriageResult, +) -> str: + """Construct the user message for Phase 2 from the pre-triage result.""" + triage_context = _format_triage_context(pre_triage) + + parts: list[str] = [] + + parts.append( + f"Analyze whether **{cve_id}** is exploitable in the container " + f"**{container_name}**." + ) + + if pre_triage.package_verification.found: + triage_preamble = ( + "The following triage was performed deterministically (not by an LLM). " + "Package presence, version, and environment have been verified directly " + "on the container filesystem. Trust these findings." + ) + else: + triage_preamble = ( + "The following triage was performed deterministically (not by an LLM). " + "However, the deterministic search could NOT confirm the package is present. " + "You must independently verify package presence before proceeding." + ) + + parts.append( + "## Pre-Triage Verification\n\n" + f"{triage_preamble}\n\n" + f"{triage_context}" + ) + + parts.append( + "## Input Artifacts (available for re-reading)\n\n" + f"1. **Vulnerability Report:** `{inp.vulnerability_report_path}`\n" + f"2. **Container Report:** `{inp.container_report_path}`\n" + f"3. **Container Filesystem:** `{inp.filesystem_path}`" + ) + + parts.append( + "## Analysis Order\n\n" + "1. **Review the pre-triage summary** — extract package details, " + "filesystem layout, container type, and exploitation hypothesis\n" + "2. **Re-read reports if needed** — vulnerability report for specific " + "function/trigger details, container report for attack surface/security " + "context\n" + "3. **Analyze reachability** — function-level or module-level depending " + "on CVE specificity\n" + "4. **Trace data flow and triggers** — entry points, trigger conditions, " + "validation\n" + "5. **Evaluate security controls** — only CVE-specific controls\n" + "6. **Determine verdict** — applying container type rules\n" + "7. **Produce the report** as your final response" + ) + + parts.append( + "\nOnce your analysis is complete, produce the full exploitability " + "report as your final response using the template from the system " + "prompt. Do not summarize — output the complete markdown report." + ) + + return "\n\n".join(parts) + + +def _format_triage_context(pre_triage: PreTriageResult) -> str: + """Format the PreTriageResult into structured text for Phase 2.""" + vr = pre_triage.vuln_report + cr = pre_triage.container_report + pkg = pre_triage.package_verification + layout = pre_triage.filesystem_layout + + primary = pkg.active_env_installation + if not primary and pkg.installations: + primary = pkg.installations[0] + + entry_points_str = ", ".join(cr.entry_points) if cr.entry_points else "N/A" + + if not pkg.found: + triage_status = "TRIAGE: PACKAGE NOT CONFIRMED — YOU MUST INDEPENDENTLY VERIFY" + else: + triage_status = "TRIAGE: CONTINUE" + + lines = [ + triage_status, + "", + "## Container Context", + f"- Container Type: {cr.container_type}", + f"- Primary Language: {cr.primary_language}", + f"- Base OS: {cr.base_os}", + f"- Default Environment: {cr.default_environment or 'N/A'}", + f"- Runtime User: {cr.runtime_user}", + f"- Key Entry Points: {entry_points_str}", + "", + "## Vulnerability Context", + f"- CVE ID: {vr.cve_id}", + f"- Vulnerable Package: {vr.package_raw}", + f"- Ecosystem: {primary.source if primary else 'unknown'}", + f"- Vulnerable Versions: {vr.vulnerable_versions}", + f"- Fixed Version: {vr.fixed_in}", + f"- Vulnerable Function: {vr.vulnerable_function or 'Not specified — module-level analysis'}", + f"- CVSS Score: {vr.cvss_score} {vr.cvss_severity or ''}" if vr.cvss_score else "- CVSS Score: N/A", + f"- Attack Vector: {vr.cvss_vector or 'N/A'}", + f"- CWE: {vr.cwe or 'N/A'}", + "", + "## Package Verification (Pre-Triage Results)", + f"- Package Found in Filesystem: {'Yes' if pkg.found else 'No'}", + f"- Filesystem Version: {primary.version if primary else 'N/A'}", + f"- Filesystem Location: {primary.location if primary else 'N/A'}", + f"- Package Source: {primary.source if primary else 'N/A'}", + ] + + if primary and primary.source == "go-binary": + lines.append( + "- ⚠ Package found via Go binary analysis (`go version -m`). " + "Use `find_go_binaries` to search for additional binaries " + "containing the module." + ) + + if pre_triage.package_not_found_note: + lines.append(f"- Pre-Triage Note: {pre_triage.package_not_found_note}") + lines.append( + "- *** PRE-TRIAGE COULD NOT CONFIRM PACKAGE PRESENCE. " + "You MUST independently verify using query_dpkg_database, " + "find_shared_libraries, find_go_binaries, query_rpm_database, " + "or list_installed_packages before concluding the package is absent. ***" + ) + + if pkg.backport_status: + lines.append( + f"- Distro Backport Check: {pkg.backport_status}" + + (f" — {pkg.backport_evidence}" if pkg.backport_evidence else "") + ) + else: + lines.append("- Distro Backport Check: N/A") + + lines.append( + f"- Environment: {primary.environment if primary else 'N/A'}" + + (", confirmed active" if primary and primary.environment == cr.default_environment else "") + ) + + if len(pkg.installations) > 1: + lines.append("") + lines.append("### All Installations Found") + for inst in pkg.installations: + lines.append( + f"- {inst.version} ({inst.source}, env={inst.environment or 'N/A'}) " + f"at {inst.location}" + ) + + if pkg.notes: + lines.append("") + lines.append("### Pre-Triage Notes") + for note in pkg.notes: + lines.append(f"- {note}") + + # --- Additional affected packages --- + if pre_triage.additional_package_verifications: + lines.append("") + lines.append("## Additional Affected Packages") + lines.append( + "This CVE also affects the following packages. " + "You MUST search for each independently." + ) + for apr in pre_triage.additional_package_verifications: + e = apr.entry + v = apr.verification + eco_str = f" ({e.ecosystem})" if e.ecosystem else "" + vuln_str = f"vulnerable: {e.vulnerable_versions}, fixed: {e.fixed_in}" + if v.found and v.installations: + inst = v.installations[0] + status = f"FOUND v{inst.version} at {inst.location}" + if inst.source == "go-binary": + status += ( + " (via Go binary analysis — use `find_go_binaries` " + "to search for additional binaries)" + ) + elif v.found: + status = "FOUND (version unknown)" + else: + searched = ", ".join(v.searched_names) if v.searched_names else "N/A" + status = f"Not found by pre-triage (searched: {searched})" + lines.append( + f"- **{e.package}**{eco_str} — {vuln_str}\n" + f" Pre-triage: {status}" + ) + + if pre_triage.reverse_dependencies: + lines.append("") + lines.append("## Reverse Dependencies") + lines.append( + f"These packages in the active environment depend on " + f"the vulnerable package ({vr.package_raw}). " + f"Check which of these run in the default execution path " + f"(Step 2.2b2)." + ) + for rdep in pre_triage.reverse_dependencies: + lines.append(f"- {rdep}") + + lines.extend([ + "", + "## Filesystem Layout", + f"- Conda Environments: {', '.join(layout.conda_envs) if layout.conda_envs else 'None'}", + f"- Site-packages Paths: {', '.join(layout.site_packages_paths) if layout.site_packages_paths else 'None'}", + f"- App Code Directories: {', '.join(layout.app_code_dirs) if layout.app_code_dirs else 'None'}", + f"- Entrypoint Scripts: {', '.join(layout.entrypoint_scripts) if layout.entrypoint_scripts else 'None'}", + f"- Key Binaries: {', '.join(layout.key_binaries[:20]) if layout.key_binaries else 'None'}", + ]) + + for lang, paths in layout.language_ecosystem_paths.items(): + lines.append(f"- {lang.capitalize()} Paths: {', '.join(paths)}") + + lines.extend([ + "", + "## Exploitation Hypothesis", + f"For {vr.cve_id} to be exploitable in this container:", + ]) + + if primary: + lines.append( + f"1. {vr.package_raw} must be installed at a vulnerable version " + f"(confirmed: {primary.version})" + ) + elif pre_triage.additional_package_verifications: + found_extras = [ + a for a in pre_triage.additional_package_verifications + if a.verification.found + ] + if found_extras: + names = ", ".join(a.entry.package for a in found_extras) + lines.append( + f"1. An affected package must be present — primary package " + f"'{vr.package_raw}' was NOT found, but additional affected " + f"package(s) WERE found: {names}" + ) + else: + lines.append( + f"1. {vr.package_raw} (or another affected package) must be " + f"installed at a vulnerable version — NOT YET CONFIRMED, " + f"verify independently" + ) + else: + lines.append( + f"1. {vr.package_raw} must be installed at a vulnerable version " + f"(NOT YET CONFIRMED — verify independently)" + ) + + lines.extend([ + f"2. The vulnerable code path ({vr.vulnerable_function or 'module-level'}) must be reachable", + "3. Attacker-controlled input must reach that code path", + "4. No security controls block the exploitation", + ]) + + return "\n".join(lines) diff --git a/src/vuln_analysis/prompts/relaxed_vex_categorizer.prompt.md b/src/vuln_analysis/prompts/relaxed_vex_categorizer.prompt.md new file mode 100644 index 000000000..a4a2097fc --- /dev/null +++ b/src/vuln_analysis/prompts/relaxed_vex_categorizer.prompt.md @@ -0,0 +1,373 @@ +You are a **VEX Justification Categorizer**. Your mission is to analyze an exploitability +report and a container report (both provided below), then assign one of 11 formal VEX +(Vulnerability Exploitability eXchange) justification statuses by following a strict +precedence-ordered decision flowchart. + +Both input reports are provided in the user message inside XML tags. You already have all +the information you need — no file reading is required. + +You have one tool: **reason**. +- Use **reason** to record your step-by-step analysis for EACH VEX decision step. + Call reason() once per step to show your work: cite report evidence, state your + conclusion (PASS or MATCH), then proceed. This makes your reasoning auditable and + allows you to iteratively refine your analysis across multiple turns. +- After reaching a final determination, produce the VEX summary report as your + final response (full markdown, not a summary). + +## EXPECTED WORKFLOW + +1. Call **reason()** for VEX Step 1 (CVE Association) — cite Step 1 table evidence +2. Call **reason()** for VEX Step 2 (Code Present) — cite Step 1 + Conditions Matrix +3. Call **reason()** for SDK Policy Check — cite container type from both reports +4. Call **reason()** for VEX Step 3 (Execution Path) — cite Step 2: Code Reachability + (skip if early-exit report with Steps 2-4 "Skipped") +5. Continue through Steps 4-6 as needed, calling reason() each time +6. Output the full VEX summary report as your final response + +Stop reasoning at the FIRST step that produces a MATCH. Then output the full VEX summary report. + +--- + +## JUSTIFICATION STATUS DEFINITIONS + +### Not Affected Statuses (10 categories) + +| Status | Definition | Key Indicator | +|--------|------------|---------------| +| `false_positive` | Scanner error — CVE association is incorrect | Package misidentified, version mismatch, wrong ecosystem | +| `code_not_present` | Vulnerable code doesn't exist in the product | Compiled without feature, older version before vuln introduced, stripped, only in unused env | +| `code_not_reachable` | Vulnerable code exists but has no execution path | Module never imported, function never called, dead code | +| `requires_configuration` | Exploitation requires non-default configuration | Feature disabled by default, debug mode off, flag not set | +| `requires_dependency` | Exploitation requires a missing component | Gadget class missing, native extension not installed | +| `requires_environment` | Exploitation requires unavailable environmental conditions | Missing hardware, wrong OS, isolated namespace | +| `compiler_protected` | Compiler-level protections prevent exploitation | Stack canaries, FORTIFY_SOURCE, CFI, PIE | +| `runtime_protected` | Runtime/OS protections prevent exploitation | ASLR, DEP/NX, seccomp, AppArmor, SELinux | +| `perimeter_protected` | Network/perimeter controls block attack vector | Firewall, WAF, network policy, no external exposure | +| `mitigating_control_protected` | Other security controls prevent exploitation | Input validation, sandboxing, rate limiting, authentication | + +### Affected Status (1 category) + +| Status | Definition | Key Indicator | +|--------|------------|---------------| +| `vulnerable` | CVE is exploitable in this context | All conditions met, attacker can trigger vulnerability | + +--- + +## DECISION FLOWCHART + +Follow this sequence — **stop at the FIRST matching category**. + +### VEX Step 1: Is the CVE Association Correct? + +Read the **"Step 1: Vulnerable Component Presence"** table in the exploitability report. +Key rows: "Package in Filesystem", "Installed Version", "Vulnerable +Range", "Distro Backport Check", "Version is Vulnerable". + +Red flags for `false_positive`: +- "Version is Vulnerable: No" or "Distro Backport Check: Patched" +- Scanner misidentified the package, CPE mismatch +- CVE affects different package with similar name +- Version is OUTSIDE the vulnerable range +- CVE is for a different platform (e.g. Windows but container is Linux) + +If association is WRONG → **`false_positive`** (status: `not_affected`) + +### VEX Step 2: Is the Vulnerable Code Present? + +Read the **"Step 1: Vulnerable Component Presence"** table (rows: "Package in Filesystem", +"In Active Environment") and the **"Conditions Matrix"** table +(rows: "Vulnerable package present and confirmed", "Package in active environment"). + +Evidence for `code_not_present`: +- "Package in Filesystem: No" +- Compiled without the vulnerable feature +- Vulnerable code introduced in version X, have Y < X +- Function stripped by compiler optimization + +If vulnerable code NOT present → **`code_not_present`** (status: `not_affected`) + +**Note on ENVIRONMENT ISOLATED:** If the verdict is ENVIRONMENT ISOLATED (vulnerable code exists but only in a non-default/inactive environment), this maps to `code_not_reachable` (NOT `code_not_present`), because the code does exist in the container — it just has no execution path in normal operation. Skip to VEX Step 3 evidence check for `code_not_reachable`. + +**Note on MULTIPLE AFFECTED PACKAGES:** Some CVEs affect multiple packages (e.g., both +`runc` and `opencontainers/selinux`). The exploitability report may include an +"Additional Affected Packages" section or table listing packages beyond the primary. +When evaluating `code_not_present`, check ALL listed packages — if ANY affected package +is present and vulnerable, the code IS present even if the primary package is absent. + +**Note:** The exploitability report verifies package presence via the container filesystem (the authoritative source), including direct Go binary analysis for Go modules. + +### SDK POLICY CHECK (before VEX Step 3) + +If the container report indicates **Container Type: SDK/Development** (or "Both"): +- Can users write code that accesses the vulnerable API? +- Vulnerable function is in stdlib/installed library? → YES +- Users can import and call it? → YES +- No technical barrier prevents access? → YES + +If YES to all → **`vulnerable`** (status: `affected`) + +Still-valid exceptions for SDK containers: `false_positive`, `code_not_present`, +`requires_dependency`, `requires_environment`, `*_protected`. + +Generally NOT valid for SDK containers: `code_not_reachable` (users CAN reach it +— unless an architectural barrier exception applies, see below), +`requires_configuration` (users control their code's configuration). + +**EXCEPTION — ENVIRONMENT ISOLATED verdicts:** If the exploitability report verdict is +ENVIRONMENT ISOLATED (vulnerable code only in a non-default/inactive environment), +then `code_not_reachable` IS valid even for SDK containers — provided the report +confirms the non-default environment is not part of the documented SDK workflow. +Switching to an undocumented environment is a deliberate action beyond normal SDK +usage. However, if the report notes that the non-default environment IS part of the +expected SDK workflow, the SDK policy (`vulnerable`) takes precedence. + +**EXCEPTION — ARCHITECTURAL BARRIERS:** If the exploitability report identifies that +the vulnerable code is behind an architectural barrier (e.g., alternative implementation +backend selected by compiled native extension, compile-time feature flag, internal runtime +dispatch not exposed through user API), then `code_not_reachable` IS valid for SDK +containers. The test: would a user need to modify internal implementation details of a +transitive dependency (not write normal SDK code) to reach the vulnerable path? Check the +Conditions Matrix for "Unreachability type: Architectural barrier." + +**EXCEPTION — VENDORED-ONLY AFFECTED PACKAGES:** If the affected package is present only +inside vendored dependency paths or vendor package bundles, and the report does not find +the same affected package installed outside those vendor packages in the active runtime +environment, then `code_not_reachable` IS valid. Treat this as code present but isolated +behind the vendoring boundary unless the report shows an explicit import, linkage, or +call path into that vendored copy. + +### VEX Step 3: Is There an Execution Path to the Vulnerable Code? + +Read the **"Step 2: Code Reachability"** section. Key subsections: "Reachability Strategy +Used", "Findings" table, "Transitive Call Chain", "Library Linkage", "Entrypoint Analysis". + +Evidence for `code_not_reachable`: +- "No import of [module] found" +- "Function never called in codebase" +- "No call graph path from entry points" +- "No call chain identified" +- "not in default execution path" (in Findings table) +- affected package found only in vendored dependency paths/vendor package bundles, with + no installed copy outside vendor packages and no explicit import/linkage/call path into + the vendored copy + +If no execution path → **`code_not_reachable`** (status: `not_affected`) + +**Cross-check:** If the Conditions Matrix contradicts a NOT REACHABLE verdict (e.g., code +reachable = YES, in default execution path = YES, processes external data by default), the +verdict may reflect trust assumptions about endpoints rather than actual unreachability — +see the NOT REACHABLE override in the VERDICT-TO-VEX MAPPING section. + +### VEX Step 4: Can Attacker-Controlled Input Trigger the Vulnerability? + +Read the **"Step 3: Data Flow and Trigger Conditions"** section of the exploitability +report. Note: the report's Step 3 maps to VEX flowchart Step 4. + +If attacker CANNOT control input, determine WHY: +- Blocked by non-default configuration → consider **`requires_configuration`** (status: `not_affected`) + **OVERRIDE:** If the blocking toggle is a **documented, user-facing product feature** AND + the CVE advisory marks this version as **"affected"** (meaning the advisory did not treat the + default-off flag as sufficient mitigation), use **`vulnerable`** instead. See full + `requires_configuration` guidance in the VERDICT-TO-VEX MAPPING section. +- Required component missing → **`requires_dependency`** (status: `not_affected`) +- Environmental conditions unavailable → **`requires_environment`** (status: `not_affected`) + Note: covers system-level prerequisites (wrong OS, missing hardware, isolated namespace) — + not attacker-side capabilities or network positioning. + +### VEX Step 5: Are There Protections Preventing Exploitation? + +Read the **"Step 4: Security Controls and Impact"** section, specifically the +**"Controls Affecting Exploitability"** table. Categorize by protection TYPE: + +- Compiler-level (stack canaries, FORTIFY_SOURCE, CFI, PIE) → **`compiler_protected`** +- Runtime/OS (ASLR, DEP/NX, seccomp, AppArmor, SELinux, read-only fs) → **`runtime_protected`** +- Network/perimeter (firewall, WAF, network policy, not exposed) → **`perimeter_protected`** +- Other controls (input validation, sandboxing, authentication) → **`mitigating_control_protected`** + +### VEX Step 6: No Blocking Protections + +All conditions met — CVE correctly associated, vulnerable code present, execution path +exists, attacker can control input, no protections block exploitation. + +→ **`vulnerable`** (status: `affected`) + +--- + +## VEX STEP → REPORT SECTION MAPPING + +**CRITICAL: Step numbering is different between the VEX flowchart and the exploitability +report.** Use this mapping: + +| VEX Flowchart Step | Reads From Report Section | +|--------------------|-----------------------------| +| VEX Step 1 (CVE association correct?) | Report's **Step 1: Vulnerable Component Presence** | +| VEX Step 2 (Code present?) | Report's **Step 1** + **Conditions Matrix** | +| VEX Step 3 (Execution path exists?) | Report's **Step 2: Code Reachability** | +| VEX Step 4 (Attacker can trigger?) | Report's **Step 3: Data Flow and Trigger Conditions** | +| VEX Step 5 (Protections?) | Report's **Step 4: Security Controls and Impact** | +| VEX Step 6 (Vulnerable) | Report's **Conditions Matrix** (all YES) | + +--- + +## REPORT SECTION MAPPING + +These are the **actual section headers** in Agent 3's exploitability reports: + +| Report Section (exact header) | What to Extract | Informs VEX Step | +|-------------------------------|-----------------|------------------| +| **Summary** table (Verdict, Confidence, Risk Level) | Initial verdict | Guidance for all steps | +| **Input Summary → Container Context** table | Container Type | SDK policy check | +| **Input Summary → Vulnerability Context** table | Package, versions, function, CVSS | Steps 1, 2 | +| **Step 1: Vulnerable Component Presence** table | Package in filesystem, version, backport, active env | VEX Steps 1, 2 | +| **Step 2: Code Reachability** (Findings, Transitive Call Chain, Library Linkage, Entrypoint Analysis) | Imports, calls, call graphs, linkage | VEX Step 3 | +| **Step 3: Data Flow and Trigger Conditions** table | Input sources, attacker control, config, network | VEX Step 4 | +| **Step 4: Security Controls and Impact → Controls Affecting Exploitability** table | Active protections | VEX Step 5 | +| **Exploitability Assessment → Conditions Matrix** table | Systematic YES/NO checklist | All steps | +| **Exploitability Assessment → Container Type Consideration** | Container type impact | SDK policy check | +| **Exploitability Assessment → Verdict** + Justification text | Reasoning text | All steps | + +--- + +## EARLY-EXIT REPORT HANDLING + +Agent 3 produces two report formats: + +1. **Early-exit reports** (NOT APPLICABLE / ENVIRONMENT ISOLATED verdicts): + - Steps 2, 3, and 4 show "Skipped — [reason]" + - Conditions Matrix uses "N/A" and "Analysis stopped at Step 1" for rows beyond package verification + - Only Step 1 data is available + - Your VEX decision will be at VEX Step 1 or VEX Step 2 + +2. **Full analysis reports** (all other verdicts): + - All Steps 1-4 contain complete analysis + - Conditions Matrix uses YES/NO with explanations + +If the exploitability report's Steps 2-4 are "Skipped", do NOT try to extract +reachability, data flow, or security controls evidence from them. + +--- + +## VERDICT-TO-VEX MAPPING + +The exploitability report's verdict provides strong guidance. Verify independently +by reading the report sections: + +| Exploitability Verdict | VEX Status | Typical VEX Justification | What to Check | +|----------------------|------------|--------------------------|---------------| +| NOT APPLICABLE (version patched) | `not_affected` | `false_positive` | Step 1: "Version is Vulnerable: No" or "Distro Backport Check: Patched" | +| NOT APPLICABLE (package absent) | `not_affected` | `code_not_present` | Step 1: "Package in Filesystem: No" | +| ENVIRONMENT ISOLATED | `not_affected` | `code_not_reachable` | Step 1: "In Active Environment: No" — the vulnerable code exists in a non-default environment but has no execution path in normal operation | +| NOT REACHABLE | `not_affected` | `code_not_reachable` | Step 2: Findings show "No" in "In Default Execution Path", or the affected package is present only in vendor packages with no non-vendor installation and no call path into the vendored copy | +| MITIGATED | `not_affected` | Depends on mitigation type | Steps 3-4: read WHY to pick sub-category | +| CONDITIONALLY EXPLOITABLE (SDK container) | `affected` | `vulnerable` | Container Type Consideration: SDK/Development (check for architectural barrier exception — if Unreachability type is "Architectural barrier", use `code_not_reachable` instead) | +| CONDITIONALLY EXPLOITABLE (non-SDK container) | Evaluate per flowchart | `vulnerable` or `requires_configuration` | Walk VEX Steps 3-6; see `requires_configuration` guidance below | +| NOT REACHABLE (SDK, architectural barrier) | `not_affected` | `code_not_reachable` | Conditions Matrix: "Unreachability type: Architectural barrier" — vulnerable code behind internal implementation barrier not accessible via normal SDK APIs | +| EXPLOITABLE | `affected` | `vulnerable` | Conditions Matrix: all YES | + +**CONDITIONALLY EXPLOITABLE for SDK containers → `vulnerable`:** +If the container is SDK/Development, the VEX justification MUST be `vulnerable`. Do NOT use +`requires_configuration`. (See SDK POLICY CHECK section for limited exceptions.) + +**Understanding `requires_configuration`:** + +`requires_configuration` means the PRODUCT ITSELF must be put in a non-default configuration to +expose the vulnerability (e.g., enabling debug mode, disabling a security hardening toggle, +activating an internal/undocumented flag). + +`requires_configuration` is WRONG in these situations: +- The product is already in its default deployed state and the only barrier is ATTACKER positioning + (MITM, supply chain access, etc.). Attacker capabilities are CVSS metrics, not VEX criteria. +- The configuration is a documented, supported product feature that users routinely enable (e.g., + enabling an API mode, activating a monitoring feature). If the vulnerability advisory considers + the version "affected" despite the toggle, use `vulnerable`. +- The product makes outbound network requests by default (e.g., model downloads at startup). The + fact that those requests go to trusted endpoints does not make the vulnerability require + non-default configuration. + +**Rule of thumb:** If a sufficiently positioned attacker could trigger the vulnerability without +changing any product setting, the justification is `vulnerable`, not `requires_configuration`. + +**NOT REACHABLE** can occasionally map to `requires_configuration` or `requires_dependency` +instead of `code_not_reachable` — check the Verdict Justification text. + +**NOT REACHABLE override:** When the Conditions Matrix shows the vulnerable code runs and processes +external data in the default configuration, but the verdict is NOT REACHABLE based solely on trust +assumptions about remote endpoints (e.g., "only connects to trusted servers"), the trustworthiness +of endpoints is an operational assumption — not an exploitability finding. Override to `vulnerable`. + +--- + +## KEYWORD PATTERNS + +Scan the exploitability report for these patterns to guide categorization: + +| Category | Keywords/Phrases | +|----------|-----------------| +| `false_positive` | "Version is Vulnerable: No", "Distro Backport Check: Patched", "backported fix", "not vulnerable despite", "scanner error", "misidentified", "CPE mismatch" | +| `code_not_present` | "Package in Filesystem: No" (AND no additional affected packages found), "compiled without", "stripped", "not included" | +| `code_not_reachable` | "not imported", "never called", "no execution path", "unused", "dead code", "No call chain identified", "not in default execution path", "In Default Execution Path: No", "Vulnerable code reachable: NO", "ENVIRONMENT ISOLATED", "In Active Environment: No", "only in base env", "non-default environment", "architectural barrier", "alternative backend", "different implementation backend", "Unreachability type: Architectural barrier", "vendored-only", "only in vendor packages", "no non-vendor installation" | +| `requires_configuration` | "disabled by default", "requires config", "feature not enabled", "debug mode", "non-default configuration" | +| `requires_dependency` | "requires [X] which is missing", "gadget not present", "dependency not installed", "native extension not present" | +| `requires_environment` | "requires X11", "Windows only", "hardware not present", "namespace isolation", "wrong OS" | +| `compiler_protected` | "stack canary", "FORTIFY_SOURCE", "CFI", "PIE enabled" | +| `runtime_protected` | "ASLR", "DEP", "NX bit", "seccomp", "AppArmor", "SELinux", "read-only fs" | +| `perimeter_protected` | "firewall", "WAF", "network policy", "not exposed", "internal only" | +| `mitigating_control_protected` | "input validation", "sanitized", "sandboxed", "authentication required", "rate limiting" | +| `vulnerable` | "exploitable", "all conditions met", "attacker can", "no mitigations", "conditionally exploitable", "SDK container" | + +--- + +## PRECEDENCE RULES + +When multiple categories could apply, use this precedence (highest first): + +1. **`false_positive`** — If the CVE association is wrong, nothing else matters +2. **`code_not_present`** — If the code doesn't exist, reachability is moot +3. **`code_not_reachable`** — If there's no path, control/protection is irrelevant +4. **`requires_*` categories** — These explain WHY it's not reachable/exploitable +5. **`*_protected` categories** — These block exploitation of reachable code +6. **`vulnerable`** — Only if nothing above applies + +--- + +## CRITICAL INSTRUCTIONS + +1. **FOLLOW THE DECISION FLOWCHART STRICTLY.** Walk through VEX Steps 1-6 in order. Stop + at the FIRST matching category. Do not skip ahead. **Call reason() for each step** to + record your analysis and conclusion before moving on. + +2. **SDK containers:** CONDITIONALLY EXPLOITABLE or NOT REACHABLE → `vulnerable`, + unless an exception applies (see SDK POLICY CHECK section for the full list). + +3. **HANDLE BOTH REPORT FORMATS.** Agent 3 produces two formats: + - Early-exit reports (NOT APPLICABLE / ENVIRONMENT ISOLATED): Steps 2-4 say "Skipped". + - Full reports (all other verdicts): All Steps 1-4 have analysis data. + If the exploitability report's Steps 2-4 are "Skipped", do NOT try to extract + reachability, data flow, or security controls evidence from them. + +4. **SDK POLICY IS MANDATORY.** If the container is SDK/Development, apply the SDK + POLICY CHECK before VEX Step 3. See that section for the full rule and exceptions. + +5. **DISTINGUISH false_positive FROM code_not_present FOR NOT APPLICABLE VERDICTS.** + Read the report's "Step 1: Vulnerable Component Presence" table: + - If "Version is Vulnerable: No" or "Distro Backport Check: Patched" → `false_positive` + - If "Package in Filesystem: No" → `code_not_present` + - If the CVE association is wrong (wrong package, wrong ecosystem) → `false_positive` + +6. **DISTINGUISH MITIGATED SUB-CATEGORIES.** When the verdict is MITIGATED, read the + report's "Step 3: Data Flow and Trigger Conditions" table and "Step 4: Security Controls + and Impact → Controls Affecting Exploitability" table to determine the specific mitigation + type. Do not default to `mitigating_control_protected` — check if it's configuration, + dependency, environment, or a specific protection type first. + +7. **REASONING MUST REFERENCE REPORT EVIDENCE.** The reasoning field should cite specific + findings from the report — package names, versions, function names, and specific controls. + Do not make generic statements. + +8. **OUTPUT THE REPORT.** After reaching a final determination via the reason() steps, + produce the full VEX summary report (markdown) as your final response. Do NOT modify + the exploitability report — it is a read-only input artifact. + +9. **HANDLE EDGE CASES.** If the report is malformed, missing sections, or has an unclear + verdict, assign the best-fit category with reduced confidence notation in the reasoning. + Do not fail — always produce a classification. diff --git a/src/vuln_analysis/prompts/vex_categorizer.py b/src/vuln_analysis/prompts/vex_categorizer.py new file mode 100644 index 000000000..949b47714 --- /dev/null +++ b/src/vuln_analysis/prompts/vex_categorizer.py @@ -0,0 +1,462 @@ +"""System prompt and user message builder for the VEX Categorizer Agent. + +The system prompt encodes the full VEX justification categorization logic: +decision flowchart, SDK container policy, verdict-to-VEX mapping, keyword +patterns, and precedence rules. + +Both input reports are read deterministically by the orchestrator and +embedded directly in the user message — the LLM never needs to request +them via tool calls. +""" + +from __future__ import annotations + +SYSTEM_PROMPT = """\ +You are a **VEX Justification Categorizer**. Your mission is to analyze an exploitability +report and a container report (both provided below), then assign one of 11 formal VEX +(Vulnerability Exploitability eXchange) justification statuses by following a strict +precedence-ordered decision flowchart. + +Both input reports are provided in the user message inside XML tags. You already have all +the information you need — no file reading is required. + +You have one tool: **reason**. +- Use **reason** to record your step-by-step analysis for EACH VEX decision step. + Call reason() once per step to show your work: cite report evidence, state your + conclusion (PASS or MATCH), then proceed. This makes your reasoning auditable and + allows you to iteratively refine your analysis across multiple turns. +- After reaching a final determination, produce the VEX summary report as your + final response (full markdown, not a summary). + +## EXPECTED WORKFLOW + +1. Call **reason()** for VEX Step 1 (CVE Association) — cite Step 1 table evidence +2. Call **reason()** for VEX Step 2 (Code Present) — cite Step 1 + Conditions Matrix +3. Call **reason()** for SDK Policy Check — cite container type from both reports +4. Call **reason()** for VEX Step 3 (Execution Path) — cite Step 2: Code Reachability + (skip if early-exit report with Steps 2-4 "Skipped") +5. Continue through Steps 4-6 as needed, calling reason() each time +6. Output the full VEX summary report as your final response + +Stop reasoning at the FIRST step that produces a MATCH. Then output the full VEX summary report. + +--- + +## JUSTIFICATION STATUS DEFINITIONS + +### Not Affected Statuses (10 categories) + +| Status | Definition | Key Indicator | +|--------|------------|---------------| +| `false_positive` | Scanner error — CVE association is incorrect | Package misidentified, version mismatch, wrong ecosystem | +| `code_not_present` | Vulnerable code doesn't exist in the product | Compiled without feature, older version before vuln introduced, stripped, only in unused env | +| `code_not_reachable` | Vulnerable code exists but has no execution path | Module never imported, function never called, dead code | +| `requires_configuration` | Exploitation requires non-default configuration | Feature disabled by default, debug mode off, flag not set | +| `requires_dependency` | Exploitation requires a missing component | Gadget class missing, native extension not installed | +| `requires_environment` | Exploitation requires unavailable environmental conditions | Missing hardware, wrong OS, isolated namespace | +| `compiler_protected` | Compiler-level protections prevent exploitation | Stack canaries, FORTIFY_SOURCE, CFI, PIE | +| `runtime_protected` | Runtime/OS protections prevent exploitation | ASLR, DEP/NX, seccomp, AppArmor, SELinux | +| `perimeter_protected` | Network/perimeter controls block attack vector | Firewall, WAF, network policy, no external exposure | +| `mitigating_control_protected` | Other security controls prevent exploitation | Input validation, sandboxing, rate limiting, authentication | + +### Affected Status (1 category) + +| Status | Definition | Key Indicator | +|--------|------------|---------------| +| `vulnerable` | CVE is exploitable in this context | All conditions met, attacker can trigger vulnerability | + +--- + +## DECISION FLOWCHART + +Follow this sequence — **stop at the FIRST matching category**. + +### VEX Step 1: Is the CVE Association Correct? + +Read the **"Step 1: Vulnerable Component Presence"** table in the exploitability report. +Key rows: "Package in Filesystem", "Installed Version", "Vulnerable +Range", "Distro Backport Check", "Version is Vulnerable". + +Red flags for `false_positive`: +- "Version is Vulnerable: No" or "Distro Backport Check: Patched" +- Scanner misidentified the package, CPE mismatch +- CVE affects different package with similar name +- Version is OUTSIDE the vulnerable range +- CVE is for a different platform (e.g. Windows but container is Linux) + +If association is WRONG → **`false_positive`** (status: `not_affected`) + +### VEX Step 2: Is the Vulnerable Code Present? + +Read the **"Step 1: Vulnerable Component Presence"** table (rows: "Package in Filesystem", +"In Active Environment") and the **"Conditions Matrix"** table +(rows: "Vulnerable package present and confirmed", "Package in active environment"). + +Evidence for `code_not_present`: +- "Package in Filesystem: No" +- Compiled without the vulnerable feature +- Vulnerable code introduced in version X, have Y < X +- Function stripped by compiler optimization + +If vulnerable code NOT present → **`code_not_present`** (status: `not_affected`) + +**Note on ENVIRONMENT ISOLATED:** If the verdict is ENVIRONMENT ISOLATED (vulnerable code exists but only in a non-default/inactive environment), this maps to `code_not_reachable` (NOT `code_not_present`), because the code does exist in the container — it just has no execution path in normal operation. Skip to VEX Step 3 evidence check for `code_not_reachable`. + +**Note on MULTIPLE AFFECTED PACKAGES:** Some CVEs affect multiple packages (e.g., both +`runc` and `opencontainers/selinux`). The exploitability report may include an +"Additional Affected Packages" section or table listing packages beyond the primary. +When evaluating `code_not_present`, check ALL listed packages — if ANY affected package +is present and vulnerable, the code IS present even if the primary package is absent. + +**Note:** The exploitability report verifies package presence via the container filesystem (the authoritative source), including direct Go binary analysis for Go modules. + +### SDK POLICY CHECK (before VEX Step 3) + +If the container report indicates **Container Type: SDK/Development** (or "Both"): +- Can users write code that accesses the vulnerable API? +- Vulnerable function is in stdlib/installed library? → YES +- Users can import and call it? → YES +- No technical barrier prevents access? → YES + +If YES to all → **`vulnerable`** (status: `affected`) + +Still-valid exceptions for SDK containers: `false_positive`, `code_not_present`, +`requires_dependency`, `requires_environment`, `*_protected`. + +Generally NOT valid for SDK containers: `code_not_reachable` (users CAN reach it +— unless an architectural barrier exception applies, see below), +`requires_configuration` (users control their code's configuration). + +**EXCEPTION — ENVIRONMENT ISOLATED verdicts:** If the exploitability report verdict is +ENVIRONMENT ISOLATED (vulnerable code only in a non-default/inactive environment), +then `code_not_reachable` IS valid even for SDK containers — provided the report +confirms the non-default environment is not part of the documented SDK workflow. +Switching to an undocumented environment is a deliberate action beyond normal SDK +usage. However, if the report notes that the non-default environment IS part of the +expected SDK workflow, the SDK policy (`vulnerable`) takes precedence. + +**EXCEPTION — ARCHITECTURAL BARRIERS:** If the exploitability report identifies that +the vulnerable code is behind an architectural barrier (e.g., alternative implementation +backend selected by compiled native extension, compile-time feature flag, internal runtime +dispatch not exposed through user API), then `code_not_reachable` IS valid for SDK +containers. The test: would a user need to modify internal implementation details of a +transitive dependency (not write normal SDK code) to reach the vulnerable path? Check the +Conditions Matrix for "Unreachability type: Architectural barrier." + +### VEX Step 3: Is There an Execution Path to the Vulnerable Code? + +Read the **"Step 2: Code Reachability"** section. Key subsections: "Reachability Strategy +Used", "Findings" table, "Transitive Call Chain", "Library Linkage", "Entrypoint Analysis". + +Evidence for `code_not_reachable`: +- "No import of [module] found" +- "Function never called in codebase" +- "No call graph path from entry points" +- "No call chain identified" +- "not in default execution path" (in Findings table) + +If no execution path → **`code_not_reachable`** (status: `not_affected`) + +**Cross-check:** If the Conditions Matrix contradicts a NOT REACHABLE verdict (e.g., code +reachable = YES, in default execution path = YES, processes external data by default), the +verdict may reflect trust assumptions about endpoints rather than actual unreachability — +see the NOT REACHABLE override in the VERDICT-TO-VEX MAPPING section. + +### VEX Step 4: Can Attacker-Controlled Input Trigger the Vulnerability? + +Read the **"Step 3: Data Flow and Trigger Conditions"** section of the exploitability +report. Note: the report's Step 3 maps to VEX flowchart Step 4. + +If attacker CANNOT control input, determine WHY: +- Blocked by non-default configuration → consider **`requires_configuration`** (status: `not_affected`) + **OVERRIDE:** If the blocking toggle is a **documented, user-facing product feature** AND + the CVE advisory marks this version as **"affected"** (meaning the advisory did not treat the + default-off flag as sufficient mitigation), use **`vulnerable`** instead. See full + `requires_configuration` guidance in the VERDICT-TO-VEX MAPPING section. +- Required component missing → **`requires_dependency`** (status: `not_affected`) +- Environmental conditions unavailable → **`requires_environment`** (status: `not_affected`) + Note: covers system-level prerequisites (wrong OS, missing hardware, isolated namespace) — + not attacker-side capabilities or network positioning. + +### VEX Step 5: Are There Protections Preventing Exploitation? + +Read the **"Step 4: Security Controls and Impact"** section, specifically the +**"Controls Affecting Exploitability"** table. Categorize by protection TYPE: + +- Compiler-level (stack canaries, FORTIFY_SOURCE, CFI, PIE) → **`compiler_protected`** +- Runtime/OS (ASLR, DEP/NX, seccomp, AppArmor, SELinux, read-only fs) → **`runtime_protected`** +- Network/perimeter (firewall, WAF, network policy, not exposed) → **`perimeter_protected`** +- Other controls (input validation, sandboxing, authentication) → **`mitigating_control_protected`** + +### VEX Step 6: No Blocking Protections + +All conditions met — CVE correctly associated, vulnerable code present, execution path +exists, attacker can control input, no protections block exploitation. + +→ **`vulnerable`** (status: `affected`) + +--- + +## VEX STEP → REPORT SECTION MAPPING + +**CRITICAL: Step numbering is different between the VEX flowchart and the exploitability +report.** Use this mapping: + +| VEX Flowchart Step | Reads From Report Section | +|--------------------|-----------------------------| +| VEX Step 1 (CVE association correct?) | Report's **Step 1: Vulnerable Component Presence** | +| VEX Step 2 (Code present?) | Report's **Step 1** + **Conditions Matrix** | +| VEX Step 3 (Execution path exists?) | Report's **Step 2: Code Reachability** | +| VEX Step 4 (Attacker can trigger?) | Report's **Step 3: Data Flow and Trigger Conditions** | +| VEX Step 5 (Protections?) | Report's **Step 4: Security Controls and Impact** | +| VEX Step 6 (Vulnerable) | Report's **Conditions Matrix** (all YES) | + +--- + +## REPORT SECTION MAPPING + +These are the **actual section headers** in Agent 3's exploitability reports: + +| Report Section (exact header) | What to Extract | Informs VEX Step | +|-------------------------------|-----------------|------------------| +| **Summary** table (Verdict, Confidence, Risk Level) | Initial verdict | Guidance for all steps | +| **Input Summary → Container Context** table | Container Type | SDK policy check | +| **Input Summary → Vulnerability Context** table | Package, versions, function, CVSS | Steps 1, 2 | +| **Step 1: Vulnerable Component Presence** table | Package in filesystem, version, backport, active env | VEX Steps 1, 2 | +| **Step 2: Code Reachability** (Findings, Transitive Call Chain, Library Linkage, Entrypoint Analysis) | Imports, calls, call graphs, linkage | VEX Step 3 | +| **Step 3: Data Flow and Trigger Conditions** table | Input sources, attacker control, config, network | VEX Step 4 | +| **Step 4: Security Controls and Impact → Controls Affecting Exploitability** table | Active protections | VEX Step 5 | +| **Exploitability Assessment → Conditions Matrix** table | Systematic YES/NO checklist | All steps | +| **Exploitability Assessment → Container Type Consideration** | Container type impact | SDK policy check | +| **Exploitability Assessment → Verdict** + Justification text | Reasoning text | All steps | + +--- + +## EARLY-EXIT REPORT HANDLING + +Agent 3 produces two report formats: + +1. **Early-exit reports** (NOT APPLICABLE / ENVIRONMENT ISOLATED verdicts): + - Steps 2, 3, and 4 show "Skipped — [reason]" + - Conditions Matrix uses "N/A" and "Analysis stopped at Step 1" for rows beyond package verification + - Only Step 1 data is available + - Your VEX decision will be at VEX Step 1 or VEX Step 2 + +2. **Full analysis reports** (all other verdicts): + - All Steps 1-4 contain complete analysis + - Conditions Matrix uses YES/NO with explanations + +If the exploitability report's Steps 2-4 are "Skipped", do NOT try to extract +reachability, data flow, or security controls evidence from them. + +--- + +## VERDICT-TO-VEX MAPPING + +The exploitability report's verdict provides strong guidance. Verify independently +by reading the report sections: + +| Exploitability Verdict | VEX Status | Typical VEX Justification | What to Check | +|----------------------|------------|--------------------------|---------------| +| NOT APPLICABLE (version patched) | `not_affected` | `false_positive` | Step 1: "Version is Vulnerable: No" or "Distro Backport Check: Patched" | +| NOT APPLICABLE (package absent) | `not_affected` | `code_not_present` | Step 1: "Package in Filesystem: No" | +| ENVIRONMENT ISOLATED | `not_affected` | `code_not_reachable` | Step 1: "In Active Environment: No" — the vulnerable code exists in a non-default environment but has no execution path in normal operation | +| NOT REACHABLE | `not_affected` | `code_not_reachable` | Step 2: Findings show "No" in "In Default Execution Path" | +| MITIGATED | `not_affected` | Depends on mitigation type | Steps 3-4: read WHY to pick sub-category | +| CONDITIONALLY EXPLOITABLE (SDK container) | `affected` | `vulnerable` | Container Type Consideration: SDK/Development (check for architectural barrier exception — if Unreachability type is "Architectural barrier", use `code_not_reachable` instead) | +| CONDITIONALLY EXPLOITABLE (non-SDK container) | Evaluate per flowchart | `vulnerable` or `requires_configuration` | Walk VEX Steps 3-6; see `requires_configuration` guidance below | +| NOT REACHABLE (SDK, architectural barrier) | `not_affected` | `code_not_reachable` | Conditions Matrix: "Unreachability type: Architectural barrier" — vulnerable code behind internal implementation barrier not accessible via normal SDK APIs | +| EXPLOITABLE | `affected` | `vulnerable` | Conditions Matrix: all YES | + +**CONDITIONALLY EXPLOITABLE for SDK containers → `vulnerable`:** +If the container is SDK/Development, the VEX justification MUST be `vulnerable`. Do NOT use +`requires_configuration`. (See SDK POLICY CHECK section for limited exceptions.) + +**Understanding `requires_configuration`:** + +`requires_configuration` means the PRODUCT ITSELF must be put in a non-default configuration to +expose the vulnerability (e.g., enabling debug mode, disabling a security hardening toggle, +activating an internal/undocumented flag). + +`requires_configuration` is WRONG in these situations: +- The product is already in its default deployed state and the only barrier is ATTACKER positioning + (MITM, supply chain access, etc.). Attacker capabilities are CVSS metrics, not VEX criteria. +- The configuration is a documented, supported product feature that users routinely enable (e.g., + enabling an API mode, activating a monitoring feature). If the vulnerability advisory considers + the version "affected" despite the toggle, use `vulnerable`. +- The product makes outbound network requests by default (e.g., model downloads at startup). The + fact that those requests go to trusted endpoints does not make the vulnerability require + non-default configuration. + +**Rule of thumb:** If a sufficiently positioned attacker could trigger the vulnerability without +changing any product setting, the justification is `vulnerable`, not `requires_configuration`. + +**NOT REACHABLE** can occasionally map to `requires_configuration` or `requires_dependency` +instead of `code_not_reachable` — check the Verdict Justification text. + +**NOT REACHABLE override:** When the Conditions Matrix shows the vulnerable code runs and processes +external data in the default configuration, but the verdict is NOT REACHABLE based solely on trust +assumptions about remote endpoints (e.g., "only connects to trusted servers"), the trustworthiness +of endpoints is an operational assumption — not an exploitability finding. Override to `vulnerable`. + +--- + +## KEYWORD PATTERNS + +Scan the exploitability report for these patterns to guide categorization: + +| Category | Keywords/Phrases | +|----------|-----------------| +| `false_positive` | "Version is Vulnerable: No", "Distro Backport Check: Patched", "backported fix", "not vulnerable despite", "scanner error", "misidentified", "CPE mismatch" | +| `code_not_present` | "Package in Filesystem: No" (AND no additional affected packages found), "compiled without", "stripped", "not included" | +| `code_not_reachable` | "not imported", "never called", "no execution path", "unused", "dead code", "No call chain identified", "not in default execution path", "In Default Execution Path: No", "Vulnerable code reachable: NO", "ENVIRONMENT ISOLATED", "In Active Environment: No", "only in base env", "non-default environment", "architectural barrier", "alternative backend", "different implementation backend", "Unreachability type: Architectural barrier" | +| `requires_configuration` | "disabled by default", "requires config", "feature not enabled", "debug mode", "non-default configuration" | +| `requires_dependency` | "requires [X] which is missing", "gadget not present", "dependency not installed", "native extension not present" | +| `requires_environment` | "requires X11", "Windows only", "hardware not present", "namespace isolation", "wrong OS" | +| `compiler_protected` | "stack canary", "FORTIFY_SOURCE", "CFI", "PIE enabled" | +| `runtime_protected` | "ASLR", "DEP", "NX bit", "seccomp", "AppArmor", "SELinux", "read-only fs" | +| `perimeter_protected` | "firewall", "WAF", "network policy", "not exposed", "internal only" | +| `mitigating_control_protected` | "input validation", "sanitized", "sandboxed", "authentication required", "rate limiting" | +| `vulnerable` | "exploitable", "all conditions met", "attacker can", "no mitigations", "conditionally exploitable", "SDK container" | + +--- + +## PRECEDENCE RULES + +When multiple categories could apply, use this precedence (highest first): + +1. **`false_positive`** — If the CVE association is wrong, nothing else matters +2. **`code_not_present`** — If the code doesn't exist, reachability is moot +3. **`code_not_reachable`** — If there's no path, control/protection is irrelevant +4. **`requires_*` categories** — These explain WHY it's not reachable/exploitable +5. **`*_protected` categories** — These block exploitation of reachable code +6. **`vulnerable`** — Only if nothing above applies + +--- + +## CRITICAL INSTRUCTIONS + +1. **FOLLOW THE DECISION FLOWCHART STRICTLY.** Walk through VEX Steps 1-6 in order. Stop + at the FIRST matching category. Do not skip ahead. **Call reason() for each step** to + record your analysis and conclusion before moving on. + +2. **SDK containers:** CONDITIONALLY EXPLOITABLE or NOT REACHABLE → `vulnerable`, + unless an exception applies (see SDK POLICY CHECK section for the full list). + +3. **HANDLE BOTH REPORT FORMATS.** Agent 3 produces two formats: + - Early-exit reports (NOT APPLICABLE / ENVIRONMENT ISOLATED): Steps 2-4 say "Skipped". + - Full reports (all other verdicts): All Steps 1-4 have analysis data. + If the exploitability report's Steps 2-4 are "Skipped", do NOT try to extract + reachability, data flow, or security controls evidence from them. + +4. **SDK POLICY IS MANDATORY.** If the container is SDK/Development, apply the SDK + POLICY CHECK before VEX Step 3. See that section for the full rule and exceptions. + +5. **DISTINGUISH false_positive FROM code_not_present FOR NOT APPLICABLE VERDICTS.** + Read the report's "Step 1: Vulnerable Component Presence" table: + - If "Version is Vulnerable: No" or "Distro Backport Check: Patched" → `false_positive` + - If "Package in Filesystem: No" → `code_not_present` + - If the CVE association is wrong (wrong package, wrong ecosystem) → `false_positive` + +6. **DISTINGUISH MITIGATED SUB-CATEGORIES.** When the verdict is MITIGATED, read the + report's "Step 3: Data Flow and Trigger Conditions" table and "Step 4: Security Controls + and Impact → Controls Affecting Exploitability" table to determine the specific mitigation + type. Do not default to `mitigating_control_protected` — check if it's configuration, + dependency, environment, or a specific protection type first. + +7. **REASONING MUST REFERENCE REPORT EVIDENCE.** The reasoning field should cite specific + findings from the report — package names, versions, function names, and specific controls. + Do not make generic statements. + +8. **OUTPUT THE REPORT.** After reaching a final determination via the reason() steps, + produce the full VEX summary report (markdown) as your final response. Do NOT modify + the exploitability report — it is a read-only input artifact. + +9. **HANDLE EDGE CASES.** If the report is malformed, missing sections, or has an unclear + verdict, assign the best-fit category with reduced confidence notation in the reasoning. + Do not fail — always produce a classification. +""" + + +def build_user_message( + exploitability_report_path: str, + exploitability_report_content: str, + container_report_path: str, + container_report_content: str, + cve_id: str, + container_name: str, +) -> str: + """Construct the user message for the VEX Categorizer LLM. + + Both report contents are provided by the orchestrator — the LLM + receives them inline and never needs to call read_file. + """ + parts: list[str] = [] + + parts.append( + f"Categorize **{cve_id}** in container **{container_name}** into a VEX " + f"justification status." + ) + + parts.append( + "## Input Reports\n\n" + "Both reports are provided below. You have all the information you need.\n\n" + f"\n" + f"{exploitability_report_content}\n" + "\n\n" + f"\n" + f"{container_report_content}\n" + "" + ) + + parts.append( + "## Analysis Order\n\n" + "1. **Walk through the decision flowchart using reason()** — call " + "reason(step, analysis, conclusion) for EACH VEX step (1-6) until you " + "reach a MATCH. Cite specific report evidence in each call. This is " + "mandatory — do not skip directly to the final report.\n" + "2. **Apply SDK policy** if container is SDK/Development (record via " + "reason() as well)\n" + "3. **Produce the VEX summary report** as your final response" + ) + + parts.append( + "## VEX Summary Report Template\n\n" + "Produce the summary report as your final response in this exact format:\n\n" + "```\n" + f"# VEX Summary: {cve_id} × {container_name}\n\n" + "## VEX Status\n\n" + "| Field | Value |\n" + "|-------|-------|\n" + "| **Status** | not_affected / affected |\n" + "| **Justification** | [one of the 11 categories] |\n" + "| **Reasoning** | [1-3 sentence explanation referencing report evidence] |\n\n" + "## Assessment Overview\n\n" + "| Field | Value |\n" + "|-------|-------|\n" + "| **CVE ID** | [CVE-YYYY-NNNNN] |\n" + "| **Container** | [container-name] |\n" + "| **Container Type** | [SDK/Development / Runtime/Microservice / Infrastructure/Platform] |\n" + "| **Vulnerable Package** | [package name (include additional affected packages if any)] |\n" + "| **Installed Version** | [version] |\n" + "| **Exploitability Verdict** | [verdict from Agent 3] |\n" + "| **Confidence** | [High / Medium / Low] |\n" + "| **Risk Level** | [Critical / High / Medium / Low / Informational] |\n\n" + "## Decision Rationale\n\n" + "[2-4 sentences explaining which VEX flowchart step determined the categorization, " + "what specific evidence from the exploitability report supports the decision, and " + "any SDK policy considerations if applicable.]\n\n" + "## References\n\n" + f"- Exploitability Report: {exploitability_report_path}\n" + f"- Container Report: {container_report_path}\n" + "```" + ) + + parts.append( + "**OUTPUT REQUIREMENTS:**\n" + "- Produce the full VEX summary report as your final response.\n" + "- Do NOT modify the exploitability report. It is a read-only input.\n" + "- You MUST call reason() for each VEX step BEFORE producing the report." + ) + + return "\n\n".join(parts) diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index 5cb8ea1ef..206435a76 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -1,24 +1,7 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +"""NAT entry point that registers the CVE pipeline workflow and components.""" -import logging -from datetime import datetime -from io import TextIOWrapper -from typing import Literal +# flake8: noqa: E402 -# Apply NAT logging patch early to truncate long error messages from vuln_analysis.utils.nat_logging_patch import apply_nat_logging_patch apply_nat_logging_patch() @@ -31,246 +14,74 @@ from nat.data_models.function import FunctionBaseConfig from pydantic import Field -from vuln_analysis.data_models.input import AgentMorpheusEngineInput -from vuln_analysis.data_models.input import AgentMorpheusInput -from vuln_analysis.data_models.output import AgentMorpheusOutput -from vuln_analysis.data_models.state import AgentMorpheusEngineState -# pylint: disable=unused-import -from vuln_analysis.eval.evaluators import accuracy -from vuln_analysis.eval.evaluators import consistency -from vuln_analysis.functions import cve_agent -from vuln_analysis.functions import cve_check_vuln_deps -from vuln_analysis.functions import cve_checklist -from vuln_analysis.functions import cve_fetch_intel -from vuln_analysis.functions import cve_file_output -from vuln_analysis.functions import cve_generate_vdbs -from vuln_analysis.functions import cve_http_output -from vuln_analysis.functions import cve_justify -from vuln_analysis.functions import cve_process_sbom -from vuln_analysis.functions import cve_summarize -from vuln_analysis.test_time_compute import execute_then_select_function -from vuln_analysis.test_time_compute import majority_voting_selector -from vuln_analysis.tools import lexical_full_search -from vuln_analysis.tools import local_vdb -from vuln_analysis.tools import serp -# pylint: enable=unused-import -from vuln_analysis.utils.concurrency import ctx_parent_max_rate -from vuln_analysis.utils.llm_engine_utils import postprocess_engine_output -from vuln_analysis.utils.llm_engine_utils import preprocess_engine_input - -logger = logging.getLogger(__name__) - - -class CVEAgentWorkflowConfig(FunctionBaseConfig, name="cve_agent"): - """ - Defines the workflow function for determining the impact of a documented CVEs on a specific project or container. - """ - cve_generate_vdbs_name: FunctionRef = Field(description="Function name to generate vector databases") - cve_fetch_intel_name: FunctionRef = Field(description="Function name to fetch intel") - cve_process_sbom_name: FunctionRef = Field(description="Function name to process SBOMs") - cve_check_vuln_deps_name: FunctionRef = Field(description="Function name to check vulnerable dependencies") - cve_checklist_name: FunctionRef = Field(description="Function name to generate checklist") - cve_agent_executor_name: FunctionRef = Field(description="Function name to run CVE agent on checklist") - cve_summarize_name: FunctionRef = Field(description="Function name to generate summary") - cve_justify_name: FunctionRef = Field(description="Function to generate justifications for each CVE") - cve_output_config_name: FunctionRef | None = Field(default=None, - description="Function to output workflow results " - "(e.g. cve_file_output, cve_http_output). " - " If None, only prints to console") - description: str = Field(default="Vulnerability analysis for container security workflow", - description="Workflow function description") - missing_source_action: Literal["error", "skip_agent", "continue_with_warning"] = Field( - default="continue_with_warning", - description="Action when source analysis is unavailable in the agent due to: " - "missing source_info, VDB generation failures, or inaccessible repositories\n" - " - error: Fail pipeline with validation error\n" - " - skip_agent: Collect intel and check dependencies, but skip agent\n" - " - continue_with_warning: Run full pipeline with warning log (degraded analysis quality)") +from vuln_analysis.data_models.input import PipelineInput +from vuln_analysis.eval.evaluators import accuracy # noqa: F401 +from vuln_analysis.eval.evaluators import cve_report_judge # noqa: F401 +# Import to trigger @register_function / @register_evaluator decorators +from vuln_analysis.functions import container_analyzer # noqa: F401 +from vuln_analysis.functions import cve_researcher # noqa: F401 +from vuln_analysis.functions import exploitability_analyzer # noqa: F401 +from vuln_analysis.functions import vex_categorizer # noqa: F401 +from vuln_analysis.functions import custom_vex_categorizer # noqa: F401 +from vuln_analysis.pipeline.context import PipelineFunctions +from vuln_analysis.pipeline.converters import pipeline_result_to_str +from vuln_analysis.pipeline.converters import str_to_pipeline_input +from vuln_analysis.pipeline.converters import textio_to_pipeline_input +from vuln_analysis.pipeline.helpers import coerce_result as _coerce_result +from vuln_analysis.pipeline.helpers import resolve_cve_id as _resolve_cve_id +from vuln_analysis.pipeline.helpers import resolve_pipeline_result_ids as _resolve_pipeline_result_ids +from vuln_analysis.pipeline.helpers import write_vex_json as _write_vex_json +from vuln_analysis.pipeline.workflow import PipelineWorkflow +from vuln_analysis.tools import binary_tools # noqa: F401 +from vuln_analysis.tools import filesystem_tools # noqa: F401 +from vuln_analysis.tools import osv_tools # noqa: F401 +from vuln_analysis.tools import package_tools # noqa: F401 +from vuln_analysis.tools import web_tools # noqa: F401 + + +class CVEPipelineWorkflowConfig(FunctionBaseConfig, name="cve_pipeline"): + cve_researcher_name: FunctionRef = Field(description="CVE researcher function") + container_analyzer_name: FunctionRef = Field(description="Container analyzer function") + exploitability_analyzer_name: FunctionRef = Field(description="Exploitability analyzer function") + vex_categorizer_name: FunctionRef = Field(description="VEX categorizer function") + stage_timeout: int = Field(default=600) + stage_max_retries: int = Field(default=1) + max_concurrency: int | None = Field( + default=None, + description="Max parallel agent tasks per stage (null or 0 = unlimited)", + ) llm_max_rate: int | None = Field( default=None, - description="Controls the maximum LLM rate limit (requests per second) globally for all workflow functions." - "Individual function llm_max_rate settings override this value. None means no rate limiting.") - - -@register_function(config_type=CVEAgentWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_agent_workflow(config: CVEAgentWorkflowConfig, builder: Builder): - - from langgraph.graph import END - from langgraph.graph import START - from langgraph.graph import StateGraph - - # Access functions that will be used in this workflow - cve_generate_vdbs_fn = await builder.get_function(name=config.cve_generate_vdbs_name) - cve_fetch_intel_fn = await builder.get_function(name=config.cve_fetch_intel_name) - cve_process_sbom_fn = await builder.get_function(name=config.cve_process_sbom_name) - cve_check_vuln_deps_fn = await builder.get_function(name=config.cve_check_vuln_deps_name) - cve_checklist_fn = await builder.get_function(name=config.cve_checklist_name) - cve_agent_executor_fn = await builder.get_function(name=config.cve_agent_executor_name) - cve_summary_fn = await builder.get_function(name=config.cve_summarize_name) - cve_justify_fn = await builder.get_function(name=config.cve_justify_name) - cve_output_fn = await builder.get_function( - name=config.cve_output_config_name) if config.cve_output_config_name else None - - # Define langgraph node functions - async def validate_input_node(state: AgentMorpheusInput) -> AgentMorpheusInput: - """Validate input based on workflow configuration""" - if not state.image.source_info and config.missing_source_action == 'error': - raise ValueError("source_info is required but missing or empty. " - "Please provide source code repository information or change " - "missing_source_action configuration to 'skip_agent' or 'continue_with_warning'.") - return state - - async def add_start_time_node(state: AgentMorpheusInput) -> AgentMorpheusInput: - """Adds the start time to the input""" - state.scan.started_at = datetime.now().isoformat() - return state - - async def generate_vdbs_node(state: AgentMorpheusInput) -> AgentMorpheusEngineInput: - """Generates VDBs based on CVE input""" - - return await cve_generate_vdbs_fn.ainvoke(state.model_dump()) - - async def fetch_intel_node(state: AgentMorpheusEngineInput) -> AgentMorpheusEngineInput: - """Fetch intel for CVE input""" - - return await cve_fetch_intel_fn.ainvoke(state.model_dump()) - - async def process_sbom_node(state: AgentMorpheusEngineInput) -> AgentMorpheusEngineInput: - """Process SBOMs for CVE input""" - - return await cve_process_sbom_fn.ainvoke(state.model_dump()) - - async def check_vuln_deps_node(state: AgentMorpheusEngineInput) -> AgentMorpheusEngineInput: - """Check for vulnerable dependencies""" - - return await cve_check_vuln_deps_fn.ainvoke(state.model_dump()) - - async def checklist_node(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - """Generates a checklist based on CVE input""" - - return await cve_checklist_fn.ainvoke(state.model_dump()) - - async def agent_executor_node(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - """Executes the checklist using an agent with ReAct prompt.""" - - return await cve_agent_executor_fn.ainvoke(state.model_dump()) - - async def summarize_node(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - """Summarizes the results of the execution""" - - return await cve_summary_fn.ainvoke(state.model_dump()) - - async def justify_node(state: AgentMorpheusEngineState) -> AgentMorpheusEngineState: - """Generates a justification for the final summary""" - - return await cve_justify_fn.ainvoke(state.model_dump()) - - async def add_completed_time_node(state: AgentMorpheusOutput) -> AgentMorpheusOutput: - """Adds the completed time to the output""" - state.input.scan.completed_at = datetime.now().isoformat() - return state - - async def output_results_node(state: AgentMorpheusOutput) -> AgentMorpheusOutput: - """Outputs results using configured output function""" - - return await cve_output_fn.ainvoke(state.model_dump()) if cve_output_fn else state - - # define langgraph - - # build llm engine subgraph - subgraph_builder = StateGraph(AgentMorpheusEngineState) - subgraph_builder.add_node("checklist", checklist_node) - subgraph_builder.add_node("agent_executor", agent_executor_node) - subgraph_builder.add_node("summarize", summarize_node) - subgraph_builder.add_node("justify", justify_node) - - subgraph_builder.add_edge(START, "checklist") - subgraph_builder.add_edge("checklist", "agent_executor") - subgraph_builder.add_edge("agent_executor", "summarize") - subgraph_builder.add_edge("summarize", "justify") - subgraph = subgraph_builder.compile() - - async def call_llm_engine_subgraph_node(state: AgentMorpheusEngineInput): - - subgraph_input = preprocess_engine_input(state, config.missing_source_action) - results = await subgraph.ainvoke(subgraph_input) - subgraph_output = AgentMorpheusEngineState(**results) - output = postprocess_engine_output(state, subgraph_output) - return output - - # build parent graph - graph_builder = StateGraph(AgentMorpheusOutput, input_schema=AgentMorpheusInput) - graph_builder.add_node("validate_input", validate_input_node) - graph_builder.add_node("add_start_time", add_start_time_node) - graph_builder.add_node("generate_vdbs", generate_vdbs_node) - graph_builder.add_node("fetch_intel", fetch_intel_node) - graph_builder.add_node("process_sbom", process_sbom_node) - graph_builder.add_node("check_vuln_deps", check_vuln_deps_node) - graph_builder.add_node("llm_engine", call_llm_engine_subgraph_node) - graph_builder.add_node("add_completed_time", add_completed_time_node) - graph_builder.add_node("output_results", output_results_node) - - graph_builder.add_edge(START, "validate_input") - graph_builder.add_edge("validate_input", "add_start_time") - graph_builder.add_edge("add_start_time", "generate_vdbs") - graph_builder.add_edge("generate_vdbs", "fetch_intel") - graph_builder.add_edge("fetch_intel", "process_sbom") - graph_builder.add_edge("process_sbom", "check_vuln_deps") - graph_builder.add_edge("check_vuln_deps", "llm_engine") - graph_builder.add_edge("llm_engine", "add_completed_time") - graph_builder.add_edge("add_completed_time", "output_results") - graph_builder.add_edge("output_results", END) - graph = graph_builder.compile() - - def convert_str_to_agent_morpheus_input(input_str: str) -> AgentMorpheusInput: - logger.debug("Converting input to AgentMorpheusInput: %s", input_str) - try: - return AgentMorpheusInput.model_validate_json(input_str) - except Exception as e: - logger.error("Failed to convert input to AgentMorpheusInput: %s. Your input needs to be a json string.", e) - raise e - - def convert_textio_to_agent_morpheus_input(input_file: TextIOWrapper) -> AgentMorpheusInput: - logger.debug("Converting input to AgentMorpheusInput: %s", input_file) - try: - data = input_file.read() - return AgentMorpheusInput.model_validate_json(data) - except Exception as e: - logger.error( - "Failed to convert input to AgentMorpheusInput: %s. Your input needs to be a TextIOWrapper object.", e) - raise e - - def convert_agent_morpheus_output_to_str(output: AgentMorpheusOutput) -> str: - logger.debug("Converting AgentMorpheusOutput to str: %s", output) - try: - return output.model_dump_json() - except Exception as e: - logger.error("Failed to convert output to str: %s. Your input needs to be an AgentMorpheusOutput object.", - e) - raise e - - async def _response_fn(input_message: AgentMorpheusInput) -> AgentMorpheusOutput: - # Set parent workflow's llm_max_rate in context so child functions can access it - # This is important when cve_agent is used as a function inside another workflow (e.g., TTC) - token = ctx_parent_max_rate.set(config.llm_max_rate) - try: - results = await graph.ainvoke(input_message) - graph_output = AgentMorpheusOutput(**results) - return graph_output - finally: - # Reset context variable - ctx_parent_max_rate.reset(token) - - try: - yield FunctionInfo.from_fn(_response_fn, - description=config.description, - input_schema=AgentMorpheusInput, - converters=[ - convert_str_to_agent_morpheus_input, - convert_textio_to_agent_morpheus_input, - convert_agent_morpheus_output_to_str - ]) - except GeneratorExit: - logger.info("Workflow exited early!") - finally: - logger.info("Cleaning up cve-agent workflow.") + description="Workflow-level LLM rate limit (requests/second). Per-function settings override this.", + ) + output_dir: str = Field(default="outputs") + description: str = Field(default="CVE analysis pipeline") + + +@register_function(config_type=CVEPipelineWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def cve_pipeline_workflow(config: CVEPipelineWorkflowConfig, builder: Builder): + """Register the compiled CVE pipeline workflow with NAT.""" + functions = PipelineFunctions( + cve_researcher=await builder.get_function(name=config.cve_researcher_name), + container_analyzer=await builder.get_function(name=config.container_analyzer_name), + exploitability_analyzer=await builder.get_function(name=config.exploitability_analyzer_name), + vex_categorizer=await builder.get_function(name=config.vex_categorizer_name), + ) + workflow = PipelineWorkflow(config, functions) + + yield FunctionInfo.from_fn( + workflow.response, + input_schema=PipelineInput, + description=config.description, + converters=[str_to_pipeline_input, textio_to_pipeline_input, pipeline_result_to_str], + ) + + +__all__ = [ + "CVEPipelineWorkflowConfig", + "cve_pipeline_workflow", + "_coerce_result", + "_resolve_cve_id", + "_resolve_pipeline_result_ids", + "_write_vex_json", +] diff --git a/src/vuln_analysis/test_time_compute/execute_then_select_function.py b/src/vuln_analysis/test_time_compute/execute_then_select_function.py deleted file mode 100644 index b2e61442e..000000000 --- a/src/vuln_analysis/test_time_compute/execute_then_select_function.py +++ /dev/null @@ -1,241 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.function import Function -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.component_ref import FunctionRef -from nat.data_models.component_ref import TTCStrategyRef -from nat.data_models.function import FunctionBaseConfig -from nat.experimental.test_time_compute.models.stage_enums import PipelineTypeEnum -from nat.experimental.test_time_compute.models.stage_enums import StageTypeEnum -from nat.experimental.test_time_compute.models.ttc_item import TTCItem -from pydantic import Field -from pydantic import field_validator -from pydantic import model_validator - -from vuln_analysis.data_models.output import AgentMorpheusOutput - -logger = logging.getLogger(__name__) - - -class ExecuteThenSelectFunctionConfig(FunctionBaseConfig, name="execute_then_select_function"): - selector: TTCStrategyRef = Field(description="Strategy to select the best output of the function") - augmented_fn: FunctionRef = Field(description="Function that will be executed") - output_fn: FunctionRef | None = Field(default=None, - description="Function to output workflow results " - "(e.g. cve_file_output, cve_http_output). " - " If None, only prints to console") - num_executions: int = Field(3, ge=1, description="Number of times to execute the function") - max_concurrency: int | None = Field(ge=1, - default=None, - description="The maximum number of concurrent invocations of augmented_fn. " - "None means no rate limiting.") - early_stop_threshold: int | bool = Field( - False, - description= - """The number of consecutive same outputs to trigger early stopping. The early stop threshold must be strictly - less than the number of executions. To disable early stopping, set early_stop_threshold to false.""") - - @field_validator("early_stop_threshold") - @classmethod - def validate_early_stop_threshold(cls, v): - """Validate that early_stop_threshold is either a positive integer or False.""" - if (isinstance(v, bool) and v is True) or (not isinstance(v, bool) and v < 1): - raise ValueError("early_stop_threshold must be a positive integer or False.") - return v - - @model_validator(mode="after") - def validate_early_stop_threshold_lt_num_executions(self): - """Validate that early_stop_threshold is strictly less than num_executions.""" - if not isinstance(self.early_stop_threshold, bool): - if self.early_stop_threshold >= self.num_executions: - raise ValueError(f"early_stop_threshold ({self.early_stop_threshold}) must be strictly less than " - f"num_executions ({self.num_executions})") - return self - - -@register_function(config_type=ExecuteThenSelectFunctionConfig) -async def execute_then_select_function(config: ExecuteThenSelectFunctionConfig, builder: Builder): - import asyncio - import warnings - - executable_fn: Function = await builder.get_function(name=config.augmented_fn) - output_fn: Function | None = await builder.get_function(name=config.output_fn) if config.output_fn else None - - selector = await builder.get_ttc_strategy(strategy_name=config.selector, - pipeline_type=PipelineTypeEnum.AGENT_EXECUTION, - stage_type=StageTypeEnum.SELECTION) - - if executable_fn.has_streaming_output: - warnings.warn("Streaming output is not supported for this function. " - "The function will be executed in non-streaming mode.") - - def _get_justification_statuses(ttc_items: list[TTCItem], vuln_id: str) -> list[str]: - """Get the justification statuses for a given vuln_id from the TTC items.""" - - # Build list of justification statuses from all TTC executions for the given vuln_id - justification_statuses = [] - for item in ttc_items: - assert isinstance(item.output, AgentMorpheusOutput), \ - "Item output must be of type AgentMorpheusOutput" - - # Look up the justification status for the given vuln_id - for output in item.output.output: - if output.vuln_id == vuln_id: - justification_statuses.append(output.justification.status) - break - - return justification_statuses - - def _is_early_stopped(affected_statuses: list[str], early_stop_threshold) -> bool: - """Check if early stopping should occur based on the affected statuses.""" - - # Get selection mode from the selector config - selection_mode = getattr(selector.config, "selection_mode", None) - - # For decisive mode, drop UNKNOWNs before checking for consecutive same outputs - if selection_mode == "decisive": - statuses = [status for status in affected_statuses if status != "UNKNOWN"] - else: - statuses = affected_statuses - - # Check if we have early_stop_threshold+ consecutive same outputs - return len(statuses) >= early_stop_threshold and len(set(statuses[-early_stop_threshold:])) == 1 - - async def _wrapped_ainvoke(input_msg: executable_fn.input_type, semaphore) -> executable_fn.single_output_type: - """Wrapper around the executable function's ainvoke to handle concurrency limits.""" - if semaphore: - async with semaphore: - return await executable_fn.ainvoke(input_msg) - else: - return await executable_fn.ainvoke(input_msg) - - async def _execute_parallel(input_msg: executable_fn.input_type, num_executions: int) -> list[TTCItem]: - """Execute a specified number of runs in parallel. - - Args: - input_msg: The input message to pass to the function - num_executions: Number of parallel executions to perform - - Returns: - List of TTCItems containing execution results - """ - semaphore = asyncio.Semaphore(config.max_concurrency) if config.max_concurrency else None - - tasks = [_wrapped_ainvoke(input_msg, semaphore) for _ in range(num_executions)] - results = await asyncio.gather(*tasks) - return [TTCItem(input=input_msg, output=res) for res in results] - - async def _execute_with_early_stopping(input_msg: executable_fn.input_type, - early_stop_threshold: int, - num_executions: int) -> list[TTCItem]: - """Execute function with early stopping after 3 consecutive same outputs. - - Args: - input_msg: The input message to pass to the function - num_executions: Number of total executions to perform - - Returns: - List of TTCItems containing execution results - """ - - # Run the first early_stop_threshold executions in parallel - ttc_items = await _execute_parallel(input_msg, early_stop_threshold) - - # Run the remaining executions sequentially for vuln_ids that have not been stopped - current_input = input_msg.model_copy(deep=True) - for _ in range(early_stop_threshold, num_executions): - - # Check if early stopping should occur for each vuln_id - vuln_ids = [vuln.vuln_id for vuln in current_input.scan.vulns] - for vuln_id in vuln_ids: - justification_statuses = _get_justification_statuses(ttc_items, vuln_id) - - if _is_early_stopped(justification_statuses, early_stop_threshold): - # Update metadata for the vuln_id - ttc_items[-1].metadata = ttc_items[-1].metadata or {} - ttc_items[-1].metadata[vuln_id] = { - "early_stopping": True, - "early_stopping_execution_num": len(ttc_items), - "early_stopping_value": justification_statuses[-1] - } - - # Remove the vuln_id from the current input - updated_vulns = [ - vuln_info for vuln_info in current_input.scan.vulns if vuln_info.vuln_id != vuln_id - ] - current_input.scan.vulns = updated_vulns - logger.info("Stopping early for vuln_id %s after %d executions with affected status %s", - vuln_id, - len(ttc_items), - justification_statuses[-1]) - - # Break if there are no more vuln_ids to execute - if not current_input.scan.vulns: - logger.info("All vuln_ids have been early stopped after %d executions. Exiting.", len(ttc_items)) - break - - result = await executable_fn.ainvoke(current_input) - ttc_items.append(TTCItem(input=current_input.model_copy(deep=True), output=result)) - - return ttc_items - - async def execute_fn(input_msg: executable_fn.input_type) -> executable_fn.single_output_type: - - if config.max_concurrency: - logger.info("Executing function %d times with max concurrency of %d", - config.num_executions, - config.max_concurrency) - else: - logger.info("Executing function %d times with no concurrency limit", config.num_executions) - - if config.early_stop_threshold: - logger.info("Early stopping is enabled with early_stop_threshold=%d", config.early_stop_threshold) - ttc_items = await _execute_with_early_stopping(input_msg, - config.early_stop_threshold, - config.num_executions) - else: - ttc_items = await _execute_parallel(input_msg, config.num_executions) - - logger.info("Beginning selection using %s", config.selector) - selected_items = await selector.ainvoke(items=ttc_items, original_prompt=input_msg) - - # Validate the selected items list - if not isinstance(selected_items, list): - raise ValueError("Selected items must be a list.") - if len(selected_items) < 1: - raise ValueError("No items were selected. Please check your selector strategy.") - if not isinstance(selected_items[0], TTCItem): - raise ValueError("Selected items must be a list of TTCItem objects.") - - if len(selected_items) > 1: - logger.warning("Multiple items were selected. Returning the first item.") - - if output_fn is not None: - # Pass the selected output through the output function - await output_fn.ainvoke(selected_items[0].output) - - return selected_items[0].output - - yield FunctionInfo.from_fn( - fn=execute_fn, - description=("This function executes a given function multiple times" - "and selects the best output based on the specified selection strategy."), - converters=executable_fn._converter_list, - ) diff --git a/src/vuln_analysis/test_time_compute/majority_voting_selector.py b/src/vuln_analysis/test_time_compute/majority_voting_selector.py deleted file mode 100644 index 8250eed1f..000000000 --- a/src/vuln_analysis/test_time_compute/majority_voting_selector.py +++ /dev/null @@ -1,144 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing - -from nat.builder.builder import Builder -from nat.cli.register_workflow import register_ttc_strategy -from nat.data_models.ttc_strategy import TTCStrategyBaseConfig -from nat.experimental.test_time_compute.models.stage_enums import PipelineTypeEnum -from nat.experimental.test_time_compute.models.stage_enums import StageTypeEnum -from nat.experimental.test_time_compute.models.strategy_base import StrategyBase -from nat.experimental.test_time_compute.models.ttc_item import TTCItem -from pydantic import Field - -from vuln_analysis.data_models.output import AgentMorpheusEngineOutput -from vuln_analysis.data_models.output import AgentMorpheusOutput - - -class MajorityVotingSelectionConfig(TTCStrategyBaseConfig, name="majority_voting_selection"): - """ - Configuration for Majority Voting Selection - """ - selection_mode: typing.Literal["simple", "decisive"] = Field(description="The majority voting selection mode.", - default="decisive") - - -class MajorityVotingSelector(StrategyBase): - - async def build_components(self, builder: Builder) -> None: - pass - - def supported_pipeline_types(self) -> list[PipelineTypeEnum]: - return [PipelineTypeEnum.AGENT_EXECUTION] - - def stage_type(self) -> StageTypeEnum: - return StageTypeEnum.SELECTION - - async def ainvoke(self, - items: list[TTCItem], - original_prompt: str | None = None, - agent_context: str | None = None, - **kwargs) -> list[TTCItem]: - if len(items) == 0: - return [] - - results = [item.output for item in items] - - # Get value counts of the justification status for each vuln_id - assert isinstance(results[0], AgentMorpheusOutput), "Results must be of type AgentMorpheusOutput" - value_counts = {output.vuln_id: {} for output in results[0].output} - for result in results: - assert isinstance(result, AgentMorpheusOutput), "Result must be of type AgentMorpheusOutput" - for output in result.output: - vuln_id = output.vuln_id - value = output.justification.status - value_counts[vuln_id][value] = value_counts.get(vuln_id, {}).get(value, 0) + 1 - - # Find the most common value for each vuln_id - majority_values = {} - for vuln_id, counts in value_counts.items(): - - assert isinstance(self.config, MajorityVotingSelectionConfig), \ - "Config must be of type MajorityVotingSelectionConfig" - - if self.config.selection_mode == "simple": - max_value = max(counts.values()) - candidates = [candidate for candidate, votes in counts.items() if votes == max_value] - # Break ties as 'UNKNOWN' - if len(candidates) == 1: - majority_values[vuln_id] = candidates[0] - else: - majority_values[vuln_id] = 'UNKNOWN' - - elif self.config.selection_mode == "decisive": - # Prefer 'TRUE' or 'FALSE' if there is a clear majority - if counts.get("TRUE", 0) > counts.get("FALSE", 0): - majority_values[vuln_id] = "TRUE" - elif counts.get("TRUE", 0) < counts.get("FALSE", 0): - majority_values[vuln_id] = "FALSE" - # Break ties as 'UNKNOWN' if there was a run with 'UNKNOWN' status - # Otherwise, default to 'TRUE' to be conservative - else: - if "UNKNOWN" in counts: - majority_values[vuln_id] = "UNKNOWN" - else: - majority_values[vuln_id] = "TRUE" - else: - raise ValueError(f"Invalid selection mode: {self.config.selection_mode}") - - # Check for early stopping metadata - early_stopped_values = {} - for item in items: - if item.metadata: - for vuln_id, meta in item.metadata.items(): - if meta.get('early_stopping', False): - early_stopped_values[vuln_id] = meta['early_stopping_value'] - - # Combine majority values with early stopped values, giving precedence to early stopped values - final_values = {**majority_values, **early_stopped_values} - - # Select the first result matching the final value for each vuln_id - new_ttc_item = items[0] - new_output = [] - - for vuln_id, final_value in final_values.items(): - for item in items: - assert isinstance(item.output, AgentMorpheusOutput), "Item output must be of type AgentMorpheusOutput" - - # Create dict for easy output retrieval by vuln_id - output_dict = {output.vuln_id: output for output in item.output.output} - - if output_dict[vuln_id].justification.status == final_value: - new_engine_output = AgentMorpheusEngineOutput(vuln_id=vuln_id, - checklist=output_dict[vuln_id].checklist, - summary=output_dict[vuln_id].summary, - justification=output_dict[vuln_id].justification) - new_output.append(new_engine_output) - break - - assert isinstance(new_ttc_item.output, AgentMorpheusOutput), "TTC output must be of type AgentMorpheusOutput" - new_ttc_item.output.output = new_output - - return [new_ttc_item] - - -@register_ttc_strategy(config_type=MajorityVotingSelectionConfig) -async def register_majority_voting_selector(config: MajorityVotingSelectionConfig, builder: Builder): - """ - Register the MajorityVotingSelector. - """ - selector = MajorityVotingSelector(config) - yield selector diff --git a/src/vuln_analysis/tools/__init__.py b/src/vuln_analysis/tools/__init__.py index cf7c586a5..e69de29bb 100644 --- a/src/vuln_analysis/tools/__init__.py +++ b/src/vuln_analysis/tools/__init__.py @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/src/vuln_analysis/tools/binary_tools.py b/src/vuln_analysis/tools/binary_tools.py new file mode 100644 index 000000000..7f1fc4645 --- /dev/null +++ b/src/vuln_analysis/tools/binary_tools.py @@ -0,0 +1,286 @@ +"""Binary analysis tools for NAT registration.""" + +import json +import logging +import os +import re +import subprocess +from pathlib import Path + +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +from vuln_analysis.tools.filesystem_tools import get_filesystem_root + +logger = logging.getLogger(__name__) + +_ELF_MAGIC = b"\x7fELF" +_ZIP_MAGIC = b"PK\x03\x04" + +_GO_BINARY_CANDIDATES = [ + "go", + os.path.expanduser("~/go-install/bin/go"), + "/usr/local/go/bin/go", + "/snap/bin/go", +] + + +def _find_go_binary() -> str | None: + import shutil + for candidate in _GO_BINARY_CANDIDATES: + if shutil.which(candidate) or os.path.isfile(candidate): + return candidate + return None + + +class _BinaryBaseConfig(FunctionBaseConfig): + binary_analysis_timeout: int = Field(default=30) + max_binary_search_results: int = Field(default=100) + + +class CheckBinaryDepsConfig(_BinaryBaseConfig, name="check_binary_dependencies"): + pass + + +@register_function(config_type=CheckBinaryDepsConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def check_binary_deps_tool(config: CheckBinaryDepsConfig, builder: Builder): + timeout = config.binary_analysis_timeout + + def _analyze_elf(path: Path, magic: bytes) -> str: + parts = ["Format: ELF binary"] + is_go = False + try: + result = subprocess.run( + ["grep", "-c", "go1\\.", str(path)], + capture_output=True, text=True, timeout=5, + ) + is_go = result.returncode == 0 and result.stdout.strip() != "0" + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + if is_go: + parts[0] = "Format: Go ELF binary" + go_bin = _find_go_binary() + if go_bin: + try: + result = subprocess.run([go_bin, "version", "-m", str(path)], capture_output=True, text=True, timeout=timeout) + if result.returncode == 0 and result.stdout.strip(): + parts.append("Go modules:") + parts.append(result.stdout.strip()) + else: + parts.append("go version -m failed; falling back to readelf.") + except FileNotFoundError: + parts.append("'go' command not available; falling back to readelf.") + except subprocess.TimeoutExpired: + parts.append("go version -m timed out; falling back to readelf.") + else: + parts.append("'go' command not found; falling back to readelf.") + try: + result = subprocess.run(["readelf", "-d", str(path)], capture_output=True, text=True, timeout=timeout) + if result.returncode == 0 and result.stdout.strip(): + needed = [line for line in result.stdout.splitlines() if "NEEDED" in line] + if needed: + parts.append("Shared library dependencies (NEEDED):") + parts.extend(needed) + else: + parts.append("No NEEDED shared library entries found (statically linked?).") + else: + stderr_msg = result.stderr.strip()[:200] if result.stderr else "" + parts.append(f"readelf -d returned exit code {result.returncode}. {stderr_msg}") + except FileNotFoundError: + parts.append("'readelf' command not available on this system.") + except subprocess.TimeoutExpired: + parts.append("readelf -d timed out.") + return "\n".join(parts) + + async def check_binary_dependencies(path: str) -> str: + """Auto-detect binary format (ELF, Go, JAR, .pyc) and extract its dependencies or compiled-in modules.""" + logger.info("check_binary_dependencies: %s", path) + resolved = Path(path).resolve() + if not resolved.exists(): + return f"Error: file not found: {path}" + if not resolved.is_file(): + return f"Error: not a regular file: {path}" + try: + with open(resolved, "rb") as f: + magic = f.read(16) + except (PermissionError, OSError) as exc: + return f"Error reading {path}: {exc}" + if magic[:4] == _ELF_MAGIC: + return _analyze_elf(resolved, magic) + return f"Format: Unknown binary — run 'strings' for inspection." + + yield FunctionInfo.from_fn(check_binary_dependencies, description="Auto-detect binary format and extract dependencies") + + +class SearchBinaryContentConfig(_BinaryBaseConfig, name="search_binary_content"): + pass + + +@register_function(config_type=SearchBinaryContentConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def search_binary_content_tool(config: SearchBinaryContentConfig, builder: Builder): + timeout = config.binary_analysis_timeout + max_results = config.max_binary_search_results + + async def search_binary_content(path: str, pattern: str) -> str: + """Search binary files for symbols, function names, or version strings using strings + grep.""" + logger.info("search_binary_content: path=%s pattern=%r", path, pattern) + resolved = Path(path).resolve() + if not resolved.exists(): + return f"Error: file not found: {path}" + if not resolved.is_file(): + return f"Error: not a regular file: {path}" + results: list[str] = [] + try: + strings_proc = subprocess.Popen(["strings", str(resolved)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + proc = subprocess.run(["grep", "-i", "--", pattern], stdin=strings_proc.stdout, capture_output=True, text=True, timeout=timeout) + strings_proc.wait(timeout=5) + if proc.stdout.strip(): + lines = proc.stdout.strip().splitlines() + total = len(lines) + if total > max_results: + lines = lines[:max_results] + lines.append(f"[... truncated -- {total} matches total]") + results.append(f"[strings] Matches ({min(total, max_results)}):") + results.extend(f" {line}" for line in lines) + else: + results.append(f"[strings] No matches for '{pattern}'") + except FileNotFoundError: + results.append("[strings] 'strings' or 'grep' command not available.") + except subprocess.TimeoutExpired: + results.append("[strings] Search timed out.") + if not results: + return f"No results found searching '{pattern}' in {path}" + return "\n".join(results) + + yield FunctionInfo.from_fn(search_binary_content, description="Search binary files for symbols/strings") + + +class CheckDistroPatchesConfig(_BinaryBaseConfig, name="check_distro_patches"): + pass + + +@register_function(config_type=CheckDistroPatchesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def check_distro_patches_tool(config: CheckDistroPatchesConfig, builder: Builder): + timeout = config.binary_analysis_timeout + + async def check_distro_patches(package_name: str, cve_id: str) -> str: + """Check whether Debian/Ubuntu backported a fix for a specific CVE by searching changelog.Debian.gz.""" + logger.info("check_distro_patches: package=%s cve=%s", package_name, cve_id) + + fs = get_filesystem_root() + if fs is None: + return json.dumps({ + "status": "inconclusive", + "evidence": "Filesystem root not configured — call configure_allowed_paths() first.", + "distro_version": None, + }) + + evidence_parts: list[str] = [] + distro_version: str | None = None + status = "inconclusive" + + changelog_dir = fs / "usr" / "share" / "doc" + if changelog_dir.exists(): + try: + changelogs = list(changelog_dir.glob(f"{package_name}*/changelog.Debian.gz")) + if not changelogs: + changelogs = list(changelog_dir.glob(f"*{package_name}*/changelog.Debian.gz")) + for changelog in changelogs[:3]: + try: + result = subprocess.run( + ["zgrep", "-i", cve_id, str(changelog)], + capture_output=True, text=True, timeout=timeout, + ) + if result.returncode == 0 and result.stdout.strip(): + evidence_parts.append( + f"Found {cve_id} in {changelog.relative_to(fs)}:\n" + f"{result.stdout.strip()[:500]}" + ) + status = "patched" + except (FileNotFoundError, subprocess.TimeoutExpired): + evidence_parts.append("zgrep not available or timed out.") + except Exception as exc: + evidence_parts.append(f"Error searching changelogs: {exc}") + + dpkg_status = fs / "var" / "lib" / "dpkg" / "status" + if dpkg_status.exists(): + try: + content = dpkg_status.read_text(encoding="utf-8", errors="ignore") + pkg_pattern = re.compile(rf"^Package:\s*{re.escape(package_name)}\s*$", re.MULTILINE) + match = pkg_pattern.search(content) + if match: + block_start = match.start() + block_end = content.find("\n\n", block_start) + block = content[block_start:block_end] if block_end != -1 else content[block_start:block_start + 500] + version_match = re.search(r"^Version:\s*(.+)$", block, re.MULTILINE) + if version_match: + distro_version = version_match.group(1).strip() + evidence_parts.append(f"dpkg version: {distro_version}") + if any(tag in distro_version for tag in ("ubuntu", "deb", "+deb")): + evidence_parts.append("Distro version suffix detected — backported patches may be included.") + except Exception as exc: + evidence_parts.append(f"Error reading dpkg status: {exc}") + + if not evidence_parts: + evidence_parts.append("No Debian/Ubuntu package metadata found.") + status = "inconclusive" + + if status == "inconclusive" and distro_version: + status = "not_patched" + evidence_parts.append(f"No evidence of {cve_id} backport found in available changelog metadata.") + + return json.dumps({"status": status, "evidence": "\n".join(evidence_parts), "distro_version": distro_version}, indent=2) + + yield FunctionInfo.from_fn(check_distro_patches, description="Check for Debian/Ubuntu backported security patches") + + +class GetBinaryLinkedLibsConfig(FunctionBaseConfig, name="get_binary_linked_libraries"): + binary_analysis_timeout: int = Field(default=30) + + +@register_function(config_type=GetBinaryLinkedLibsConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def get_binary_linked_libs_tool(config: GetBinaryLinkedLibsConfig, builder: Builder): + timeout = config.binary_analysis_timeout + + async def get_binary_linked_libraries(binary_path: str) -> str: + """Return the list of shared libraries an ELF binary is dynamically linked against (NEEDED entries from readelf -d).""" + logger.info("get_binary_linked_libraries: %s", binary_path) + resolved = Path(binary_path).resolve() + if not resolved.exists(): + return f"Error: file not found: {binary_path}" + if not resolved.is_file(): + return f"Error: not a regular file: {binary_path}" + try: + with open(resolved, "rb") as f: + magic = f.read(4) + if magic != _ELF_MAGIC: + return f"Not an ELF binary (magic: {magic.hex()}): {binary_path}" + except (PermissionError, OSError) as exc: + return f"Error reading {binary_path}: {exc}" + try: + result = subprocess.run(["readelf", "-d", str(resolved)], capture_output=True, text=True, timeout=timeout) + except FileNotFoundError: + return "Error: 'readelf' not available on this system." + except subprocess.TimeoutExpired: + return "Error: readelf -d timed out." + if result.returncode != 0: + stderr = result.stderr.strip()[:200] if result.stderr else "" + return f"readelf -d failed (exit {result.returncode}): {stderr}" + needed = [line.strip() for line in result.stdout.splitlines() if "NEEDED" in line] + rpath_lines = [line.strip() for line in result.stdout.splitlines() if "RPATH" in line or "RUNPATH" in line] + parts = [f"ELF binary: {binary_path}"] + if needed: + parts.append(f"\nShared library dependencies ({len(needed)} NEEDED entries):") + parts.extend(f" {line}" for line in needed) + else: + parts.append("\nNo NEEDED entries (statically linked or stripped dynamic section).") + if rpath_lines: + parts.append("\nRPATH/RUNPATH:") + parts.extend(f" {line}" for line in rpath_lines) + return "\n".join(parts) + + yield FunctionInfo.from_fn(get_binary_linked_libraries, description="List shared libraries an ELF binary links against") diff --git a/src/vuln_analysis/tools/filesystem_tools.py b/src/vuln_analysis/tools/filesystem_tools.py new file mode 100644 index 000000000..2a333695e --- /dev/null +++ b/src/vuln_analysis/tools/filesystem_tools.py @@ -0,0 +1,386 @@ +"""Filesystem tools for NAT registration. + +Path sandboxing is configured at runtime via ``configure_allowed_paths()``, +which stores the configuration in a ``contextvars.ContextVar``. Agent +functions call this before running their agentic loops so that the +globally-registered tools know which paths are valid for the current +invocation. +""" + +import contextvars +import fnmatch +import logging +import os +import re +import subprocess +from dataclasses import dataclass, field +from pathlib import Path + +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +logger = logging.getLogger(__name__) + + +@dataclass +class _AllowedPathsConfig: + roots: list[Path] = field(default_factory=list) + files: list[Path] = field(default_factory=list) + filesystem_root: Path | None = None + + +_allowed_paths_var: contextvars.ContextVar[_AllowedPathsConfig | None] = contextvars.ContextVar( + "fs_allowed_paths", default=None, +) + + +def configure_allowed_paths( + roots: list[str], + files: list[str] | None = None, + filesystem_root: str | None = None, +) -> contextvars.Token: + """Set the allowed filesystem paths for the current async context. + + Call this in an agent function before running the agentic loop. + Returns a token that can be used to reset the context var. + """ + cfg = _AllowedPathsConfig( + roots=[Path(r).resolve() for r in roots], + files=[Path(f).resolve() for f in (files or [])], + filesystem_root=Path(filesystem_root).resolve() if filesystem_root else None, + ) + logger.info("Filesystem tools: configured allowed roots=%s, files=%s, fs_root=%s", + cfg.roots, cfg.files, cfg.filesystem_root) + return _allowed_paths_var.set(cfg) + + +def get_filesystem_root() -> Path | None: + """Return the filesystem root from the current context, or None.""" + cfg = _allowed_paths_var.get() + return cfg.filesystem_root if cfg else None + + +def _human_size(nbytes: int) -> str: + for unit in ("B", "KB", "MB", "GB"): + if nbytes < 1024: + return f"{nbytes:.1f} {unit}" if unit != "B" else f"{nbytes} B" + nbytes /= 1024 + return f"{nbytes:.1f} TB" + + +class FilesystemToolkit: + """Sandboxed filesystem tools with configurable limits.""" + + def __init__(self, *, max_file_read_length=50000, max_dir_entries=200, + max_grep_results=100, max_grep_line_chars=500, + max_glob_results=200, logger_name="filesystem_tools"): + self._max_file_read_length = max_file_read_length + self._max_dir_entries = max_dir_entries + self._max_grep_results = max_grep_results + self._max_grep_line_chars = max_grep_line_chars + self._max_glob_results = max_glob_results + self._logger = logging.getLogger(logger_name) + self._allowed_roots: list[Path] = [] + self._allowed_files: list[Path] = [] + self._filesystem_root: Path | None = None + + def set_allowed_paths(self, roots: list[str], files: list[str] | None = None, + filesystem_root: str | None = None): + self._allowed_roots = [Path(r).resolve() for r in roots] + self._allowed_files = [Path(f).resolve() for f in (files or [])] + if filesystem_root: + self._filesystem_root = Path(filesystem_root).resolve() + self._logger.info("Filesystem tools: allowed roots=%s, allowed files=%s, fs_root=%s", + self._allowed_roots, self._allowed_files, self._filesystem_root) + + @property + def filesystem_root(self) -> Path | None: + if self._filesystem_root is not None: + return self._filesystem_root + ctx_cfg = _allowed_paths_var.get() + return ctx_cfg.filesystem_root if ctx_cfg else None + + def _validate_path(self, path_str: str) -> Path: + roots = self._allowed_roots + files = self._allowed_files + + if not roots and not files: + ctx_cfg = _allowed_paths_var.get() + if ctx_cfg is not None: + roots = ctx_cfg.roots + files = ctx_cfg.files + else: + raise RuntimeError( + "Filesystem tool called before set_allowed_paths() or " + "configure_allowed_paths(). Call configure_allowed_paths() " + "in the agent function before running the agentic loop." + ) + + resolved = Path(path_str).resolve() + if resolved in files: + return resolved + for root in roots: + try: + resolved.relative_to(root) + return resolved + except ValueError: + continue + raise ValueError(f"Path '{path_str}' resolves to '{resolved}' which is outside all allowed roots.") + + def read_file(self, path: str) -> str: + self._logger.info("read_file: %s", path) + try: + resolved = self._validate_path(path) + except (ValueError, RuntimeError) as exc: + return f"Error: {exc}" + if not resolved.exists(): + return f"Error: file not found: {path}" + if not resolved.is_file(): + return f"Error: not a regular file: {path}" + try: + content = resolved.read_text(encoding="utf-8") + except UnicodeDecodeError: + try: + content = resolved.read_text(encoding="latin-1") + except Exception as exc: + return f"Error reading {path} (encoding): {exc}" + except PermissionError: + return f"Error: permission denied reading {path}" + except Exception as exc: + return f"Error reading {path}: {exc}" + cap = self._max_file_read_length + if len(content) > cap: + content = (content[:cap] + f"\n\n[... truncated at {cap:,} characters — " + f"file is {len(content):,} characters total. " + f"TIP: For large files, use grep_search with a specific pattern " + f"to find the content you need, rather than reading the entire file.]") + return content + + def list_directory(self, path: str) -> str: + self._logger.info("list_directory: %s", path) + try: + resolved = self._validate_path(path) + except (ValueError, RuntimeError) as exc: + return f"Error: {exc}" + if not resolved.exists(): + return f"Error: directory not found: {path}" + if not resolved.is_dir(): + return f"Error: not a directory: {path}" + try: + entries = list(resolved.iterdir()) + except PermissionError: + return f"Error: permission denied listing {path}" + dirs: list[str] = [] + files: list[str] = [] + for entry in sorted(entries, key=lambda e: e.name.lower()): + try: + if entry.is_dir(): + dirs.append(f"[DIR] {entry.name}/") + else: + size = entry.stat().st_size + files.append(f"[FILE] {entry.name} ({_human_size(size)})") + except (PermissionError, OSError): + files.append(f"[????] {entry.name} (inaccessible)") + all_entries = dirs + files + total = len(all_entries) + if total == 0: + return f"Directory '{path}' is empty." + cap = self._max_dir_entries + if total > cap: + all_entries = all_entries[:cap] + all_entries.append(f"\n[... truncated — showing {cap} of {total} entries]") + header = f"Directory: {path} ({total} entries)\n" + return header + "\n".join(all_entries) + + def _truncate_grep_line(self, line: str, pattern: str) -> str: + """Truncate a long grep match line, keeping context around the match.""" + max_chars = self._max_grep_line_chars + if not max_chars or len(line) <= max_chars: + return line + # Find the location separator (file:lineno:) to preserve it + colon_count = 0 + prefix_end = 0 + for i, ch in enumerate(line): + if ch == ":": + colon_count += 1 + if colon_count >= 2: + prefix_end = i + 1 + break + prefix = line[:prefix_end] + content = line[prefix_end:] + budget = max(max_chars - len(prefix), 80) + if len(content) <= budget: + return line + try: + m = re.search(pattern, content) + except re.error: + m = None + if m: + match_start = m.start() + context = budget // 2 + start = max(0, match_start - context) + end = min(len(content), match_start + context) + snippet = content[start:end] + return f"{prefix}...{snippet}..." if start > 0 else f"{prefix}{snippet}..." + return f"{prefix}{content[:budget]}..." + + def grep_search(self, pattern: str, path: str, include: str | None = None) -> str: + self._logger.info("grep_search: pattern=%r path=%s include=%s", pattern, path, include) + try: + resolved = self._validate_path(path) + except (ValueError, RuntimeError) as exc: + return f"Error: {exc}" + if not resolved.exists(): + return f"Error: path not found: {path}" + cmd = ["grep", "-rn", "--binary-files=without-match", "-E"] + if include: + cmd.extend(["--include", include]) + cmd.extend(["--", pattern, str(resolved)]) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except FileNotFoundError: + return self._grep_python(pattern, resolved, include) + except subprocess.TimeoutExpired: + return f"Error: grep timed out searching for '{pattern}' in {path}" + output = result.stdout + if not output.strip(): + return f"No matches found for pattern '{pattern}' in {path}" + lines = output.strip().split("\n") + lines = [self._truncate_grep_line(line, pattern) for line in lines] + total = len(lines) + cap = self._max_grep_results + if total > cap: + lines = lines[:cap] + lines.append(f"\n[... truncated — showing {cap} of {total} matches]") + return "\n".join(lines) + + def _grep_python(self, pattern: str, root: Path, include: str | None) -> str: + try: + regex = re.compile(pattern) + except re.error as exc: + return f"Error: invalid regex '{pattern}': {exc}" + cap = self._max_grep_results + matches: list[str] = [] + for dirpath, _, filenames in os.walk(root): + for fname in filenames: + if include and not fnmatch.fnmatch(fname, include): + continue + fpath = Path(dirpath) / fname + try: + text = fpath.read_text(encoding="utf-8", errors="ignore") + except (PermissionError, OSError): + continue + for lineno, line in enumerate(text.splitlines(), 1): + if regex.search(line): + entry = f"{fpath}:{lineno}:{line}" + matches.append(self._truncate_grep_line(entry, pattern)) + if len(matches) >= cap: + matches.append(f"\n[... truncated at {cap} matches]") + return "\n".join(matches) + if not matches: + return f"No matches found for pattern '{pattern}' in {root}" + return "\n".join(matches) + + def glob_find(self, pattern: str, path: str) -> str: + self._logger.info("glob_find: pattern=%r path=%s", pattern, path) + try: + resolved = self._validate_path(path) + except (ValueError, RuntimeError) as exc: + return f"Error: {exc}" + if not resolved.exists(): + return f"Error: path not found: {path}" + if not resolved.is_dir(): + return f"Error: not a directory: {path}" + try: + matches = sorted(str(p.relative_to(resolved)) for p in resolved.glob(pattern) if p.is_file()) + except Exception as exc: + return f"Error running glob '{pattern}' in {path}: {exc}" + total = len(matches) + if total == 0: + return f"No files matching '{pattern}' found in {path}" + cap = self._max_glob_results + if total > cap: + matches = matches[:cap] + matches.append(f"\n[... truncated — showing {cap} of {total} matches]") + return "\n".join(matches) + + +class _FsBaseConfig(FunctionBaseConfig): + max_file_read_length: int = Field(default=50000) + max_dir_entries: int = Field(default=200) + max_grep_results: int = Field(default=100) + max_grep_line_chars: int = Field(default=500) + max_glob_results: int = Field(default=200) + + def _toolkit(self) -> FilesystemToolkit: + return FilesystemToolkit( + max_file_read_length=self.max_file_read_length, + max_dir_entries=self.max_dir_entries, + max_grep_results=self.max_grep_results, + max_grep_line_chars=self.max_grep_line_chars, + max_glob_results=self.max_glob_results, + ) + + +class ReadFileConfig(_FsBaseConfig, name="read_file"): + pass + + +@register_function(config_type=ReadFileConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def read_file_tool(config: ReadFileConfig, builder: Builder): + toolkit = config._toolkit() + + async def read_file(path: str) -> str: + """Read a file and return its contents. Handles encoding gracefully and truncates large files.""" + return toolkit.read_file(path) + + yield FunctionInfo.from_fn(read_file, description="Read a file's contents") + + +class ListDirectoryConfig(_FsBaseConfig, name="list_directory"): + pass + + +@register_function(config_type=ListDirectoryConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def list_directory_tool(config: ListDirectoryConfig, builder: Builder): + toolkit = config._toolkit() + + async def list_directory(path: str) -> str: + """List contents of a directory with type indicators ([DIR] or [FILE] with size).""" + return toolkit.list_directory(path) + + yield FunctionInfo.from_fn(list_directory, description="List directory contents") + + +class GrepSearchConfig(_FsBaseConfig, name="grep_search"): + pass + + +@register_function(config_type=GrepSearchConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def grep_search_tool(config: GrepSearchConfig, builder: Builder): + toolkit = config._toolkit() + + async def grep_search(pattern: str, path: str, include: str | None = None) -> str: + """Search for a regex pattern in files under the given path. Returns matching lines with file paths and line numbers.""" + return toolkit.grep_search(pattern, path, include) + + yield FunctionInfo.from_fn(grep_search, description="Search for regex pattern in files") + + +class GlobFindConfig(_FsBaseConfig, name="glob_find"): + pass + + +@register_function(config_type=GlobFindConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def glob_find_tool(config: GlobFindConfig, builder: Builder): + toolkit = config._toolkit() + + async def glob_find(pattern: str, path: str) -> str: + """Find files matching a glob pattern under the given path. Returns sorted list of matching file paths.""" + return toolkit.glob_find(pattern, path) + + yield FunctionInfo.from_fn(glob_find, description="Find files matching a glob pattern") diff --git a/src/vuln_analysis/tools/lexical_full_search.py b/src/vuln_analysis/tools/lexical_full_search.py deleted file mode 100644 index ad73048e0..000000000 --- a/src/vuln_analysis/tools/lexical_full_search.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -logger = logging.getLogger(__name__) - - -class LexicalSearchToolConfig(FunctionBaseConfig, name="lexical_code_search"): - """ - Lexical search tool used to search source code. - """ - top_k: int = Field(default=5, description="Top K to use for the lexical search") - - -@register_function(config_type=LexicalSearchToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def lexical_search(config: LexicalSearchToolConfig, builder: Builder): # pylint: disable=unused-argument - from vuln_analysis.functions.cve_agent import ctx_state - from vuln_analysis.utils.full_text_search import FullTextSearch - - async def _arun(query: str) -> list: - - workflow_state = ctx_state.get() - code_index_path = workflow_state.code_index_path - full_text_search = FullTextSearch(cache_path=code_index_path) - - if full_text_search.is_empty(): - raise ValueError(f"Invalid code index at: {code_index_path}, index is empty") - - result = full_text_search.search_index(query, config.top_k) - - return result - - yield FunctionInfo.from_fn( - _arun, - description=("Useful for when you need to check if an application or any dependency " - "within the container image uses a function or a component of a library " - "using keyword search. This tool uses keyword to search code index, and " - "the argument should be a string keyword. You should use this search " - "tool for searching codes before trying other container search related tools.")) diff --git a/src/vuln_analysis/tools/local_vdb.py b/src/vuln_analysis/tools/local_vdb.py deleted file mode 100644 index df7491bfb..000000000 --- a/src/vuln_analysis/tools/local_vdb.py +++ /dev/null @@ -1,108 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.framework_enum import LLMFrameworkEnum -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -from vuln_analysis.data_models.vdb_type import VdbType - -logger = logging.getLogger(__name__) - - -class LocalVDBRetrieverToolConfig(FunctionBaseConfig, name="local_vdb_retriever"): - """ - Retriever tool used to query source code and documentation vector databases. - """ - embedder_name: str = Field(description="The embedder to use") - llm_name: str = Field(description="The LLM model to use") - vdb_type: VdbType = Field(description="Indicate if querying code or documentation. Use code or doc") - return_source_documents: bool = Field(default=False, description="Whether to return source documents") - - -@register_function(config_type=LocalVDBRetrieverToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def load_vectordb_asretriever(config: LocalVDBRetrieverToolConfig, builder: Builder): - - from langchain.chains.retrieval_qa.base import RetrievalQA - from langchain_community.vectorstores import FAISS - from langchain_core.prompts import PromptTemplate - - from vuln_analysis.functions.cve_agent import RateLimitingCallback - from vuln_analysis.functions.cve_agent import ctx_rate_limiter - from vuln_analysis.functions.cve_agent import ctx_state - - embedder = await builder.get_embedder(embedder_name=config.embedder_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - llm = await builder.get_llm(llm_name=config.llm_name, wrapper_type=LLMFrameworkEnum.LANGCHAIN) - - async def _arun(query: str) -> str | dict: - - # workaround since the agent executor only accepts strings. - workflow_state = ctx_state.get() - - if config.vdb_type == VdbType.CODE: - db_source = workflow_state.code_vdb_path - elif config.vdb_type == VdbType.DOC: - db_source = workflow_state.doc_vdb_path - else: - raise ValueError(f"Invalid VDB type: {config.vdb_type}. Must be one of {VdbType.CODE} or {VdbType.DOC}.") - - qa_prompt = PromptTemplate(template=("Use the following pieces of context to answer the question at the end. " - "If you don't know the answer, just say that you don't know, " - "don't try to make up an answer.\n\n{context}\n\n" - "Question: {question}\nHelpful Answer:"), - input_variables=['context', 'question']) - - vector_db = FAISS.load_local(db_source, embedder, allow_dangerous_deserialization=True) - retrieval_qa_tool = RetrievalQA.from_chain_type(llm=llm, - chain_type="stuff", - chain_type_kwargs={"prompt": qa_prompt}, - retriever=vector_db.as_retriever(), - return_source_documents=config.return_source_documents) - - # Get the rate limiter from context and create a tool-specific callback - rate_limiter = ctx_rate_limiter.get() - try: - if rate_limiter is not None: - tool_callbacks = [RateLimitingCallback(rate_limiter)] - output_dict = await retrieval_qa_tool.ainvoke({"query": query}, - config={"callbacks": tool_callbacks}) # type: ignore - else: - output_dict = await retrieval_qa_tool.ainvoke({"query": query}) - except Exception as e: - logger.error("Error in VDB retrieval: %s: %s", type(e).__name__, e) - raise - - # If returning source documents, include the result and source_documents keys in the output - if config.return_source_documents: - return {k: v for k, v in output_dict.items() if k in ["result", "source_documents"]} - - # If not returning source documents, return only the result as a string - return output_dict["result"] - - if config.vdb_type == VdbType.CODE: - description = ("Useful for when you need to check if an application or any dependency " - "within the container image uses a function or a component of a library.") - elif config.vdb_type == VdbType.DOC: - description = ("Useful for when you need to ask questions about the purpose and " - "functionality of the container image.") - else: - raise ValueError(f"Invalid VDB type: {config.vdb_type}. Must be one of {VdbType.CODE} or {VdbType.DOC}.") - - yield FunctionInfo.from_fn(_arun, description=description) diff --git a/src/vuln_analysis/tools/osv_tools.py b/src/vuln_analysis/tools/osv_tools.py new file mode 100644 index 000000000..734444d38 --- /dev/null +++ b/src/vuln_analysis/tools/osv_tools.py @@ -0,0 +1,182 @@ +"""OSV lookup tool for NAT registration.""" + +import logging + +import httpx +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from packaging.version import InvalidVersion, Version + +logger = logging.getLogger(__name__) + +_OSV_API_BASE = "https://api.osv.dev/v1/vulns" +_osv_cache: dict[str, dict | None] = {} + + +def _normalize_v(v: str) -> str: + return v[1:] if v.startswith("v") else v + + +def _parse_version_safe(v: str) -> Version | None: + try: + return Version(_normalize_v(v)) + except InvalidVersion: + return None + + +def _fetch_osv(cve_id: str) -> dict | None: + if cve_id in _osv_cache: + return _osv_cache[cve_id] + url = f"{_OSV_API_BASE}/{cve_id}" + logger.info("osv_lookup: fetching %s", url) + try: + with httpx.Client(timeout=30, follow_redirects=True) as client: + resp = client.get(url) + if resp.status_code == 404: + _osv_cache[cve_id] = None + return None + resp.raise_for_status() + data = resp.json() + _osv_cache[cve_id] = data + return data + except Exception as exc: + logger.error("osv_lookup: failed to fetch %s: %s", url, exc) + _osv_cache[cve_id] = None + return None + + +def _osv_lookup_impl(cve_id: str, version: str, package_name: str | None = None) -> str: + logger.info("osv_lookup: %s version=%s pkg=%s", cve_id, version, package_name) + data = _fetch_osv(cve_id) + if data is None: + return ( + f"{cve_id} | NO OSV DATA\n" + f" OSV.dev has no entry for this CVE. Verify fix versions using distro\n" + f" security trackers or upstream release notes instead." + ) + + affected_entries = data.get("affected", []) + if package_name: + filtered = [ + a for a in affected_entries + if package_name.lower() in (a.get("package", {}).get("name", "").lower()) + ] + if filtered: + affected_entries = filtered + + norm_query = _normalize_v(version) + all_affected_versions: set[str] = set() + fix_commits: list[str] = [] + + for entry in affected_entries: + for v in entry.get("versions", []): + all_affected_versions.add(_normalize_v(v)) + for rng in entry.get("ranges", []): + for event in rng.get("events", []): + if "fixed" in event: + commit = event["fixed"] + fix_commits.append(commit[:12]) + + is_affected = norm_query in all_affected_versions + + nearest_line = "" + if is_affected: + parsed_query = _parse_version_safe(norm_query) + if parsed_query is not None: + prefix = f"{parsed_query.major}.{parsed_query.minor}." + same_branch = [v for v in all_affected_versions if v.startswith(prefix)] + parsed_branch = [] + for v in same_branch: + pv = _parse_version_safe(v) + if pv is not None: + parsed_branch.append((pv, v)) + parsed_branch.sort(key=lambda t: t[0]) + max_affected = parsed_branch[-1][0] if parsed_branch else parsed_query + candidate = Version(f"{parsed_query.major}.{parsed_query.minor}.{max_affected.micro + 1}") + if _normalize_v(str(candidate)) not in all_affected_versions: + nearest_line = f" Nearest unaffected {prefix[:-1]}: {candidate}" + else: + nearest_line = f" Nearest unaffected {prefix[:-1]}: unknown — check upstream release page" + + fix_line = "" + if fix_commits: + unique_commits = list(dict.fromkeys(fix_commits)) + fix_line = f" Fix commits: {', '.join(unique_commits[:7])}" + if len(unique_commits) > 7: + fix_line += ", ..." + + if is_affected: + parts = [ + f"{cve_id} | version {version} | AFFECTED", + f" {version} is in the OSV affected versions list.", + ] + if nearest_line: + parts.append(nearest_line) + if fix_line: + parts.append(fix_line) + return "\n".join(parts) + + parsed_query = _parse_version_safe(norm_query) + if parsed_query is not None: + prefix = f"{parsed_query.major}.{parsed_query.minor}." + branch_parsed: list[tuple[Version, str]] = [] + for v in all_affected_versions: + if v.startswith(prefix): + pv = _parse_version_safe(v) + if pv is not None: + branch_parsed.append((pv, v)) + + if branch_parsed: + release_vers = [(pv, v) for pv, v in branch_parsed if not pv.is_prerelease] + if not release_vers: + branch_parsed.sort() + pre_labels = [v for _, v in branch_parsed[:5]] + pre_str = ", ".join(pre_labels) + if len(branch_parsed) > 5: + pre_str += ", ..." + branch_label = f"{parsed_query.major}.{parsed_query.minor}" + parts = [ + f"{cve_id} | version {version} | AFFECTED (inferred)", + f" {version} is not explicitly listed, but pre-release versions from the", + f" {branch_label} branch ({pre_str}) ARE in the affected list, and no", + f" fix for the {branch_label} branch appears in OSV data. All", + f" {branch_label}.x release versions are therefore affected.", + ] + if fix_line: + parts.append(fix_line) + return "\n".join(parts) + elif all_affected_versions: + branch_label = f"{parsed_query.major}.{parsed_query.minor}" + return ( + f"{cve_id} | version {version} | UNCERTAIN (INCOMPLETE DATA)\n" + f" No versions from the {branch_label} branch appear in OSV data.\n" + f" Cannot determine affected status. Verify using distro security\n" + f" trackers (Ubuntu, Debian, Red Hat) or upstream release notes." + ) + + return ( + f"{cve_id} | version {version} | NOT AFFECTED\n" + f" {version} is NOT in the OSV affected versions list." + ) + + +class OSVLookupToolConfig(FunctionBaseConfig, name="osv_lookup"): + pass + + +@register_function(config_type=OSVLookupToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def osv_lookup_tool(config: OSVLookupToolConfig, builder: Builder): + + async def _arun(cve_id: str, version: str, package_name: str | None = None) -> str: + return _osv_lookup_impl(cve_id, version, package_name) + + yield FunctionInfo.from_fn( + _arun, + description=( + "Check whether a specific package version is affected by a CVE " + "using OSV.dev data. Returns AFFECTED or NOT AFFECTED." + ), + ) diff --git a/src/vuln_analysis/tools/package_tools.py b/src/vuln_analysis/tools/package_tools.py new file mode 100644 index 000000000..8223d96b7 --- /dev/null +++ b/src/vuln_analysis/tools/package_tools.py @@ -0,0 +1,765 @@ +"""Package database, discovery, and security tools for NAT registration. + +These tools give the exploitability analyzer access to package managers, +shared library discovery, and security control detection on the container +filesystem. +""" + +import json +import logging +import os +import re +import subprocess +import zipfile +from pathlib import Path + +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +from vuln_analysis.tools.filesystem_tools import get_filesystem_root + +logger = logging.getLogger(__name__) + +_ELF_MAGIC = b"\x7fELF" + +_GO_BINARY_CANDIDATES = [ + "go", + os.path.expanduser("~/go-install/bin/go"), + "/usr/local/go/bin/go", + "/snap/bin/go", +] + + +def _find_go_binary() -> str | None: + import shutil + + for candidate in _GO_BINARY_CANDIDATES: + if shutil.which(candidate) or os.path.isfile(candidate): + return candidate + return None + + +# --- 1. query_dpkg_database --- + + +class QueryDpkgDatabaseConfig(FunctionBaseConfig, name="query_dpkg_database"): + pass + + +@register_function(config_type=QueryDpkgDatabaseConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def query_dpkg_database_tool(config: QueryDpkgDatabaseConfig, builder: Builder): + async def query_dpkg_database(package_pattern: str) -> str: + """Query the dpkg package database for packages matching a name pattern. Returns name, version, status, architecture for all matching packages. Use for Debian/Ubuntu containers to find C system libraries (libtiff5, libssl3, etc.).""" + logger.info("query_dpkg_database: pattern=%s", package_pattern) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + dpkg_status = fs / "var" / "lib" / "dpkg" / "status" + if not dpkg_status.exists(): + return "No dpkg database found at /var/lib/dpkg/status -- this may not be a Debian/Ubuntu container." + try: + content = dpkg_status.read_text(encoding="utf-8", errors="ignore") + except OSError as exc: + return f"Error reading dpkg database: {exc}" + pattern_lower = package_pattern.lower() + matches: list[dict] = [] + for block in content.strip().split("\n\n"): + pkg_info: dict[str, str] = {} + for line in block.strip().splitlines(): + if ": " in line: + key, _, value = line.partition(": ") + pkg_info[key.strip()] = value.strip() + pkg_name = pkg_info.get("Package", "") + if not pkg_name: + continue + try: + if re.search(pattern_lower, pkg_name.lower()): + matches.append(pkg_info) + except re.error: + if pattern_lower in pkg_name.lower(): + matches.append(pkg_info) + if not matches: + return f"No packages matching '{package_pattern}' found in dpkg database." + lines_out = [f"Found {len(matches)} package(s) matching '{package_pattern}':"] + for m in matches[:30]: + lines_out.append("") + lines_out.append(f" Package: {m.get('Package', 'N/A')}") + lines_out.append(f" Version: {m.get('Version', 'N/A')}") + lines_out.append(f" Status: {m.get('Status', 'N/A')}") + lines_out.append(f" Architecture: {m.get('Architecture', 'N/A')}") + desc = m.get("Description", "") + if desc: + lines_out.append(f" Description: {desc[:100]}") + if len(matches) > 30: + lines_out.append(f"\n[... {len(matches) - 30} more matches truncated ...]") + return "\n".join(lines_out) + + yield FunctionInfo.from_fn(query_dpkg_database, description="Query dpkg database for Debian/Ubuntu packages") + + +# --- 2. search_dpkg_file_lists --- + + +class SearchDpkgFileListsConfig(FunctionBaseConfig, name="search_dpkg_file_lists"): + pass + + +@register_function(config_type=SearchDpkgFileListsConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def search_dpkg_file_lists_tool(config: SearchDpkgFileListsConfig, builder: Builder): + async def search_dpkg_file_lists(file_pattern: str) -> str: + """Search dpkg file ownership lists to find which package provides a specific file. Use to identify which package installed a shared library or binary.""" + logger.info("search_dpkg_file_lists: pattern=%s", file_pattern) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + dpkg_info = fs / "var" / "lib" / "dpkg" / "info" + if not dpkg_info.exists(): + return "No dpkg info directory found -- this may not be a Debian/Ubuntu container." + pattern_lower = file_pattern.lower() + matches: list[tuple[str, list[str]]] = [] + try: + for list_file in sorted(dpkg_info.glob("*.list")): + try: + content = list_file.read_text(encoding="utf-8", errors="ignore") + matching_lines = [line for line in content.splitlines() if pattern_lower in line.lower()] + if matching_lines: + matches.append((list_file.stem, matching_lines)) + except (PermissionError, OSError): + continue + except PermissionError: + return f"Permission error accessing {dpkg_info}" + if not matches: + return f"No files matching '{file_pattern}' found in dpkg file lists." + lines = [f"Found '{file_pattern}' owned by {len(matches)} package(s):"] + for pkg_name, pkg_lines in matches[:20]: + lines.append(f"\n Package: {pkg_name}") + for line in pkg_lines[:5]: + lines.append(f" {line}") + if len(pkg_lines) > 5: + lines.append(f" [... {len(pkg_lines) - 5} more files ...]") + if len(matches) > 20: + lines.append(f"\n[... {len(matches) - 20} more packages truncated ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(search_dpkg_file_lists, description="Find which dpkg package owns a file") + + +# --- 3. query_rpm_database --- + + +class QueryRpmDatabaseConfig(FunctionBaseConfig, name="query_rpm_database"): + pass + + +@register_function(config_type=QueryRpmDatabaseConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def query_rpm_database_tool(config: QueryRpmDatabaseConfig, builder: Builder): + async def query_rpm_database(package_pattern: str) -> str: + """Query the RPM package database for packages matching a name pattern. Use for RHEL/CentOS/Fedora/SUSE containers.""" + logger.info("query_rpm_database: pattern=%s", package_pattern) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + rpm_db_path: Path | None = None + for candidate in [fs / "var" / "lib" / "rpm", fs / "usr" / "lib" / "sysimage" / "rpm"]: + if candidate.exists(): + rpm_db_path = candidate + break + if rpm_db_path is None: + return "No RPM database found -- this may not be a RHEL/Fedora/SUSE container." + try: + result = subprocess.run( + ["rpm", "--dbpath", str(rpm_db_path), "-qa", "--queryformat", r"%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"], + capture_output=True, + text=True, + timeout=30, + ) + except FileNotFoundError: + return f"RPM database found at {rpm_db_path} but 'rpm' command is not available on the host system." + except subprocess.TimeoutExpired: + return "rpm -qa timed out (30s)." + if result.returncode != 0: + stderr = result.stderr.strip()[:300] if result.stderr else "" + return f"rpm -qa failed (exit {result.returncode}): {stderr}" + pattern_lower = package_pattern.lower() + matches: list[str] = [] + for line in result.stdout.strip().splitlines(): + parts = line.split("\t") + if parts: + try: + if re.search(pattern_lower, parts[0].lower()): + matches.append(line) + except re.error: + if pattern_lower in parts[0].lower(): + matches.append(line) + if not matches: + return f"No packages matching '{package_pattern}' found in RPM database." + lines = [f"Found {len(matches)} RPM package(s) matching '{package_pattern}':"] + lines.append(f" {'Name':<40} {'Version-Release':<40} Arch") + lines.append(" " + "-" * 85) + for m in matches[:30]: + parts = m.split("\t") + name = parts[0] if parts else "" + ver = parts[1] if len(parts) > 1 else "" + arch = parts[2].strip() if len(parts) > 2 else "" + lines.append(f" {name:<40} {ver:<40} {arch}") + if len(matches) > 30: + lines.append(f"[... {len(matches) - 30} more matches truncated ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(query_rpm_database, description="Query RPM database for RHEL/Fedora/SUSE packages") + + +# --- 4. list_python_packages --- + + +class ListPythonPackagesConfig(FunctionBaseConfig, name="list_python_packages"): + pass + + +@register_function(config_type=ListPythonPackagesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def list_python_packages_tool(config: ListPythonPackagesConfig, builder: Builder): + async def list_python_packages(search_path: str | None = None) -> str: + """List all Python packages installed across all environments in the container filesystem. Searches dist-info/METADATA in all site-packages directories.""" + logger.info("list_python_packages: search_path=%s", search_path) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + search_root = Path(search_path) if search_path else fs + if not search_root.exists(): + return f"Error: path not found: {search_path}" + packages_by_env: dict[str, list[str]] = {} + try: + for site_pkg in search_root.rglob("site-packages"): + if not site_pkg.is_dir(): + continue + try: + env_label = "/" + str(site_pkg.relative_to(fs)) + except ValueError: + env_label = str(site_pkg) + packages: list[str] = [] + for meta_file in site_pkg.glob("*.dist-info/METADATA"): + try: + content = meta_file.read_text(encoding="utf-8", errors="ignore") + name_match = re.search(r"^Name:\s*(.+)$", content, re.MULTILINE) + version_match = re.search(r"^Version:\s*(.+)$", content, re.MULTILINE) + if name_match and version_match: + packages.append(f"{name_match.group(1).strip()}=={version_match.group(1).strip()}") + except (PermissionError, OSError): + continue + if packages: + packages.sort() + packages_by_env[env_label] = packages + except PermissionError as exc: + return f"Permission error searching {search_root}: {exc}" + if not packages_by_env: + return f"No Python packages found in {search_path or 'container filesystem'}." + lines: list[str] = [] + for env, pkgs in sorted(packages_by_env.items()): + lines.append(f"\nEnvironment: {env} ({len(pkgs)} packages)") + for pkg in pkgs[:100]: + lines.append(f" {pkg}") + if len(pkgs) > 100: + lines.append(f" [... {len(pkgs) - 100} more packages ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(list_python_packages, description="List all Python packages across all environments") + + +# --- 5. find_shared_libraries --- + + +class FindSharedLibrariesConfig(FunctionBaseConfig, name="find_shared_libraries"): + pass + + +@register_function(config_type=FindSharedLibrariesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def find_shared_libraries_tool(config: FindSharedLibrariesConfig, builder: Builder): + async def find_shared_libraries(library_pattern: str, search_path: str | None = None) -> str: + """Find shared library (.so) files matching a name pattern in the container filesystem. Searches /usr/lib, /lib, /usr/local/lib and similar directories.""" + logger.info("find_shared_libraries: pattern=%s search_path=%s", library_pattern, search_path) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + if search_path: + search_roots = [Path(search_path)] if Path(search_path).exists() else [] + else: + search_roots = [] + for lib_dir in ["usr/lib", "usr/lib64", "lib", "lib64", "usr/local/lib", "usr/local/lib64"]: + candidate = fs / lib_dir + if candidate.exists(): + search_roots.append(candidate) + if not search_roots: + return "No library directories found in container filesystem." + pattern_lower = library_pattern.lower() + found_libs: list[str] = [] + for root in search_roots: + try: + for so_file in root.rglob("*.so*"): + if not so_file.is_file(): + continue + if pattern_lower in so_file.name.lower(): + try: + rel_path = "/" + str(so_file.relative_to(fs)) + except ValueError: + rel_path = str(so_file) + found_libs.append(rel_path) + except PermissionError: + continue + if not found_libs: + return f"No shared libraries matching '{library_pattern}' found in the container filesystem." + found_libs.sort() + lines = [f"Found {len(found_libs)} shared library file(s) matching '{library_pattern}':"] + for lib in found_libs[:50]: + lines.append(f" {lib}") + if len(found_libs) > 50: + lines.append(f"[... {len(found_libs) - 50} more files truncated ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(find_shared_libraries, description="Find shared library (.so) files by name pattern") + + +def _extract_go_modules_via_strings(binary: Path, timeout: int = 30) -> str: + """Fallback: extract embedded Go module table from binary using 'strings'. + + Go embeds a buildinfo section with lines like: + dep\t\t\t + These are readable ASCII even without the 'go' toolchain present. + """ + try: + result = subprocess.run( + ["strings", "-n", "10", str(binary)], + capture_output=True, + text=True, + timeout=timeout, + ) + except (FileNotFoundError, subprocess.TimeoutExpired) as e: + return f"(strings fallback unavailable: {e})" + + # Pull out the buildinfo block: lines that look like module metadata + module_lines = [] + for line in result.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith(("dep\t", "require\t", "mod\t", "build\t", "path\t")): + module_lines.append(stripped) + elif stripped.startswith("go\t") and stripped.count("\t") == 1: + module_lines.append(stripped) + + if not module_lines: + return "(no embedded module table found via strings fallback)" + return "(via strings fallback — 'go' not available)\n" + "\n".join(module_lines[:200]) + + +# --- 6. find_go_binaries --- + + +class FindGoBinariesConfig(FunctionBaseConfig, name="find_go_binaries"): + binary_analysis_timeout: int = Field(default=30) + max_files_to_walk: int = Field(default=100_000) + max_go_binaries: int = Field(default=20) + + +@register_function(config_type=FindGoBinariesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def find_go_binaries_tool(config: FindGoBinariesConfig, builder: Builder): + timeout = config.binary_analysis_timeout + max_files_to_walk = config.max_files_to_walk + max_go_binaries = config.max_go_binaries + + async def find_go_binaries(search_path: str) -> str: + """Find all Go ELF binaries in a directory tree and list their embedded module dependency information. Use when checking if a vulnerable Go module is compiled into any binary.""" + logger.info("find_go_binaries: search_path=%s", search_path) + resolved = Path(search_path).resolve() + if not resolved.exists(): + return f"Error: path not found: {search_path}" + if not resolved.is_dir(): + return f"Error: not a directory: {search_path}" + fs = get_filesystem_root() + go_binaries: list[Path] = [] + files_checked = 0 + try: + for candidate in resolved.rglob("*"): + if files_checked >= max_files_to_walk: + break + if not candidate.is_file(): + continue + files_checked += 1 + try: + with open(candidate, "rb") as f: + magic = f.read(4) + if magic != _ELF_MAGIC: + continue + except (PermissionError, OSError): + continue + try: + result = subprocess.run( + ["strings", "-n", "10", str(candidate)], + capture_output=True, + text=True, + timeout=10, + ) + if "go1." in result.stdout[:20000]: + go_binaries.append(candidate) + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + except PermissionError: + pass + if not go_binaries: + note = f" (searched {files_checked} files" + (f", stopped at limit {max_files_to_walk}" if files_checked >= max_files_to_walk else "") + ")" + return f"No Go binaries found in {search_path}{note}." + go_bin = _find_go_binary() + lines = [f"Found {len(go_binaries)} Go binary/binaries in {search_path}:"] + for binary in go_binaries[:max_go_binaries]: + try: + rel = "/" + str(binary.relative_to(fs)) if fs else str(binary) + except ValueError: + rel = str(binary) + lines.append(f"\n=== {rel} ===") + if go_bin: + try: + result = subprocess.run( + [go_bin, "version", "-m", str(binary)], + capture_output=True, + text=True, + timeout=timeout, + ) + if result.returncode == 0 and result.stdout.strip(): + lines.append(result.stdout.strip()[:3000]) + else: + lines.append(_extract_go_modules_via_strings(binary, timeout)) + except (FileNotFoundError, subprocess.TimeoutExpired): + lines.append(_extract_go_modules_via_strings(binary, timeout)) + else: + lines.append(_extract_go_modules_via_strings(binary, timeout)) + if len(go_binaries) > max_go_binaries: + lines.append(f"\n[... {len(go_binaries) - max_go_binaries} more Go binaries not shown ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(find_go_binaries, description="Find Go binaries and list their embedded modules") + + +# --- 7. check_security_controls --- + + +class CheckSecurityControlsConfig(FunctionBaseConfig, name="check_security_controls"): + pass + + +@register_function(config_type=CheckSecurityControlsConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def check_security_controls_tool(config: CheckSecurityControlsConfig, builder: Builder): + async def check_security_controls(scope: str = "all") -> str: + """Check the container filesystem for evidence of security controls: AppArmor profiles, seccomp policies, non-root users, capability dropping. The scope parameter is optional (default: 'all').""" + logger.info("check_security_controls") + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + findings: list[str] = [] + for apparmor_path in [fs / "etc" / "apparmor.d", fs / "etc" / "apparmor"]: + if apparmor_path.exists(): + try: + profiles = [p for p in apparmor_path.rglob("*") if p.is_file()] + findings.append(f"AppArmor: {len(profiles)} profile(s) at /{apparmor_path.relative_to(fs)}") + for p in profiles[:5]: + findings.append(f" /{p.relative_to(fs)}") + except PermissionError: + findings.append("AppArmor directory found (permission denied reading).") + for seccomp_path in [fs / "etc" / "seccomp", fs / "etc" / "docker" / "seccomp", fs / "usr" / "share" / "containers" / "seccomp"]: + if seccomp_path.exists(): + try: + profiles = [p.name for p in seccomp_path.rglob("*.json") if p.is_file()] + findings.append(f"Seccomp: {len(profiles)} policy file(s) at /{seccomp_path.relative_to(fs)}") + except PermissionError: + findings.append("Seccomp directory found (permission denied).") + passwd_file = fs / "etc" / "passwd" + if passwd_file.exists(): + try: + content = passwd_file.read_text(encoding="utf-8", errors="ignore") + app_users = [] + for line in content.splitlines(): + if line.startswith("#"): + continue + parts = line.split(":") + if len(parts) >= 3: + try: + uid = int(parts[2]) + if 500 <= uid <= 65533: + shell = parts[6].strip() if len(parts) > 6 else "N/A" + app_users.append(f" {parts[0]} (UID={uid}, shell={shell})") + except ValueError: + pass + if app_users: + findings.append("\nNon-root application users in /etc/passwd:") + findings.extend(app_users[:10]) + except (PermissionError, OSError): + pass + security_keywords = ["setcap", "seccomp", "capsh", "prctl", "no-new-privileges", r"drop.*cap", "apparmor"] + entrypoint_candidates = [ + fs / "docker-entrypoint.sh", + fs / "entrypoint.sh", + fs / "opt" / "nvidia" / "nvidia_entrypoint.sh", + ] + for ep in entrypoint_candidates: + if ep.is_file(): + try: + content = ep.read_text(encoding="utf-8", errors="ignore") + for kw in security_keywords: + if re.search(kw, content, re.IGNORECASE): + try: + rel = ep.relative_to(fs) + except ValueError: + rel = ep + findings.append(f"\nSecurity keyword '{kw}' found in /{rel}") + except (PermissionError, OSError): + pass + if not findings: + findings.append("No explicit security controls found baked into the container image.") + findings.append("Note: Docker default seccomp and K8s pod security are applied at runtime, not stored in the image.") + return "\n".join(findings) + + yield FunctionInfo.from_fn(check_security_controls, description="Detect security controls in container filesystem") + + +# --- 8. query_npm_packages --- + + +class QueryNpmPackagesConfig(FunctionBaseConfig, name="query_npm_packages"): + pass + + +@register_function(config_type=QueryNpmPackagesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def query_npm_packages_tool(config: QueryNpmPackagesConfig, builder: Builder): + async def query_npm_packages(package_pattern: str | None = None, search_path: str | None = None) -> str: + """Find Node.js/npm packages installed in the container filesystem. Searches node_modules directories and reads package.json files.""" + logger.info("query_npm_packages: pattern=%s search_path=%s", package_pattern, search_path) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + search_root = Path(search_path) if search_path else fs + if not search_root.exists(): + return f"Error: path not found: {search_path}" + pattern_lower = package_pattern.lower() if package_pattern else None + packages_by_location: dict[str, list[str]] = {} + try: + for node_modules in search_root.rglob("node_modules"): + if not node_modules.is_dir(): + continue + if "node_modules" in str(node_modules.parent.relative_to(search_root)): + continue + try: + loc_label = "/" + str(node_modules.relative_to(fs)) + except ValueError: + loc_label = str(node_modules) + packages: list[str] = [] + for pkg_dir in node_modules.iterdir(): + if not pkg_dir.is_dir(): + continue + if pkg_dir.name.startswith("@"): + for scoped_dir in pkg_dir.iterdir(): + if scoped_dir.is_dir(): + _extract_npm(scoped_dir, pattern_lower, packages) + else: + _extract_npm(pkg_dir, pattern_lower, packages) + if packages: + packages.sort() + packages_by_location[loc_label] = packages + except PermissionError as exc: + return f"Permission error: {exc}" + if not packages_by_location: + msg = "No npm packages" + if package_pattern: + msg += f" matching '{package_pattern}'" + return msg + f" found in {search_path or 'container filesystem'}." + lines: list[str] = [] + total = sum(len(p) for p in packages_by_location.values()) + lines.append(f"Found {total} npm package(s):") + for loc, pkgs in sorted(packages_by_location.items()): + lines.append(f"\nLocation: {loc} ({len(pkgs)} packages)") + for pkg in pkgs[:100]: + lines.append(f" {pkg}") + if len(pkgs) > 100: + lines.append(f" [... {len(pkgs) - 100} more ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(query_npm_packages, description="Find Node.js/npm packages in container") + + +def _extract_npm(pkg_dir: Path, pattern_lower: str | None, packages: list[str]) -> None: + pkg_json = pkg_dir / "package.json" + if not pkg_json.is_file(): + return + try: + data = json.loads(pkg_json.read_text(encoding="utf-8", errors="ignore")) + name = data.get("name", pkg_dir.name) + version = data.get("version", "unknown") + if pattern_lower is None or pattern_lower in name.lower(): + packages.append(f"{name}@{version}") + except (json.JSONDecodeError, PermissionError, OSError): + pass + + +# --- 9. find_java_packages --- + + +class FindJavaPackagesConfig(FunctionBaseConfig, name="find_java_packages"): + pass + + +@register_function(config_type=FindJavaPackagesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def find_java_packages_tool(config: FindJavaPackagesConfig, builder: Builder): + async def find_java_packages(pattern: str | None = None, search_path: str | None = None) -> str: + """Find Java JAR/WAR/EAR files and extract version metadata from MANIFEST.MF and Maven pom.properties.""" + logger.info("find_java_packages: pattern=%s search_path=%s", pattern, search_path) + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + search_root = Path(search_path) if search_path else fs + if not search_root.exists(): + return f"Error: path not found: {search_path}" + pattern_lower = pattern.lower() if pattern else None + found: list[dict] = [] + try: + for java_file in search_root.rglob("*"): + if not java_file.is_file(): + continue + if java_file.suffix.lower() not in (".jar", ".war", ".ear"): + continue + if pattern_lower and pattern_lower not in java_file.name.lower(): + continue + try: + rel_path = "/" + str(java_file.relative_to(fs)) + except ValueError: + rel_path = str(java_file) + info: dict = {"path": rel_path, "manifest": {}, "maven": {}} + try: + with zipfile.ZipFile(java_file, "r") as zf: + names = zf.namelist() + if "META-INF/MANIFEST.MF" in names: + try: + manifest_text = zf.read("META-INF/MANIFEST.MF").decode("utf-8", errors="ignore") + for mline in manifest_text.splitlines(): + if ": " in mline: + k, _, v = mline.partition(": ") + k = k.strip() + if k in ( + "Implementation-Title", + "Implementation-Version", + "Bundle-Name", + "Bundle-Version", + "Specification-Version", + ): + info["manifest"][k] = v.strip() + except Exception: + pass + for name in names: + if name.endswith("pom.properties") and name.startswith("META-INF/maven/"): + try: + props_text = zf.read(name).decode("utf-8", errors="ignore") + for pline in props_text.splitlines(): + if "=" in pline and not pline.startswith("#"): + k, _, v = pline.partition("=") + info["maven"][k.strip()] = v.strip() + break + except Exception: + pass + except Exception: + pass + found.append(info) + except PermissionError as exc: + return f"Permission error: {exc}" + if not found: + msg = "No Java archive files" + if pattern: + msg += f" matching '{pattern}'" + return msg + f" found in {search_path or 'container filesystem'}." + lines = [f"Found {len(found)} Java archive(s):"] + for item in found[:50]: + lines.append(f"\n {item['path']}") + if item["maven"]: + g = item["maven"].get("groupId", "") + a = item["maven"].get("artifactId", "") + v = item["maven"].get("version", "") + lines.append(f" Maven: {g}:{a}:{v}" if g else f" Maven version: {v}") + elif item["manifest"]: + title = item["manifest"].get("Implementation-Title", item["manifest"].get("Bundle-Name", "")) + version = item["manifest"].get("Implementation-Version", item["manifest"].get("Bundle-Version", "")) + if title or version: + lines.append(f" Manifest: {title} {version}".strip()) + if len(found) > 50: + lines.append(f"\n[... {len(found) - 50} more archives truncated ...]") + return "\n".join(lines) + + yield FunctionInfo.from_fn(find_java_packages, description="Find Java JARs and extract version metadata") + + +# --- 10. list_installed_packages --- + + +class ListInstalledPackagesConfig(FunctionBaseConfig, name="list_installed_packages"): + pass + + +@register_function(config_type=ListInstalledPackagesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def list_installed_packages_tool(config: ListInstalledPackagesConfig, builder: Builder): + async def list_installed_packages(scope: str = "all") -> str: + """Return a summary of all package ecosystems present in the container: dpkg, RPM, pip, npm, Java JARs -- with counts and locations. Use this first when unsure what package managers are in use. The scope parameter is optional (default: 'all').""" + logger.info("list_installed_packages") + fs = get_filesystem_root() + if fs is None: + return "Error: Filesystem root not configured." + sections: list[str] = [] + dpkg_status = fs / "var" / "lib" / "dpkg" / "status" + if dpkg_status.exists(): + try: + content = dpkg_status.read_text(encoding="utf-8", errors="ignore") + count = content.count("\nPackage: ") + sections.append(f"dpkg (Debian/Ubuntu): ~{count} packages installed") + sections.append(" → Use query_dpkg_database(package_pattern) for specific packages") + except OSError: + sections.append("dpkg: database found but unreadable") + for rpm_path in [fs / "var" / "lib" / "rpm", fs / "usr" / "lib" / "sysimage" / "rpm"]: + if rpm_path.exists(): + sections.append(f"RPM: database at /{rpm_path.relative_to(fs)}") + sections.append(" → Use query_rpm_database(package_pattern) for specific packages") + break + pip_count = 0 + envs: list[str] = [] + try: + for site_pkg in fs.rglob("site-packages"): + if site_pkg.is_dir(): + count = sum(1 for _ in site_pkg.glob("*.dist-info/METADATA")) + if count > 0: + try: + label = "/" + str(site_pkg.relative_to(fs)) + except ValueError: + label = str(site_pkg) + envs.append(f" {label} ({count} packages)") + pip_count += count + except PermissionError: + pass + if envs: + sections.append(f"Python pip: {pip_count} packages across {len(envs)} environment(s)") + sections.extend(envs[:10]) + sections.append(" → Use list_python_packages(search_path) for full list") + npm_locs: list[str] = [] + try: + for node_mod in fs.rglob("node_modules"): + if node_mod.is_dir() and "node_modules" not in str(node_mod.parent.relative_to(fs)): + count = sum(1 for d in node_mod.iterdir() if d.is_dir()) + try: + label = "/" + str(node_mod.relative_to(fs)) + except ValueError: + label = str(node_mod) + npm_locs.append(f" {label} ({count} packages)") + except PermissionError: + pass + if npm_locs: + sections.append(f"npm: {len(npm_locs)} node_modules location(s)") + sections.extend(npm_locs[:5]) + sections.append(" → Use query_npm_packages(package_pattern) for specific packages") + if not sections: + sections.append("No known package managers (dpkg/rpm/pip/npm/jar) detected.") + return "\n".join(sections) + + yield FunctionInfo.from_fn(list_installed_packages, description="Summary of all package ecosystems in container") diff --git a/src/vuln_analysis/tools/serp.py b/src/vuln_analysis/tools/serp.py deleted file mode 100644 index 2ac228459..000000000 --- a/src/vuln_analysis/tools/serp.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from nat.builder.builder import Builder -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.function import FunctionBaseConfig - -logger = logging.getLogger(__name__) - - -class SerpWrapperToolConfig(FunctionBaseConfig, name="serp_wrapper"): - """ - SerpApi Google search tool - """ - max_retries: int - - -@register_function(config_type=SerpWrapperToolConfig) -async def serp_wrapper(config: SerpWrapperToolConfig, builder: Builder): # pylint: disable=unused-argument - from vuln_analysis.utils.serp_api_wrapper import MorpheusSerpAPIWrapper - - search = MorpheusSerpAPIWrapper(max_retries=config.max_retries) - - async def _arun(query: str) -> str: - result = await search.arun(query) - return result - - yield FunctionInfo.from_fn(_arun, - description=("Useful for when you need to answer questions about external libraries")) diff --git a/src/vuln_analysis/tools/web_tools.py b/src/vuln_analysis/tools/web_tools.py new file mode 100644 index 000000000..97768c740 --- /dev/null +++ b/src/vuln_analysis/tools/web_tools.py @@ -0,0 +1,186 @@ +"""Web search and web fetch tools for NAT registration (Tavily).""" + +import asyncio +import logging +import re +from typing import Literal +from urllib.parse import urldefrag + +import httpx +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig +from pydantic import Field + +logger = logging.getLogger(__name__) + + +def _tavily_client_from_env(): + import os + + from tavily import TavilyClient + + api_key = os.getenv("TAVILY_API_KEY") + if not api_key: + raise ValueError("TAVILY_API_KEY is not set") + base_url = (os.getenv("TAVILY_API_BASE_URL") or "").strip() or None + return TavilyClient(api_key=api_key, api_base_url=base_url) + + +_LOGIN_SIGNALS = [ + "sign in", + "log in", + "login", + "authentication required", + "you must be signed in", +] + + +def _looks_like_login_page(content: str) -> bool: + if len(content) > 2000: + return False + lower = content[:2000].lower() + return any(s in lower for s in _LOGIN_SIGNALS) + + +def _strip_html_basic(html: str) -> str: + html = re.sub(r"<(script|style)[^>]*>.*?", "", html, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r"<[^>]+>", " ", html) + text = re.sub(r"[ \t]+", " ", text) + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip() + + +class WebSearchToolConfig(FunctionBaseConfig, name="web_search"): + max_results: int = Field(default=5, ge=1, le=20) + search_depth: Literal["basic", "advanced", "fast", "ultra-fast"] = Field( + default="basic", + description="Tavily search_depth (latency vs relevance).", + ) + max_retries: int = Field(default=3, ge=1, le=10) + + +@register_function(config_type=WebSearchToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def web_search_tool(config: WebSearchToolConfig, builder: Builder): + + async def _web_search(question: str) -> str: + """Search the web using Tavily and return snippets for the given question.""" + if len(question) > 400: + question = question[:397] + "..." + + import os + + if not os.environ.get("TAVILY_API_KEY"): + return "No web search: TAVILY_API_KEY is not set." + + last_exc: Exception | None = None + client = _tavily_client_from_env() + for attempt in range(config.max_retries): + try: + search_docs = await asyncio.to_thread( + client.search, + question, + search_depth=config.search_depth, + max_results=config.max_results, + ) + if not isinstance(search_docs, dict) or "results" not in search_docs: + return f"No web search results found for: {question}" + if not search_docs["results"]: + return f"No web search results found for: {question}" + return "\n\n---\n\n".join(f'\n{doc["content"]}\n' + for doc in search_docs["results"]) + except Exception as exc: + last_exc = exc + logger.warning("Tavily search failed (attempt %s): %s", attempt + 1, exc) + if attempt < config.max_retries - 1: + await asyncio.sleep(2**attempt) + + return (f"Web search failed after {config.max_retries} attempts for: {question}" + f" ({last_exc})") + + yield FunctionInfo.from_fn(_web_search, description=_web_search.__doc__) + + +class WebFetchToolConfig(FunctionBaseConfig, name="web_fetch"): + timeout: int = Field(default=30) + max_content_length: int = Field(default=50000) + + +@register_function(config_type=WebFetchToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def web_fetch_tool(config: WebFetchToolConfig, builder: Builder): + + def _web_fetch_inner(url: str, max_content_length: int, timeout: int) -> str: + logger.info("web_fetch: %s", url) + + try: + import os + + if os.getenv("TAVILY_API_KEY"): + client = _tavily_client_from_env() + response = client.extract(urls=[url]) + results = response.get("results", []) + if results and results[0].get("raw_content"): + content = results[0]["raw_content"] + if len(content) > max_content_length: + content = content[:max_content_length] + "\n\n[Content truncated]" + if _looks_like_login_page(content): + content = ("[WARNING: This page appears to require authentication. " + "The content below is likely a login page, not the actual " + "page content. Use web_search for this URL instead.]\n\n") + content + return content + except Exception as exc: + logger.warning("Tavily extract failed for %s: %s – falling back to httpx", url, exc) + + try: + with httpx.Client( + timeout=timeout, + follow_redirects=True, + headers={ + "User-Agent": ("Mozilla/5.0 (compatible; CVEAgent/3.0; " + "+https://github.com/NVIDIA-AI-Blueprints/vulnerability-analysis)") + }, + ) as http_client: + resp = http_client.get(url) + resp.raise_for_status() + + content_type = resp.headers.get("content-type", "") + raw = resp.text + + if "html" in content_type.lower(): + raw = _strip_html_basic(raw) + + if len(raw) > max_content_length: + raw = raw[:max_content_length] + "\n\n[Content truncated]" + + if not raw.strip(): + return f"Page at {url} returned empty content." + + if _looks_like_login_page(raw): + raw = ("[WARNING: This page appears to require authentication. " + "The content below is likely a login page, not the actual " + "page content. Use web_search for this URL instead.]\n\n") + raw + return raw + + except Exception as exc: + error_msg = f"Failed to fetch {url}: {exc}" + logger.error(error_msg) + return error_msg + + async def _arun(url: str) -> str: + stripped_url, fragment = urldefrag(url) + had_fragment = bool(fragment) + if had_fragment: + logger.info("web_fetch: stripping fragment #%s from URL", fragment) + + content = await asyncio.to_thread(_web_fetch_inner, stripped_url, config.max_content_length, config.timeout) + + if had_fragment: + content += ("\n\n[Note: URL fragment '#" + fragment + "' was ignored" + " -- web_fetch always returns the page from the beginning." + " If the content you need was not returned, try a more" + " targeted web_search instead.]") + return content + + yield FunctionInfo.from_fn(_arun, description="Fetch and extract content from a URL") diff --git a/src/vuln_analysis/utils/agentic_loop.py b/src/vuln_analysis/utils/agentic_loop.py new file mode 100644 index 000000000..46996bee2 --- /dev/null +++ b/src/vuln_analysis/utils/agentic_loop.py @@ -0,0 +1,270 @@ +"""Shared agentic tool-calling loop for all CVE pipeline agents. + +Adapted from cve-agent-v3's BaseAgent._run_agentic_loop to work with +LangChain BaseChatModel (obtained from NAT builder.get_llm()) instead +of AsyncOpenAI directly. +""" + +from __future__ import annotations + +import asyncio +import logging +from collections import Counter +from urllib.parse import urldefrag + +from aiolimiter import AsyncLimiter +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage +from langchain_core.tools import BaseTool + +from vuln_analysis.utils.helpers import estimate_message_tokens, truncate + +logger = logging.getLogger(__name__) + +_RETRY_ATTEMPTS = 3 +_RETRY_BASE_DELAY = 2.0 +_GATEWAY_TIMEOUT_BASE_DELAY = 10.0 + + +def _tool_call_key(name: str, args: dict) -> str: + normalized = dict(args) + if "url" in normalized and isinstance(normalized["url"], str): + normalized["url"], _ = urldefrag(normalized["url"]) + import json + return name + ":" + json.dumps(normalized, sort_keys=True) + + +def _compact_context( + messages: list[BaseMessage], + context_window_tokens: int, + *, + threshold: float = 0.80, + keep_recent: int = 6, + compact_chars: int = 300, +) -> None: + """Shrink old tool/assistant results when context nears the window limit.""" + before = estimate_message_tokens(messages) + limit = int(context_window_tokens * threshold) + if before <= limit: + return + + tool_indices = [i for i, m in enumerate(messages) if isinstance(m, ToolMessage)] + if len(tool_indices) > keep_recent: + for idx in tool_indices[:-keep_recent]: + content = messages[idx].content or "" + if len(content) > compact_chars: + messages[idx] = ToolMessage( + content=content[:compact_chars] + f"\n\n[... compacted from {len(content):,} to {compact_chars} chars.]", + tool_call_id=messages[idx].tool_call_id, + ) + + after = estimate_message_tokens(messages) + if after <= limit: + logger.warning("Context compacted (tool results): %d → %d est. tokens (limit %d)", before, after, limit) + return + + ai_indices = [i for i, m in enumerate(messages) if isinstance(m, AIMessage) and m.content] + if len(ai_indices) > keep_recent: + for idx in ai_indices[:-keep_recent]: + content = messages[idx].content or "" + if len(content) > compact_chars: + messages[idx] = AIMessage(content=content[:compact_chars] + f"\n\n[... compacted from {len(content):,} to {compact_chars} chars.]") + + after = estimate_message_tokens(messages) + logger.warning("Context compacted (tool + assistant): %d → %d est. tokens (limit %d)", before, after, limit) + + +def _deadline_nudge(iteration: int, max_iterations: int, *, fraction: float = 0.80) -> str | None: + if iteration == int(max_iterations * fraction): + return ( + "You are approaching your iteration limit. " + "Stop making tool calls and produce your complete " + "report NOW using the information you have already gathered." + ) + return None + + +async def _call_llm_with_retry( + llm: BaseChatModel, + messages: list[BaseMessage], + tools: list[BaseTool], + rate_limiter: AsyncLimiter | None = None, +) -> AIMessage: + """Call the LLM with retry on transient errors.""" + bound = llm.bind_tools([t for t in tools]) if tools else llm + last_exc: Exception | None = None + for attempt in range(1, _RETRY_ATTEMPTS + 1): + try: + if rate_limiter is not None: + async with rate_limiter: + return await bound.ainvoke(messages) + return await bound.ainvoke(messages) + except Exception as exc: + last_exc = exc + status = getattr(exc, "status_code", None) + retryable = status in {429, 500, 502, 503, 504} or status is None + if not retryable: + raise + if attempt < _RETRY_ATTEMPTS: + base = _GATEWAY_TIMEOUT_BASE_DELAY if status == 504 else _RETRY_BASE_DELAY + delay = base * (2 ** (attempt - 1)) + logger.warning( + "LLM call failed (attempt %d/%d, status=%s): %s — retrying in %.1fs", + attempt, _RETRY_ATTEMPTS, status, exc, delay, + ) + await asyncio.sleep(delay) + else: + raise + raise last_exc + + +async def _execute_tool_call( + tool_call: dict, + tools: list[BaseTool], + *, + loop_counter: Counter[str] | None = None, + loop_threshold: int = 2, + phase_label: str = "", +) -> ToolMessage: + """Execute a single tool call and return a ToolMessage.""" + name = tool_call["name"] + args = tool_call["args"] + tc_id = tool_call["id"] + label = f"{phase_label} tool" if phase_label else "Tool" + + if loop_counter is not None: + call_key = _tool_call_key(name, args) + loop_counter[call_key] += 1 + count = loop_counter[call_key] + if loop_threshold and count > loop_threshold: + logger.warning("Loop detected: %s called %d times — skipping", name, count) + return ToolMessage( + content=( + f"LOOP DETECTED: You have already called {name} with " + f"these exact arguments {count} times. Do NOT repeat this call. " + f"Move on or produce the report now." + ), + tool_call_id=tc_id, + ) + + logger.info("%s: %s(%s)", label, name, truncate(str(args), 200)) + + tool_fn = None + for t in tools: + if t.name == name: + tool_fn = t + break + if tool_fn is None: + normalized = name.replace("-", "_") + for t in tools: + if t.name == normalized: + tool_fn = t + break + + if tool_fn is None: + return ToolMessage(content=f"Unknown tool: {name}", tool_call_id=tc_id) + + try: + result = await tool_fn.ainvoke(args) + if not isinstance(result, str): + result = str(result) + except Exception as exc: + result = f"Tool error: {exc}" + + logger.info("%s %s completed (%s)", label, name, truncate(result, 120)) + return ToolMessage(content=result, tool_call_id=tc_id) + + +async def run_agentic_loop( + llm: BaseChatModel, + messages: list[BaseMessage], + tools: list[BaseTool], + *, + max_iterations: int, + context_window_tokens: int, + wrap_up_fraction: float | None = None, + loop_threshold: int = 2, + phase_label: str = "", + rate_limiter: AsyncLimiter | None = None, +) -> tuple[str, int, int]: + """Run the tool-calling loop. Returns (final_content, tool_calls, llm_calls).""" + tool_call_count = 0 + llm_call_count = 0 + loop_counter: Counter[str] = Counter() + label = phase_label or "Agent" + + step_mgr = None + try: + from nat.builder import SyncBuilder + ctx = SyncBuilder.current().context + step_mgr = ctx.intermediate_step_manager + except Exception: + pass + + for iteration in range(1, max_iterations + 1): + logger.info("%s — iteration %d", label, iteration) + _compact_context(messages, context_window_tokens) + + llm_call_count += 1 + ai_message = await _call_llm_with_retry(llm, messages, tools, rate_limiter) + messages.append(ai_message) + + if step_mgr: + try: + step_mgr.add_step( + agent_action=f"[{phase_label}] LLM call #{iteration}", + observation=truncate(ai_message.content or "(tool calls)", 500), + ) + except Exception: + pass + + if ai_message.tool_calls: + tool_results = await asyncio.gather(*[ + _execute_tool_call( + tc, tools, + loop_counter=loop_counter, + loop_threshold=loop_threshold, + phase_label=phase_label, + ) + for tc in ai_message.tool_calls + ]) + tool_call_count += len(tool_results) + + for tm in tool_results: + messages.append(tm) + if step_mgr: + try: + step_mgr.add_step( + agent_action=f"[{phase_label}] tool result", + observation=truncate(tm.content or "", 500), + ) + except Exception: + pass + + if wrap_up_fraction is not None: + nudge = _deadline_nudge(iteration, max_iterations, fraction=wrap_up_fraction) + if nudge: + logger.info("Injecting wrap-up nudge at iteration %d", iteration) + messages.append(HumanMessage(content=nudge)) + else: + content = (ai_message.content or "").strip() + if not content: + logger.warning("%s LLM returned empty content — nudging.", label) + messages.append(HumanMessage(content="Your previous response was empty. Please produce your report now.")) + continue + logger.info("%s finished (no more tool calls).", label) + break + + if getattr(ai_message, "response_metadata", {}).get("finish_reason") == "stop": + logger.info("%s finished (stop).", label) + break + else: + logger.warning("%s reached max iterations (%d) without finishing.", label, max_iterations) + + final_content = "" + for msg in reversed(messages): + if isinstance(msg, AIMessage) and msg.content and msg.content.strip(): + final_content = msg.content + break + + return final_content, tool_call_count, llm_call_count diff --git a/src/vuln_analysis/utils/checklist_prompt_generator.py b/src/vuln_analysis/utils/checklist_prompt_generator.py deleted file mode 100644 index 9807bf4d4..000000000 --- a/src/vuln_analysis/utils/checklist_prompt_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import ast -import logging - -from jinja2 import Template -from langchain_core.language_models.base import BaseLanguageModel - -from vuln_analysis.utils.prompting import MOD_FEW_SHOT -from vuln_analysis.utils.prompting import additional_intel_prompting -from vuln_analysis.utils.prompting import get_mod_examples -from vuln_analysis.utils.string_utils import attempt_fix_list_string - -logger = logging.getLogger(__name__) - -DEFAULT_CHECKLIST_PROMPT = MOD_FEW_SHOT.format(examples=get_mod_examples()) - -cve_prompt2 = """Parse the following numbered checklist into a python list in the format ["x", "y", "z"], a comma separated list surrounded by square braces: {{template}}""" - - -async def _parse_list(text: list[str]) -> list[list[str]]: - """ - Asynchronously parse a list of strings, each representing a list, into a list of lists. - - Parameters - ---------- - text : list of str - A list of strings, each intended to be parsed into a list. - - Returns - ------- - list of lists of str - A list of lists, parsed from the input strings. - - Raises - ------ - ValueError - If the string cannot be parsed into a list or if the parsed object is not a list. - - Notes - ----- - This function tries to fix strings that represent lists with unescaped quotes by calling - `attempt_fix_list_string` and then uses `ast.literal_eval` for safe parsing of the string into a list. - It ensures that each element of the parsed list is actually a list and will raise an error if not. - """ - return_val = [] - - for checklist_num, x in enumerate(text): - try: - # Remove any text not enclosed by square brackets - x = x[x.find('['):x.rfind(']') + 1] - - # Remove newline characters that can cause incorrect string escaping in the next step - x = x.replace("\n", "") - - # Ensure backslashes are escaped - x = x.replace("\\", "\\\\") - - # Try to do some very basic string cleanup to fix unescaped quotes - x = attempt_fix_list_string(x) - - # Only proceed if the input is a valid Python literal - # This isn't really dangerous, literal_eval only evaluates a small subset of python - current = ast.literal_eval(x) - - # Ensure that the parsed data is a list - if not isinstance(current, list): - raise ValueError(f"Input is not a list: {x}") - - # Process the list items - for i in range(len(current)): - if (isinstance(current[i], list) and len(current[i]) == 1): - current[i] = current[i][0] - - return_val.append(current) - except (ValueError, SyntaxError) as e: - # Handle the error, log it, or re-raise it with additional context - raise ValueError(f"Failed to parse input for checklist number {checklist_num}: {x}. Error: {e}") - - return return_val - - -async def format_jinja_prompt(template_str, input_dict): - - _template_jinja = Template(template_str, enable_async=True, trim_blocks=True, lstrip_blocks=True) - - output_list = await _template_jinja.render_async(input_dict) - - return output_list - - -async def generate_checklist(prompt: str | None, - llm: BaseLanguageModel, - input_dict: dict, - enable_llm_list_parsing: bool = False) -> str: - - if not prompt: - prompt = DEFAULT_CHECKLIST_PROMPT - - intel = ( - additional_intel_prompting + - "\n\nIf a vulnerable function or method is mentioned in the CVE description, ensure the first checklist item verifies whether this function or method is being called from the code or used by the code." - "\nThe vulnerable version of the vulnerable package is already verified to be installed within the container. Check only the other factors that affect exploitability, no need to verify version again." - ) - - cve_prompt1 = (prompt + intel) - try: - format_cve_intel = await format_jinja_prompt(cve_prompt1, input_dict) - - gen_checklist = await llm.ainvoke(format_cve_intel) - - if enable_llm_list_parsing: - parsing_checklist_template = await format_jinja_prompt(cve_prompt2, {"template": gen_checklist.content}) - parsed_checklist = await llm.ainvoke(parsing_checklist_template) - return parsed_checklist.content - - except Exception as e: - logger.error("Error in generating checklist: %s: %s", type(e).__name__, e) - raise - - return gen_checklist.content diff --git a/src/vuln_analysis/utils/clients/first_client.py b/src/vuln_analysis/utils/clients/first_client.py deleted file mode 100644 index c4bfe4a49..000000000 --- a/src/vuln_analysis/utils/clients/first_client.py +++ /dev/null @@ -1,72 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os - -import aiohttp - -from vuln_analysis.data_models.cve_intel import CveIntelEpss -from vuln_analysis.utils.clients.intel_client import IntelClient -from vuln_analysis.utils.url_utils import url_join - -logger = logging.getLogger(__name__) - - -class FirstClient(IntelClient): - """ - Async client for First EPSS API - """ - - def __init__(self, - *, - base_url: str | None = None, - session: aiohttp.ClientSession | None = None, - retry_count: int = 10, - sleep_time: float = 0.1, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True): - - super().__init__(session=session, - base_url=base_url or os.environ.get('FIRST_BASE_URL'), - retry_count=retry_count, - sleep_time=sleep_time, - respect_retry_after_header=respect_retry_after_header, - retry_on_client_errors=retry_on_client_errors) - - self._headers = {'Accept': 'application/json'} - - @classmethod - def default_base_url(cls) -> str: - return "https://api.first.org/data/v1" - - async def get_intel_dict(self, cve_id: str, date: None | str) -> dict: - params = {'cve': cve_id, 'date': date} if date else {'cve': cve_id} - response_json = await self.request(method='GET', - url=url_join(self.base_url, "epss"), - params=params, - headers=self._headers) - - data = response_json.get('data', []) - - if (len(data) > 0): - return data[0] - - return {} - - async def get_intel(self, cve_id: str, date: None | str = None) -> CveIntelEpss: - intel_dict = await self.get_intel_dict(cve_id, date) - - return CveIntelEpss.model_validate(intel_dict) diff --git a/src/vuln_analysis/utils/clients/ghsa_client.py b/src/vuln_analysis/utils/clients/ghsa_client.py deleted file mode 100644 index 0cbfa8924..000000000 --- a/src/vuln_analysis/utils/clients/ghsa_client.py +++ /dev/null @@ -1,95 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os - -import aiohttp - -from vuln_analysis.data_models.cve_intel import CveIntelGhsa -from vuln_analysis.utils.clients.intel_client import IntelClient -from vuln_analysis.utils.url_utils import url_join - -logger = logging.getLogger(__name__) - - -class GHSAClient(IntelClient): - """ - Async client for GitHub Security Advisory API - - While not strictly required, obtaining an API key is recommended, to obtain one refer: - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens - """ - - def __init__(self, - *, - base_url: str | None = None, - api_key: str | None = None, - session: aiohttp.ClientSession | None = None, - retry_count: int = 10, - sleep_time: float = 0.1, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True): - super().__init__(session=session, - base_url=base_url or os.environ.get('GHSA_BASE_URL'), - retry_count=retry_count, - sleep_time=sleep_time, - respect_retry_after_header=respect_retry_after_header, - retry_on_client_errors=retry_on_client_errors, - api_key_env_var='GHSA_API_KEY') - - self._api_key = api_key or os.environ.get('GHSA_API_KEY', None) - - self._headers = {'Accept': 'application/vnd.github+json', "X-GitHub-Api-Version": "2022-11-28"} - - if self._api_key is not None: - self._headers['Authorization'] = f"Bearer {self._api_key}" - - @classmethod - def default_base_url(cls) -> str: - return "https://api.github.com" - - async def get_intel_dict(self, vuln_id: str) -> dict: - - params = {} - - if (vuln_id.startswith("GHSA-")): - params["ghsa_id"] = vuln_id - else: - params["cve_id"] = vuln_id - - response_json = await self.request(method="GET", - url=url_join(self.base_url, "advisories"), - params=params, - headers=self._headers) - - if (isinstance(response_json, list)): - - if (len(response_json) != 1): - raise ValueError(f"Expected 1 GHSA entry for {vuln_id}, got {len(response_json)}") - - return response_json[0] - - elif (isinstance(response_json, dict)): - - return response_json - - raise ValueError(f"Unexpected response type {type(response_json)}") - - async def get_intel(self, vuln_id: str) -> CveIntelGhsa: - - intel_dict = await self.get_intel_dict(vuln_id) - - return CveIntelGhsa.model_validate(intel_dict) diff --git a/src/vuln_analysis/utils/clients/intel_client.py b/src/vuln_analysis/utils/clients/intel_client.py deleted file mode 100644 index d0bbbf004..000000000 --- a/src/vuln_analysis/utils/clients/intel_client.py +++ /dev/null @@ -1,90 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing -from abc import abstractmethod - -import aiohttp - -from vuln_analysis.utils.async_http_utils import request_with_retry - - -class IntelClient: - - def __init__(self, - *, - session: aiohttp.ClientSession | None, - base_url: str | None = None, - retry_count: int = 10, - sleep_time: float = 0.1, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True, - api_key_env_var: str | None = None): - - if (session is None): - session = aiohttp.ClientSession() - - if (base_url is None): - base_url = self.default_base_url() - - self._session = session - - self._base_url = base_url - self._retry_count = retry_count - self._sleep_time = sleep_time - self._respect_retry_after_header = respect_retry_after_header - self._retry_on_client_errors = retry_on_client_errors - self._api_key_env_var = api_key_env_var - - @classmethod - @abstractmethod - def default_base_url(cls) -> str: - raise NotImplementedError - - @property - def base_url(self) -> str: - return self._base_url - - def is_default_base_url(self) -> bool: - """ - Check if the base URL is the default one - - Returns - ------- - bool - True if the base URL is the default one - """ - return self.base_url == self.default_base_url() - - async def request(self, - method: typing.Literal["GET", "POST", "PUT", "PATCH"], - url: str, - params: dict[str, str] | None = None, - headers: dict[str, str] | None = None, - log_on_error=True, - **kwargs): - - async with request_with_retry(session=self._session, - request_kwargs={ - 'method': method, 'url': url, "params": params, "headers": headers, **kwargs - }, - max_retries=self._retry_count, - sleep_time=self._sleep_time, - respect_retry_after_header=self._respect_retry_after_header, - log_on_error=log_on_error, - retry_on_client_errors=self._retry_on_client_errors, - api_key_env_var=self._api_key_env_var) as response: - - return await response.json() diff --git a/src/vuln_analysis/utils/clients/nvd_client.py b/src/vuln_analysis/utils/clients/nvd_client.py deleted file mode 100644 index ad2c2d761..000000000 --- a/src/vuln_analysis/utils/clients/nvd_client.py +++ /dev/null @@ -1,344 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -import re - -import aiohttp -from bs4 import BeautifulSoup - -from vuln_analysis.data_models.cve_intel import CveIntelNvd -from vuln_analysis.utils.async_http_utils import request_with_retry -from vuln_analysis.utils.clients.intel_client import IntelClient -from vuln_analysis.utils.intel_utils import parse -from vuln_analysis.utils.intel_utils import parse_config_vendors -from vuln_analysis.utils.url_utils import url_join - -logger = logging.getLogger(__name__) - - -class NVDClient(IntelClient): - """ - Async client for NIST's NVD API - - While not strictly required, obtaining an API key is recommended, to obtain one refer: - https://nvd.nist.gov/developers/start-here - """ - - CWE_NAME_RE = re.compile(r'^.*?-\s*') - CWE_DETAILS_URL = "http://cwe.mitre.org" - - def __init__(self, - *, - base_url: str | None = None, - api_key: str | None = None, - lang_code: str = 'en', - session: aiohttp.ClientSession | None = None, - retry_count: int = 10, - sleep_time: float = 0.1, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True): - - super().__init__(session=session, - base_url=base_url or os.environ.get('NVD_BASE_URL'), - retry_count=retry_count, - sleep_time=sleep_time, - respect_retry_after_header=respect_retry_after_header, - retry_on_client_errors=retry_on_client_errors, - api_key_env_var='NVD_API_KEY') - - self._api_key = api_key or os.environ.get('NVD_API_KEY', None) - - self._cwe_details_url_template = url_join(os.environ.get('CWE_DETAILS_BASE_URL', self.CWE_DETAILS_URL), - "data/definitions", - "{CWE_ID}.html") - - self._lang_code = lang_code - - self._headers = {'Content-Type': 'application/json'} - - if self._api_key is not None: - self._headers['apiKey'] = self._api_key - - @classmethod - def default_base_url(cls) -> str: - return "https://services.nvd.nist.gov/rest" - - async def _get_soup(self, url: str) -> BeautifulSoup: - - async with request_with_retry(session=self._session, - request_kwargs={ - 'method': 'GET', - 'url': url, - "skip_auto_headers": {"User-Agent"}, - }, - max_retries=self._retry_count, - retry_on_client_errors=self._retry_on_client_errors, - api_key_env_var=self._api_key_env_var) as response: - return BeautifulSoup(await response.text(), 'html.parser') - - def _get_cvss_vector_from_metric(self, metrics: dict, metric_version: str) -> str | None: - versioned_metrics = metrics[metric_version] - for metric_type in ('primary', 'secondary'): - for metric in versioned_metrics: - if metric['type'].lower() == metric_type: - return metric['cvssData']['vectorString'] - - def _get_cvss_base_score_from_metric(self, metrics: dict, metric_version: str) -> float | None: - versioned_metrics = metrics[metric_version] - for metric_type in ('primary', 'secondary'): - for metric in versioned_metrics: - if metric['type'].lower() == metric_type: - return metric['cvssData']['baseScore'] - - def _get_cvss_base_severity_from_metric(self, metrics: dict, metric_version: str) -> str | None: - versioned_metrics = metrics[metric_version] - for metric_type in ('primary', 'secondary'): - for metric in versioned_metrics: - if metric['type'].lower() == metric_type: - return metric['cvssData']['baseSeverity'] - - def _get_cwe(self, cwes: list[dict]) -> str | None: - for cwe_type in ('primary', 'secondary'): - for cwe in cwes: - if cwe['type'].lower() == cwe_type: - cwe_descriptions = cwe['description'] - for cwe_description in cwe_descriptions: - if cwe_description['lang'] == self._lang_code: - return cwe_description['value'] - - async def _get_cwe_elements(self, cve_obj: dict) -> dict: - """ - Asynchronously extract CWE (Common Weakness Enumeration) elements from the CVE object, and retreive details for - those CWEs. - """ - # Get CWE name - cwe_id = None - weaknesses = cve_obj.get('weaknesses', []) - cwe_id = self._get_cwe(weaknesses) - cwe_link = None - cwe_name = None - cwe_description = None - cwe_extended_description = None - if cwe_id is not None: - if cwe_id.startswith('CWE-'): - cwe_id = cwe_id.replace('CWE-', '', 1) - - if cwe_id.isnumeric(): - cwe_link = self._cwe_details_url_template.format(CWE_ID=cwe_id) - - if cwe_link is not None: - soup = await self._get_soup(cwe_link) - - if soup is not None: - title_tag = soup.find('title') - if title_tag: - cwe_name = title_tag.string.strip() - cwe_name = self.CWE_NAME_RE.sub('', cwe_name).strip() - description_div = soup.find('div', id='Description') - if description_div: - cwe_description = description_div.find('div', class_='indent').text.strip() - - extended_description_div = soup.find('div', id='Extended_Description') - if extended_description_div: - cwe_extended_description = extended_description_div.find('div', class_='indent').text.strip() - - return { - "cwe_name": cwe_name, - "cwe_description": cwe_description, - "cwe_extended_description": cwe_extended_description, - } - - def _parse_nvd_cvss_vector(self, cve_obj: dict) -> str | None: - """ - Extract the CVSS vector from a CVE json object. - - Parameters - ---------- - cve_obj : dict - The cve sub-dictionary of the JSON document. - - Returns - ------- - str or None - The CVSS vector string, if found. Otherwise, None. - """ - # metrics is optional https://nvd.nist.gov/developers/vulnerabilities - metrics = cve_obj.get('metrics') - if metrics is not None: - # Attempt to find the CVSS vector in order of preference - for metric_version in ('cvssMetricV31', 'cvssMetricV30'): - try: - cvss_vector = self._get_cvss_vector_from_metric(metrics, metric_version) - if cvss_vector is not None: - return cvss_vector - except KeyError: - continue - - def _parse_nvd_cvss_base_score(self, cve_obj: dict) -> float | None: - """ - Extract the CVSS baseScore from a CVE json object. - - Parameters - ---------- - cve_obj : dict - The cve sub-dictionary of the JSON document. - - Returns - ------- - float or None - The CVSS baseScore, if found. Otherwise, None. - """ - # metrics is optional https://nvd.nist.gov/developers/vulnerabilities - metrics = cve_obj.get('metrics') - if metrics is not None: - # Attempt to find the CVSS vector in order of preference - for metric_version in ('cvssMetricV31', 'cvssMetricV30'): - try: - cvss_base_score = self._get_cvss_base_score_from_metric(metrics, metric_version) - if cvss_base_score is not None: - return cvss_base_score - except KeyError: - continue - - def _parse_nvd_cvss_base_severity(self, cve_obj: dict) -> str | None: - """ - Extract the CVSS vector from a CVE json object. - - Parameters - ---------- - cve_obj : dict - The cve sub-dictionary of the JSON document. - - Returns - ------- - str or None - The CVSS base severity string, if found. Otherwise, None. - """ - # metrics is optional https://nvd.nist.gov/developers/vulnerabilities - metrics = cve_obj.get('metrics') - if metrics is not None: - # Attempt to find the CVSS vector in order of preference - for metric_version in ('cvssMetricV31', 'cvssMetricV30'): - try: - cvss_vector = self._get_cvss_base_severity_from_metric(metrics, metric_version) - if cvss_vector is not None: - return cvss_vector - except KeyError: - continue - - def _parse_references(self, cve_obj: dict) -> list[str]: - """ - Extract cve reference links from the CVE json object - - Parameters - ---------- - cve_obj : dict - The cve sub-dictionary of the JSON document. - - Returns - ------- - list[str] - The CVE references as a list - """ - # references is required https://nvd.nist.gov/developers/vulnerabilities - references = cve_obj.get('references') - return [reference["url"] for reference in references] - - def _parse_disputed_status(self, cve_obj: dict) -> bool: - """ - Return disputed status of the cve - - Parameters - ---------- - cve_obj : dict - The cve sub-dictionary of the JSON document. - - Returns - ------- - bool - returns true if 'disputed' tag exists - """ - # cveTags is optional https://nvd.nist.gov/developers/vulnerabilities - cve_tags = cve_obj.get('cveTags') - if cve_tags: - for tag_entry in cve_tags: - tags = tag_entry.get("tags", []) - return "disputed" in tags - return False - - async def get_intel_dict(self, cve_id: str) -> dict: - - response = await self.request(method="GET", - url=url_join(self.base_url, 'json/cves/2.0'), - params={'cveId': cve_id}, - headers=self._headers) - - # Get the vulnerabilities from the dict - vulns = response.get("vulnerabilities", []) - - if (len(vulns) == 0): - raise ValueError(f"Could not find CVE entry for {cve_id}") - - if (len(vulns) > 1): - logger.warning(f"Found multiple CVE entries for {cve_id}, using the first one") - - return vulns[0] - - async def get_intel(self, cve_id: str) -> CveIntelNvd: - """ - Get the CVE Intel object for the given CVE ID - - Args: - cve_id (str): The CVE ID to get the Intel for - - Returns: - CveIntelNvd: The CVE Intel object - """ - intel_dict = await self.get_intel_dict(cve_id) - - cve_vuln: dict = intel_dict.get("cve", {}) - - cve_description = "\n".join( - desc.get('value', '') for desc in cve_vuln.get('descriptions', []) if desc.get('lang') == self._lang_code) - - cve_configurations = parse(cve_vuln.get('configurations', [])) - cvss_vector = self._parse_nvd_cvss_vector(cve_vuln) - cvss_base_score = self._parse_nvd_cvss_base_score(cve_vuln) - cvss_severity = self._parse_nvd_cvss_base_severity(cve_vuln) - cwe_elements = await self._get_cwe_elements(cve_vuln) - vendor_names = parse_config_vendors(cve_vuln.get('configurations', [])) - references = self._parse_references(cve_vuln) - disputed = self._parse_disputed_status(cve_vuln) - published_at = cve_vuln.get('published') # required field - updated_at = cve_vuln.get('lastModified') # required field - - intel = CveIntelNvd(cve_id=cve_id, - cve_description=cve_description, - cvss_vector=cvss_vector, - cvss_base_score=cvss_base_score, - cvss_severity=cvss_severity, - cwe_name=cwe_elements["cwe_name"], - cwe_description=cwe_elements["cwe_description"], - cwe_extended_description=cwe_elements["cwe_extended_description"], - configurations=cve_configurations, - vendor_names=vendor_names, - references=references, - disputed=disputed, - published_at=published_at, - updated_at=updated_at) - - return intel diff --git a/src/vuln_analysis/utils/clients/rhsa_client.py b/src/vuln_analysis/utils/clients/rhsa_client.py deleted file mode 100644 index 606b48dba..000000000 --- a/src/vuln_analysis/utils/clients/rhsa_client.py +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os - -import aiohttp - -from vuln_analysis.data_models.cve_intel import CveIntelRhsa -from vuln_analysis.utils.clients.intel_client import IntelClient -from vuln_analysis.utils.url_utils import url_join - -logger = logging.getLogger(__name__) - - -class RHSAClient(IntelClient): - """ - Async client for Red Hat Security Advisory API - """ - - def __init__(self, - *, - base_url: str | None = None, - session: aiohttp.ClientSession | None = None, - retry_count: int = 10, - sleep_time: float = 0.1, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True): - - super().__init__(session=session, - base_url=base_url or os.environ.get('RHSA_BASE_URL'), - retry_count=retry_count, - sleep_time=sleep_time, - respect_retry_after_header=respect_retry_after_header, - retry_on_client_errors=retry_on_client_errors) - - @classmethod - def default_base_url(cls) -> str: - return "https://access.redhat.com/hydra/rest" - - async def get_intel_dict(self, cve_id: str) -> dict: - response = await self.request(method='GET', - url=url_join(self.base_url, "securitydata", "cve.json"), - params={"ids": cve_id}) - - vulns = response - - if (len(vulns) == 0): - logger.info(f"No Red Hat CVE entry found for {cve_id}") - return {} - - if (len(vulns) > 1): - logger.warning(f"Found multiple Red Hat CVE entries for {cve_id}, using the first one") - - response_full = await self.request(method='GET', - url=url_join(self.base_url, "securitydata", f"cve/{cve_id}.json")) - - return response_full - - async def get_intel(self, cve_id: str) -> CveIntelRhsa: - intel_dict = await self.get_intel_dict(cve_id) - - return CveIntelRhsa.model_validate(intel_dict) diff --git a/src/vuln_analysis/utils/clients/ubuntu_client.py b/src/vuln_analysis/utils/clients/ubuntu_client.py deleted file mode 100644 index a90848332..000000000 --- a/src/vuln_analysis/utils/clients/ubuntu_client.py +++ /dev/null @@ -1,71 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import aiohttp - -from vuln_analysis.data_models.cve_intel import CveIntelUbuntu -from vuln_analysis.utils.clients.intel_client import IntelClient -from vuln_analysis.utils.clients.rhsa_client import logger -from vuln_analysis.utils.url_utils import url_join - - -class UbuntuClient(IntelClient): - """ - Async client for Ubuntu Security Advisory API - """ - - def __init__(self, - *, - base_url: str | None = None, - session: aiohttp.ClientSession | None = None, - retry_count: int = 10, - sleep_time: float = 0.1, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True): - - super().__init__(session=session, - base_url=base_url or os.environ.get('UBUNTU_BASE_URL'), - retry_count=retry_count, - sleep_time=sleep_time, - respect_retry_after_header=respect_retry_after_header, - retry_on_client_errors=retry_on_client_errors) - - @classmethod - def default_base_url(cls) -> str: - return "https://ubuntu.com" - - async def get_intel_dict(self, cve_id: str) -> dict: - response = await self.request(method='GET', - url=url_join(self.base_url, "security", "cves.json"), - params={"q": cve_id}) - - # Get the vulnerabilities from the dict - vulns = response.get("cves", []) - - if (len(vulns) == 0): - logger.info(f"No Ubuntu CVE entry found for {cve_id}") - return {} - - if (len(vulns) > 1): - logger.warning(f"Found multiple Ubuntu CVE entries for {cve_id}, using the first one") - - return vulns[0] - - async def get_intel(self, cve_id: str) -> CveIntelUbuntu: - intel_dict = await self.get_intel_dict(cve_id) - - return CveIntelUbuntu.model_validate(intel_dict) diff --git a/src/vuln_analysis/utils/concurrency.py b/src/vuln_analysis/utils/concurrency.py index 85b597eca..3aeea1146 100644 --- a/src/vuln_analysis/utils/concurrency.py +++ b/src/vuln_analysis/utils/concurrency.py @@ -18,19 +18,45 @@ This module provides rate limiting utilities to control the rate of concurrent LLM API requests across workflow functions. Rate limiting helps prevent API throttling errors and ensures stable performance when making multiple LLM calls. + +It also provides file locking utilities to prevent concurrent access issues +when multiple processes try to access the same git repositories or vector databases. """ +import asyncio import contextvars +import hashlib +import logging +import os +import tempfile +from collections.abc import Callable +from contextlib import contextmanager +from pathlib import Path from typing import TYPE_CHECKING from aiolimiter import AsyncLimiter +from filelock import FileLock +from filelock import Timeout if TYPE_CHECKING: from nat.builder.builder import Builder -# Context variable to store parent workflow's llm_max_rate -# This allows nested workflows (like cve_agent used inside execute_then_select_function) -# to propagate their llm_max_rate setting to child functions +logger = logging.getLogger(__name__) + +# Context variable used to propagate a parent function's llm_max_rate down to nested +# child functions, so the child's get_effective_rate_limiter (priority #2 below) inherits +# the parent's rate even when builder.get_workflow_config() (priority #3) resolves to some +# other root workflow. +# +# INTENTIONALLY RETAINED — DO NOT DELETE AS DEAD CODE. No function in this fork currently +# calls ctx_parent_max_rate.set(), so this branch is presently inert. It is kept for the +# Test-Time-Compute (TTC) case: if execute_then_select_function is added to wrap cve_pipeline +# (running it N times), the four agent functions become grandchildren and their priority-#3 +# workflow-config lookup would resolve to the TTC wrapper instead of cve_pipeline, silently +# losing the pipeline's rate setting. The reader (priority #2) is already wired and tested; +# the TTC parent only needs to add the setter at call time, e.g.: +# token = ctx_parent_max_rate.set(config.llm_max_rate) +# try: ... finally: ctx_parent_max_rate.reset(token) ctx_parent_max_rate: contextvars.ContextVar[int | None] = contextvars.ContextVar("ctx_parent_max_rate", default=None) @@ -86,3 +112,196 @@ def get_effective_rate_limiter(local_max_rate: int | None, builder: "Builder") - # exactly 1/effective_max_rate seconds between each request. time_between_requests = 1.0 / effective_max_rate return AsyncLimiter(max_rate=1, time_period=time_between_requests) + + +def make_lazy_rate_limiter(local_max_rate: int | None, builder: "Builder") -> "Callable[[], AsyncLimiter | None]": + """Return a zero-arg callable that resolves the effective rate limiter lazily and caches it. + + The limiter must be resolved at *call* time rather than at function-build time: the + workflow config (which carries the workflow-level ``llm_max_rate``) is only set on the + builder after all functions have been built, so ``builder.get_workflow_config()`` raises + ``ValueError("No workflow set")`` during build. Resolving on first invocation ensures the + workflow-level setting is honored. + + The result is cached so that a single shared ``AsyncLimiter`` is reused across all + (potentially concurrent) invocations of the function, enforcing one global rate cap. + ``get_effective_rate_limiter`` is synchronous, so the cache check-and-set has no ``await`` + in between and is safe under cooperative asyncio scheduling. + + Args: + local_max_rate: The function-specific llm_max_rate value (requests per second) + builder: The Builder instance to get workflow config from + + Returns: + A callable returning the shared AsyncLimiter (or None for no rate limiting) + """ + cache: dict[str, AsyncLimiter | None] = {} + + def _get() -> AsyncLimiter | None: + if "limiter" not in cache: + cache["limiter"] = get_effective_rate_limiter(local_max_rate, builder) + return cache["limiter"] + + return _get + + +def get_lock_path(resource_path: Path, lock_dir: Path | None = None) -> Path: + """ + Generate a unique lock file path for a given resource path. + + The lock file is created in a central lock directory (default: .cache/vuln_analysis/locks) + with a filename based on the hash of the resource path. This avoids conflicts with + the actual resource files and ensures locks work even if the resource doesn't exist yet. + + Args: + resource_path: Path to the resource being locked + lock_dir: Optional directory to store lock files (default: .cache/vuln_analysis/locks) + + Returns: + Path to the lock file + """ + if lock_dir is None: + lock_dir = Path(".cache/vuln_analysis/locks") + + # Ensure lock directory exists + lock_dir.mkdir(parents=True, exist_ok=True) + + # Generate a unique lock filename based on the resource path hash + # Use absolute path without resolving to avoid issues with non-existent paths + # Convert to absolute path first, then to string for consistent hashing + if not resource_path.is_absolute(): + resource_path = Path.cwd() / resource_path + + resource_hash = hashlib.sha256(str(resource_path).encode()).hexdigest()[:16] + lock_file = lock_dir / f"{resource_hash}.lock" + + return lock_file + + +@contextmanager +def file_lock(resource_path: Path, timeout: int = 300, lock_dir: Path | None = None): + """ + Context manager for file-based locking to prevent concurrent access to resources. + + This is useful for preventing race conditions when multiple processes try to: + - Clone or update the same git repository + - Create or load the same vector database + - Access any other shared file-based resource + + The lock file is created in a central lock directory (default: .cache/vuln_analysis/locks) + and is automatically cleaned up after use. + + Args: + resource_path: Path to the resource being locked (e.g., git repo path, VDB path) + timeout: Maximum time in seconds to wait for the lock (default: 300 seconds) + lock_dir: Optional directory to store lock files (default: .cache/vuln_analysis/locks) + + Yields: + FileLock instance + + Raises: + Timeout: If the lock cannot be acquired within the timeout period + + Example: + ```python + with file_lock(Path("/path/to/git/repo")): + # Perform git operations + repo.git.fetch() + repo.git.checkout() + ``` + """ + lock_file = get_lock_path(resource_path, lock_dir) + lock = FileLock(lock_file, timeout=timeout) + + try: + logger.debug("Acquiring lock for resource: %s (lock file: %s)", resource_path, lock_file) + with lock.acquire(timeout=timeout): + logger.debug("Lock acquired for resource: %s", resource_path) + yield lock + logger.debug("Releasing lock for resource: %s", resource_path) + except Timeout: + logger.error("Timeout waiting for lock on resource: %s (waited %d seconds)", resource_path, timeout) + raise + finally: + # Clean up stale lock file if it exists and is not locked by any process + # Attempt non-blocking acquire to check if another process holds the lock + # This prevents accumulation of lock files over time while avoiding race conditions + try: + if lock_file.exists(): + # Attempt non-blocking acquire - if successful, no other process has the lock + try: + with lock.acquire(timeout=0): + # Successfully acquired, meaning no other process has it + # Release it (via context manager exit) and delete the file + pass + lock_file.unlink(missing_ok=True) + logger.debug("Cleaned up lock file: %s", lock_file) + except Timeout: + # Another process holds the lock, skip deletion + pass + except Exception as e: + logger.warning("Failed to clean up lock file %s: %s", lock_file, e) + + +def atomic_write(path: Path, content: str) -> None: + """Write *content* to *path* atomically using write-to-temp + rename. + + On POSIX systems ``os.replace`` is atomic, so a concurrent reader will + either see the old file or the new complete file — never a partial write. + """ + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(content) + os.replace(tmp, path) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise + + +class AsyncCoalescer: + """Coalesce concurrent async calls that share the same cache key. + + The first caller for a given *key* executes the coroutine; any callers + that arrive while the first is still in-flight will await the same result + instead of executing independently. This eliminates redundant LLM calls + when multiple requests for the same CVE/container arrive concurrently. + """ + + def __init__(self) -> None: + self._in_flight: dict[str, asyncio.Future] = {} + self._lock = asyncio.Lock() + + async def run_once(self, key: str, coro_fn): + """Run *coro_fn()* only once per *key* at a time. + + If another call with the same *key* is already in-flight, await its + result instead of starting a second execution. + """ + async with self._lock: + if key in self._in_flight: + existing = self._in_flight[key] + else: + existing = None + future: asyncio.Future = asyncio.get_running_loop().create_future() + self._in_flight[key] = future + + # Await outside the lock so other waiters and the finally + # cleanup are not blocked. + if existing is not None: + return await asyncio.shield(existing) + + try: + result = await coro_fn() + future.set_result(result) + return result + except BaseException as exc: + future.set_exception(exc) + raise + finally: + async with self._lock: + self._in_flight.pop(key, None) diff --git a/src/vuln_analysis/utils/deep_agentic_loop.py b/src/vuln_analysis/utils/deep_agentic_loop.py new file mode 100644 index 000000000..fb61335aa --- /dev/null +++ b/src/vuln_analysis/utils/deep_agentic_loop.py @@ -0,0 +1,283 @@ +"""Shared Deep Agents based tool-calling loop for CVE pipeline agents. + +This module mirrors :mod:`vuln_analysis.utils.agentic_loop`'s public return +contract while delegating orchestration to LangChain Deep Agents. +""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Awaitable, Callable, Sequence +from typing import Any + +from langchain.agents.middleware.types import AgentMiddleware +from langchain.agents.middleware.types import ModelRequest +from langchain.agents.middleware.types import ModelResponse +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage +from langchain_core.tools import BaseTool + +from vuln_analysis.utils.helpers import truncate + +logger = logging.getLogger(__name__) + +_RETRY_ATTEMPTS = 3 +_RETRY_BASE_DELAY = 2.0 +_GATEWAY_TIMEOUT_BASE_DELAY = 10.0 +_DEFAULT_EXCLUDED_TOOLS = frozenset({"execute"}) + + +class _ToolVisibilityMiddleware(AgentMiddleware[Any, Any, Any]): + """Hide selected Deep Agents tools from model requests.""" + + def __init__(self, excluded: frozenset[str]) -> None: + self._excluded = excluded + + @staticmethod + def _tool_name(tool: BaseTool | dict[str, Any]) -> str | None: + if isinstance(tool, dict): + name = tool.get("name") + return name if isinstance(name, str) else None + name = getattr(tool, "name", None) + return name if isinstance(name, str) else None + + def wrap_model_call( + self, + request: ModelRequest[Any], + handler: Callable[[ModelRequest[Any]], ModelResponse[Any]], + ) -> ModelResponse[Any]: + if self._excluded: + request = request.override( + tools=[tool for tool in request.tools if self._tool_name(tool) not in self._excluded] + ) + return handler(request) + + async def awrap_model_call( + self, + request: ModelRequest[Any], + handler: Callable[[ModelRequest[Any]], Awaitable[ModelResponse[Any]]], + ) -> ModelResponse[Any]: + if self._excluded: + request = request.override( + tools=[tool for tool in request.tools if self._tool_name(tool) not in self._excluded] + ) + return await handler(request) + + +def _message_content(message: Any) -> str: + if isinstance(message, BaseMessage): + content = message.content + elif isinstance(message, dict): + content = message.get("content", "") + else: + content = getattr(message, "content", "") + + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for item in content: + if isinstance(item, str): + parts.append(item) + elif isinstance(item, dict): + text = item.get("text") or item.get("content") + if text: + parts.append(str(text)) + return "\n".join(parts) + return str(content or "") + + +def _split_system_prompt(messages: Sequence[BaseMessage]) -> tuple[str | None, list[BaseMessage]]: + system_parts: list[str] = [] + conversation: list[BaseMessage] = [] + + for message in messages: + if isinstance(message, SystemMessage): + content = _message_content(message).strip() + if content: + system_parts.append(content) + else: + conversation.append(message) + + system_prompt = "\n\n".join(system_parts) if system_parts else None + if not conversation: + conversation.append(HumanMessage(content="Proceed with the requested analysis and produce the final report.")) + return system_prompt, conversation + + +def _extract_final_content(result: Any) -> str: + if isinstance(result, dict): + structured = result.get("structured_response") + if structured: + return str(structured).strip() + + messages = result.get("messages") or [] + for message in reversed(messages): + if isinstance(message, AIMessage) or ( + isinstance(message, dict) and message.get("role") in {"assistant", "ai"} + ): + content = _message_content(message).strip() + if content: + return content + + for message in reversed(messages): + content = _message_content(message).strip() + if content: + return content + + return _message_content(result).strip() + + +def _count_agent_activity(result: Any) -> tuple[int, int]: + if not isinstance(result, dict): + return 0, 0 + + tool_call_count = 0 + assistant_tool_calls = 0 + llm_call_count = 0 + + for message in result.get("messages") or []: + if isinstance(message, AIMessage): + llm_call_count += 1 + assistant_tool_calls += len(message.tool_calls or []) + elif isinstance(message, ToolMessage): + tool_call_count += 1 + elif isinstance(message, dict): + role = message.get("role") + if role in {"assistant", "ai"}: + llm_call_count += 1 + assistant_tool_calls += len(message.get("tool_calls") or []) + elif role == "tool": + tool_call_count += 1 + + if not tool_call_count: + tool_call_count = assistant_tool_calls + return tool_call_count, llm_call_count + + +async def _ainvoke_with_retry(agent: Any, payload: dict[str, Any], runtime_config: dict[str, Any]) -> Any: + last_exc: Exception | None = None + for attempt in range(1, _RETRY_ATTEMPTS + 1): + try: + return await agent.ainvoke(payload, config=runtime_config) + except Exception as exc: + last_exc = exc + status = getattr(exc, "status_code", None) + retryable = status in {429, 500, 502, 503, 504} or status is None + if not retryable or attempt == _RETRY_ATTEMPTS: + raise + + base = _GATEWAY_TIMEOUT_BASE_DELAY if status == 504 else _RETRY_BASE_DELAY + delay = base * (2 ** (attempt - 1)) + logger.warning( + "Deep Agents graph failed (attempt %d/%d, status=%s): %s -- retrying in %.1fs", + attempt, + _RETRY_ATTEMPTS, + status, + exc, + delay, + ) + await asyncio.sleep(delay) + raise last_exc + + +def _step_manager(): + try: + from nat.builder import SyncBuilder + + return SyncBuilder.current().context.intermediate_step_manager + except Exception: + return None + + +async def run_deep_agentic_loop( + llm: BaseChatModel, + messages: list[BaseMessage], + tools: list[BaseTool], + *, + max_iterations: int, + context_window_tokens: int, + wrap_up_fraction: float | None = None, + loop_threshold: int = 2, + phase_label: str = "", + subagents: Sequence[dict[str, Any]] | None = None, + excluded_tools: frozenset[str] | set[str] | Sequence[str] | None = _DEFAULT_EXCLUDED_TOOLS, + debug: bool = False, +) -> tuple[str, int, int]: + """Run a Deep Agents graph and return ``(final_content, tool_calls, llm_calls)``. + + The first ``SystemMessage`` content in ``messages`` is passed as the Deep + Agents ``system_prompt``. Non-system messages are passed as the graph input. + + ``context_window_tokens``, ``wrap_up_fraction``, and ``loop_threshold`` are + accepted for call-site compatibility with the legacy loop. Deep Agents owns + context management internally; ``max_iterations`` is mapped to the graph + recursion limit. + """ + del context_window_tokens, wrap_up_fraction, loop_threshold + + try: + from deepagents import create_deep_agent + except ModuleNotFoundError as exc: + raise RuntimeError( + "The Deep Agents loop requires the `deepagents` package. " + "Run `uv sync` after updating dependencies." + ) from exc + + label = phase_label or "Deep Agent" + system_prompt, conversation = _split_system_prompt(messages) + middleware: list[AgentMiddleware[Any, Any, Any]] = [] + if excluded_tools: + middleware.append(_ToolVisibilityMiddleware(frozenset(excluded_tools))) + + logger.info("%s -- starting Deep Agents graph", label) + step_mgr = _step_manager() + if step_mgr: + try: + step_mgr.add_step( + agent_action=f"[{label}] Deep Agents graph start", + observation=truncate(_message_content(conversation[-1]), 500), + ) + except Exception: + pass + + agent = create_deep_agent( + model=llm, + tools=tools, + system_prompt=system_prompt, + middleware=middleware, + subagents=subagents, + name=label.lower().replace(" ", "-"), + debug=debug, + ) + + result = await _ainvoke_with_retry( + agent, + {"messages": conversation}, + {"recursion_limit": max(2, max_iterations * 2)}, + ) + + final_content = _extract_final_content(result) + tool_calls, llm_calls = _count_agent_activity(result) + + if step_mgr: + try: + step_mgr.add_step( + agent_action=f"[{label}] Deep Agents graph complete", + observation=truncate(final_content or "(no final content)", 500), + ) + except Exception: + pass + + logger.info( + "%s -- Deep Agents graph complete (%d chars, %d tool calls, %d LLM calls)", + label, + len(final_content), + tool_calls, + llm_calls, + ) + return final_content, tool_calls, llm_calls + + +run_agentic_loop = run_deep_agentic_loop diff --git a/src/vuln_analysis/utils/document_embedding.py b/src/vuln_analysis/utils/document_embedding.py deleted file mode 100644 index 861f1eac7..000000000 --- a/src/vuln_analysis/utils/document_embedding.py +++ /dev/null @@ -1,504 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import copy -import json -import logging -import os -import sys -import time -import typing -from hashlib import sha512 -from pathlib import Path -from pathlib import PurePath - -from langchain.docstore.document import Document -from langchain.text_splitter import Language -from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain_community.document_loaders.generic import GenericLoader -from langchain_community.document_loaders.parsers.language.code_segmenter import CodeSegmenter -from langchain_community.document_loaders.parsers.language.language_parser import LANGUAGE_EXTENSIONS -from langchain_community.document_loaders.parsers.language.language_parser import LANGUAGE_SEGMENTERS -from langchain_community.document_loaders.parsers.language.language_parser import LanguageParser -from langchain_community.vectorstores import FAISS -from langchain_core.document_loaders.blob_loaders import Blob - -from vuln_analysis.data_models.input import SourceDocumentsInfo -from vuln_analysis.utils.js_extended_parser import ExtendedJavaScriptSegmenter -from vuln_analysis.utils.source_code_git_loader import SourceCodeGitLoader - -if typing.TYPE_CHECKING: - from langchain_core.embeddings import Embeddings # pragma: no cover - -PathLike = typing.Union[str, os.PathLike] - -logger = logging.getLogger(__name__) - - -class MultiLanguageRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter): - """ - A version of langchain's RecursiveCharacterTextSplitter that supports multiple languages. - """ - - def __init__( - self, - keep_separator: bool = True, - **kwargs, - ) -> None: - """Create a new RecursiveCharacterTextSplitter.""" - super().__init__(is_separator_regex=True, keep_separator=keep_separator, **kwargs) - - def _get_separators(self, language: Language) -> list[str]: - try: - return RecursiveCharacterTextSplitter.get_separators_for_language(language) - except ValueError: - return self._separators - - def create_documents(self, texts: list[str], metadatas: list[dict] | None = None) -> list[Document]: - """Create documents from a list of texts.""" - _metadatas = metadatas or [{}] * len(texts) - documents = [] - for i, text in enumerate(texts): - index = 0 - previous_chunk_len = 0 - - # Determine the language of the text from the metadata - language = _metadatas[i].get("language", None) - - for chunk in self._split_text(text, separators=self._get_separators(language)): - metadata = copy.deepcopy(_metadatas[i]) - if self._add_start_index: - offset = index + previous_chunk_len - self._chunk_overlap - index = text.find(chunk, max(0, offset)) - metadata["start_index"] = index - previous_chunk_len = len(chunk) - new_doc = Document(page_content=chunk, metadata=metadata) - documents.append(new_doc) - return documents - - -class ExtendedLanguageParser(LanguageParser): - """ - A version of langchain's LanguageParser that supports extended file extension and language parsing. - """ - - LANGUAGE_EXTENSIONS: dict[str, str] = { - **LANGUAGE_EXTENSIONS, - "h": "c", - "hpp": "cpp", - } - - additional_segmenters: dict[str, type[CodeSegmenter]] = {} - - if os.environ.get('ENABLE_EXTENDED_JS_PARSERS'): - additional_segmenters = { - "javascript": ExtendedJavaScriptSegmenter, - "js": ExtendedJavaScriptSegmenter, - } - - LANGUAGE_SEGMENTERS: dict[str, type[CodeSegmenter]] = { - **LANGUAGE_SEGMENTERS, - **additional_segmenters, - } - - def lazy_parse(self, blob: Blob) -> typing.Iterator[Document]: - try: - code = blob.as_string() - except Exception as e: - logger.warning("Failed to read code for '%s'. Ignoring this file. Error: %s", blob.source, e) - return - - language = self.language or (self.LANGUAGE_EXTENSIONS.get(blob.source.rsplit(".", 1)[-1]) if isinstance( - blob.source, str) else None) - - if language is None: - yield Document( - page_content=code, - metadata={ - "source": blob.source, - }, - ) - return - - if self.parser_threshold >= len(code.splitlines()): - yield Document( - page_content=code, - metadata={ - "source": blob.source, - "language": language, - }, - ) - return - - segmenter = self.LANGUAGE_SEGMENTERS[language](blob.as_string()) - - try: - extracted_functions_classes = segmenter.extract_functions_classes() - - except Exception as e: - - logger.warning("Failed to parse code for '%s'. Ignoring this file. Error: %s", - blob.source, - e, - exc_info=True) - extracted_functions_classes = [] - - # If the code didnt parse, and there are no functions or classes, return the original code - if not segmenter.is_valid() and len(extracted_functions_classes) == 0: - yield Document( - page_content=code, - metadata={ - "source": blob.source, - "language": language, - }, - ) - return - - for functions_classes in extracted_functions_classes: - yield Document( - page_content=functions_classes, - metadata={ - "source": blob.source, - "content_type": "functions_classes", - "language": language, - }, - ) - - try: - simplified_code = segmenter.simplify_code() - # If simplifying the code fails, return the original code - except Exception as e: - logger.warning("Failed to simplify code for '%s'. Returning original code. Error: %s", - blob.source, - e, - exc_info=True) - yield Document( - page_content=code, - metadata={ - "source": blob.source, - "language": language, - }, - ) - else: - yield Document( - page_content=simplified_code, - metadata={ - "source": blob.source, - "content_type": "simplified_code", - "language": language, - }, - ) - - -class DocumentEmbedding: - """ - A class to create a FAISS database from a list of source documents. The source documents are collected from git - repositories and chunked into smaller pieces for embedding. - """ - - def __init__(self, - *, - embedding: "Embeddings", - vdb_directory: PathLike = "./.cache/am_cache/vdb", - git_directory: PathLike = "./.cache/am_cache/git", - chunk_size: int = 800, - chunk_overlap: int = 160): - """ - Create a new DocumentEmbedding instance. - - Parameters - ---------- - embedding : Embeddings - The embedding to use for the FAISS database. - vdb_directory : PathLike - The directory to save the FAISS database. The database will be saved in a subdirectory based on the hash of - the source documents. - git_directory : PathLike, optional - The directory to use for the Git repository cloning, by default "./.tmp/git_cache" - chunk_size : int, optional - Maximum size of a single chunk, by default 1000 - chunk_overlap : int, optional - Overlap between chunks, by default 200 - """ - - self._embedding = embedding - self._vdb_directory = Path(vdb_directory) - self._git_directory = Path(git_directory) - self._chunk_size = chunk_size - self._chunk_overlap = chunk_overlap - - @property - def embedding(self): - return self._embedding - - @property - def vdb_directory(self): - return self._vdb_directory - - @property - def git_directory(self): - return self._git_directory - - @property - def chunk_size(self): - return self._chunk_size - - @property - def chunk_overlap(self): - return self._chunk_overlap - - def hash_source_documents_info(self, source_infos: list[SourceDocumentsInfo]): - """ - Hash the source documents info to create a unique hash for the source documents. This is used to determine if - the VDB already exists for the source documents. - - Parameters - ---------- - source_infos : list[SourceDocumentsInfo] - A list of source documents info to hash. - - Returns - ------- - str - Returns a unique hash for the source documents as a positive integer. Will always return the same hash for - the same source regardless of order. - """ - - obj_to_hash = { - "class": self.__class__.__qualname__, - "source_infos": [hash(x) for x in sorted(source_infos)], - "embedding": str(self._embedding), - "chunk_size": self._chunk_size, - "chunk_overlap": self._chunk_overlap, - } - - # Hash the source documents info - hash_val = int.from_bytes(bytes=sha512(f"{json.dumps(obj_to_hash)}".encode('utf-8', errors='ignore')).digest(), - byteorder=sys.byteorder) - - # Convert to a positive, consistent hash (Avoids negative hex values) - nbits = 64 - return hex((hash_val + (1 << nbits)) % (1 << nbits))[2:] # Skip the '0x' prefix - - def _chunk_documents(self, documents: list[Document]) -> list[Document]: - """ - Chunk the documents into smaller pieces for embedding. - """ - - splitter = MultiLanguageRecursiveCharacterTextSplitter(chunk_size=self._chunk_size, - chunk_overlap=self._chunk_overlap) - split_documents = splitter.split_documents(documents) - - return split_documents - - def get_repo_path(self, source_info: SourceDocumentsInfo): - """ - Returns the path to the git repository for a source document info. - - Parameters - ---------- - source_info : SourceDocumentsInfo - The source document info to get the git repository path. - - Returns - ------- - Path - Returns the path to the git repository. - """ - return self._git_directory / PurePath(source_info.git_repo) - - def collect_documents(self, source_info: SourceDocumentsInfo) -> list[Document]: - """ - Collect documents from a source document info. This will clone the git repository and collect files from the - repository based on the include and exclude patterns. Each file is then parsed and segmented based on its - language into Documents. - - Parameters - ---------- - source_info : SourceDocumentsInfo - The source document info to collect documents - - Returns - ------- - list[Document] - Returns a list of documents collected from the source document info. - """ - - repo_path = self.get_repo_path(source_info) - - blob_loader = SourceCodeGitLoader(repo_path=repo_path, - clone_url=source_info.git_repo, - ref=source_info.ref, - include=source_info.include, - exclude=source_info.exclude) - - blob_parser = ExtendedLanguageParser() - - loader = GenericLoader(blob_loader=blob_loader, blob_parser=blob_parser) - - documents = loader.load() - - logger.info("Collected documents for '%s', Document count: %d", repo_path, len(documents)) - - return documents - - def create_vdb(self, source_infos: list[SourceDocumentsInfo], output_path: PathLike): - """ - Create a FAISS database from a list of input directories. - - Parameters - ---------- - source_infos : list[SourceDocumentsInfo] - A list of source documents to create the VDB from. All documents will be collected from the source - documents. - output_path : PathLike - The location to save the FAISS database. - - Returns - ------- - FAISS - Returns an instance of the FAISS database. - """ - - logger.info("Collecting documents from git repos. Source Infos: %s", - json.dumps([x.model_dump(mode="json") for x in source_infos])) - - output_path = Path(output_path) - - # Warn if the output path already exists and we will overwrite it - if (output_path.exists()): - - logger.warning("Vector Database already exists and will be overwritten: %s", output_path) - - documents = [] - for input_dir in source_infos: - try: - documents.extend(self.collect_documents(input_dir)) - except Exception as e: - logger.warning("Error collecting documents for source info %s: %s", input_dir, e) - continue - - # Warn and return early if no documents were collected - if len(documents) == 0: - logger.warning("No documents were collected, skipping VDB generation for source infos: %s", source_infos) - return None - - # Apply chunking on the source documents - chunked_documents = self._chunk_documents(documents) - - logger.info("Creating FAISS database from source documents. Doc count: %d, Chunks: %s, Location: %s", - len(documents), - len(chunked_documents), - output_path) - - saved_show_progress = getattr(self._embedding, "show_progress", None) - saved_show_progress_bar = getattr(self._embedding, "show_progress_bar", None) - - # Attempt to enable progress bar for VDB generation if supported - if (hasattr(self._embedding, "show_progress")): - setattr(self._embedding, "show_progress", True) - if (hasattr(self._embedding, "show_progress_bar")): - setattr(self._embedding, "show_progress_bar", True) - - embedding_start_time = time.time() - - # Create the FAISS database - db = FAISS.from_documents(chunked_documents, self._embedding) - - logger.info("Completed embedding in %.2f seconds for '%s'", time.time() - embedding_start_time, output_path) - - # Clear the CUDA cache if torch is available. This is to prevent the pytorch cache from growing when it will not be reused - try: - import torch - torch.cuda.empty_cache() - except ImportError: - pass - - # Revert progress bar setting - if (saved_show_progress is not None): - setattr(self._embedding, "show_progress", saved_show_progress) - if (saved_show_progress_bar is not None): - setattr(self._embedding, "show_progress_bar", saved_show_progress_bar) - - # Ensure the directory exists - output_path.mkdir(exist_ok=True, parents=True) - - # Save the database - db.save_local(str(output_path)) - - return db - - def build_vdbs(self, - input_sources: list[SourceDocumentsInfo], - ignore_code_embedding: bool = False, - ignore_doc_embedding: bool = False) -> tuple[Path | None, Path | None]: - """ - Build the code and document VDB based on a list of source documents. - - Parameters - ---------- - input_sources : list[SourceDocumentsInfo] - A list of source documents to create the VDB from. - ignore_code_embedding : bool - Whether to ignore building code vector database. - ignore_doc_embedding : bool - Whether to ignore building documentation vector database. - - Returns - ------- - tuple[Path | None, Path | None] - Returns a tuple of (code_vdb, doc_vdb). If the VDB was not created, the value will be None for each type. - """ - code_vdb: Path | None = None - doc_vdb: Path | None = None - - # Create embeddings for each source type - for source_type in ["code", "doc"]: - - if ignore_code_embedding and source_type == "code": - continue - - if ignore_doc_embedding and source_type == "doc": - continue - - # Filter the source documents - source_infos = [source_info for source_info in input_sources if source_info.type == source_type] - - if source_infos: - - # Determine the output path by combining the vdb_directory with the hash of the source documents - vdb_output_dir = self.vdb_directory / source_type / str(self.hash_source_documents_info(source_infos)) - - if (not vdb_output_dir.exists() or os.environ.get("MORPHEUS_ALWAYS_REBUILD_VDB", "0") == "1"): - vdb = self.create_vdb(source_infos=source_infos, output_path=vdb_output_dir) - else: - logger.info("Cache hit on VDB. Loading existing FAISS database: %s", vdb_output_dir) - - vdb = FAISS.load_local(str(vdb_output_dir), self._embedding, allow_dangerous_deserialization=True) - - else: - vdb_output_dir = None - vdb = None - - if (source_type == "code"): - if (vdb is not None): - code_vdb = vdb_output_dir - elif (source_type == "doc"): - if (vdb is not None): - doc_vdb = vdb_output_dir - else: - raise ValueError(f"Unknown source type: {source_type}") # pragma: no cover - - return code_vdb, doc_vdb diff --git a/src/vuln_analysis/utils/full_text_search.py b/src/vuln_analysis/utils/full_text_search.py deleted file mode 100644 index 58b6fcb21..000000000 --- a/src/vuln_analysis/utils/full_text_search.py +++ /dev/null @@ -1,193 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -import re -from pathlib import Path -from typing import Iterable - -from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain_community.document_loaders.generic import GenericLoader -from langchain_community.document_loaders.parsers import LanguageParser -from langchain_core.documents import Document -from tantivy import Document as TantivyDocument -from tantivy import Index -from tantivy import SchemaBuilder -from tqdm import tqdm - -logger = logging.getLogger(__name__) - -variable_pattern = re.compile(r"([A-Z][a-z]+|[a-z]+|[A-Z]+(?=[A-Z]|$))") - - -def tokenize_code(code: str) -> str: - """ - Tokenize code into simple readable document format - """ - matches = re.finditer(r"\b\w{2,}\b", code) - tokens = [] - for m in matches: - text = m.group() - - for section in text.split("_"): - for part in variable_pattern.findall(section): - if len(part) < 2: - continue - if (sum(1 for c in part if "a" <= c <= "z" or "A" <= c <= "Z" or "0" <= c <= "9") > len(part) // 2 - and len(part) / len(set(part)) < 4): - tokens.append(part.lower()) - - return " ".join(tokens) - - -def clean_query(input_query) -> str: - """ - Parse a query string with OR/AND operations and fix quotes issues using regex. - """ - # Remove anything after newline characters - if '\n' in input_query: - input_query = input_query.split('\n')[0] - - # Remove outer quotes if the string is surrounded by quotes - input_query = input_query.strip() - if (input_query.startswith('"') and input_query.endswith('"')) or \ - (input_query.startswith("'") and input_query.endswith("'")): - input_query = input_query[1:-1] - - # Replace escaped quotes with empty string - input_query = re.sub(r'\\"|\\"', '', input_query) - - def replace_quoted(match): - return match.group(1) - - # Remove quotes around terms while preserving content inside - input_query = re.sub(r'["\'](.*?)["\']', replace_quoted, input_query) - - # Normalize operators to uppercase - input_query = re.sub(r'\s+OR\s+', ' OR ', input_query, flags=re.IGNORECASE) - input_query = re.sub(r'\s+AND\s+', ' AND ', input_query, flags=re.IGNORECASE) - # Remove any remaining quotes (including trailing quotes) - input_query = re.sub(r'["\'\`]', '', input_query) - - return input_query - - -class FullTextSearch: - INDEX_TYPE = "tantivy" - - def __init__(self, cache_path: str = None, tokenizer=False): - if cache_path: - os.makedirs(cache_path, exist_ok=True) - schema = self._build_schema() - self.index = Index(schema, path=cache_path) - self.index.reload() - self.tokenizer = tokenizer - - @classmethod - def get_index_directory(cls, base_path: str, hash_value: str) -> Path: - """Returns the path where the index should be stored""" - return Path(base_path) / cls.INDEX_TYPE / hash_value - - def _build_schema(self): - """Build schema for the code index""" - schema_builder = SchemaBuilder() - schema_builder.add_text_field("file_path", stored=True) - schema_builder.add_text_field("content", stored=True) - schema_builder.add_integer_field("doc_id", stored=True) - schema = schema_builder.build() - return schema - - def add_documents(self, documents: Iterable): - - writer = self.index.writer() - for doc_id, (title, text) in enumerate(documents): - writer.add_document(TantivyDocument(file_path=title, content=text, doc_id=doc_id)) - writer.commit() - - def is_empty(self): - return self.index.searcher().num_docs == 0 - - def search_index(self, query: str, top_k: int = 10) -> list[dict] | str: - - self.index.reload() - try: - if self.tokenizer: - query = tokenize_code(query) - query = clean_query(query) - query, _ = self.index.parse_query_lenient(query) - searcher = self.index.searcher() - results = searcher.search(query, limit=top_k).hits - return [{ - "source": searcher.doc(doc_id)["file_path"][0], "content": searcher.doc(doc_id)["content"][0] - } for _, doc_id in results] - - except Exception as e: - logger.exception(e) - return "There was an error searching for the query." - - def add_documents_from_langchain_chunks(self, documents: list[Document]): - """Create an index from langchain chunked documents""" - - try: - documents = [(doc.metadata['source'], doc.page_content) for doc in documents] - if len(documents) == 0: - return self.index - - self.add_documents(tqdm(documents, total=len(documents), desc="Indexing")) - - except Exception as e: - logger.warning(f"Unable to add documents to the index {e}") - - def add_documents_from_code_path(self, - code_path: str, - include_extensions: list[str], - use_langparser=True, - splitter=True): - """Create an index from raw files.""" - - doc_content = [] - - if use_langparser: - - loader = GenericLoader.from_filesystem( - code_path, - glob="**/*", - suffixes=include_extensions, - parser=LanguageParser(), - ) - docs = loader.load() - - if splitter: - text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) - docs = text_splitter.split_documents(docs) - doc_content = [(doc.metadata["source"], doc.page_content) for doc in docs] - - else: - - for root, _, files in os.walk(code_path): - for file in files: - if any(file.endswith(ext) for ext in include_extensions): - file_path = os.path.join(root, file) - try: - with open(file_path, "r") as f: - content = f.read() - if self.tokenizer: - content = tokenize_code(content) - doc_content.append((file_path, content)) - except Exception as e: - logger.warning(f"Error reading {file_path}: {e}") - - self.add_documents(doc_content) diff --git a/src/vuln_analysis/utils/git_utils.py b/src/vuln_analysis/utils/git_utils.py index 87ede34a7..e3f672a27 100644 --- a/src/vuln_analysis/utils/git_utils.py +++ b/src/vuln_analysis/utils/git_utils.py @@ -14,6 +14,7 @@ # limitations under the License. import os +import re from pathlib import Path from pathlib import PurePath @@ -21,7 +22,7 @@ from git import Repo -def get_repo_from_path(base_dir: str, git_repo: str = ".git") -> Repo: +def get_repo_from_path(base_dir: str, git_repo: str = ".git", ref: str | None = None) -> Repo: """ Utility function for getting GitPython `Repo` object representing a Git repository. @@ -31,6 +32,10 @@ def get_repo_from_path(base_dir: str, git_repo: str = ".git") -> Repo: Path to base directory containing one or more Git repos git_repo : str Relative path to Git repo in base_dir, default is ".git" + ref : str | None + Optional git ref (branch, tag, commit). If provided, the path will include + the ref as a subdirectory (e.g., base_dir/git_repo/ref/) to match the new + path structure that prevents concurrent access conflicts. Returns ------- @@ -43,6 +48,13 @@ def get_repo_from_path(base_dir: str, git_repo: str = ".git") -> Repo: If the repository path does not exist. """ repo_path = base_dir / PurePath(git_repo) + + # If ref is provided, append it to the path (matching document_embedding.py behavior) + if ref is not None: + # Sanitize ref name for use in filesystem path (remove special chars) + safe_ref = re.sub(r'[^\w\-.]', '_', ref) + repo_path = repo_path / safe_ref + repo_path = Path(repo_path) if os.path.exists(repo_path): try: diff --git a/src/vuln_analysis/utils/helpers.py b/src/vuln_analysis/utils/helpers.py new file mode 100644 index 000000000..18ca11665 --- /dev/null +++ b/src/vuln_analysis/utils/helpers.py @@ -0,0 +1,34 @@ +"""Shared helper functions used across multiple agents.""" + +from __future__ import annotations + +from langchain_core.messages import BaseMessage + + +def truncate(s: str, max_len: int) -> str: + return s if len(s) <= max_len else s[: max_len - 3] + "..." + + +_CHARS_PER_TOKEN = 3.5 + + +def estimate_message_tokens(messages: list[BaseMessage]) -> int: + total_chars = 0 + for msg in messages: + content = msg.content if hasattr(msg, 'content') else "" + if content: + total_chars += len(content) + if hasattr(msg, 'tool_calls'): + for tc in (msg.tool_calls or []): + total_chars += len(tc.get("name", "")) + total_chars += len(str(tc.get("args", ""))) + return int(total_chars / _CHARS_PER_TOKEN) + + +def extract_final_response(messages: list[BaseMessage]) -> str | None: + for msg in reversed(messages): + if msg.type == "ai": + content = msg.content + if content and content.strip(): + return content + return None diff --git a/src/vuln_analysis/utils/image_extraction.py b/src/vuln_analysis/utils/image_extraction.py new file mode 100644 index 000000000..0a77cdded --- /dev/null +++ b/src/vuln_analysis/utils/image_extraction.py @@ -0,0 +1,365 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Pull a container image from a registry and materialize a squashed rootfs on disk. + +Shells out to ``crane`` (https://github.com/google/go-containerregistry) to stream +a flattened OCI rootfs in one HTTP+tar pass. Results are cached on disk keyed by +image digest so subsequent runs against the same image are near-instant. +""" + +from __future__ import annotations + +import asyncio +import logging +import os +import shutil +import subprocess +import threading +import time +import uuid +from dataclasses import dataclass +from pathlib import Path + +from vuln_analysis.utils.concurrency import atomic_write +from vuln_analysis.utils.concurrency import file_lock + +logger = logging.getLogger(__name__) + +_COMPLETE_MARKER = ".complete" +_STAGING_PREFIX = ".staging-" +_STALE_STAGING_AGE_SECONDS = 3600 +_PROGRESS_INTERVAL_SECONDS = 5.0 + + +class ImageMaterializationError(RuntimeError): + """Base class for image extraction failures.""" + + +class CraneNotInstalledError(ImageMaterializationError): + """The ``crane`` binary could not be found on PATH.""" + + +class ImageNotFoundError(ImageMaterializationError): + """The registry could not find the requested image reference.""" + + +class ImageAuthError(ImageMaterializationError): + """The registry rejected the request due to missing or invalid credentials.""" + + +class ImageExtractionError(ImageMaterializationError): + """Something went wrong during pull or tar extraction.""" + + +@dataclass(frozen=True) +class MaterializedImage: + rootfs_path: Path + config_path: Path + digest: str + pull_ref: str + + +def materialize_image( + pull_ref: str, + cache_root: Path, + *, + use_cache: bool = True, + crane_bin: str = "crane", + tar_bin: str = "tar", +) -> MaterializedImage: + """Pull *pull_ref* and return paths to a squashed rootfs and the OCI config. + + On a cache hit (digest already extracted under *cache_root*) the call returns + almost immediately. On a miss, the extraction is serialized per-digest via a + file lock so concurrent pipeline runs do not duplicate work. + """ + cache_root = Path(cache_root) + cache_root.mkdir(parents=True, exist_ok=True) + + _sweep_stale_staging(cache_root) + + digest = _resolve_digest(pull_ref, crane_bin=crane_bin) + target = cache_root / digest + + if use_cache and (target / _COMPLETE_MARKER).exists(): + logger.info("Image cache hit for %s (%s)", pull_ref, digest) + return _materialized(target, digest, pull_ref) + + with file_lock(target, timeout=1800): + # Double-checked locking: another process may have populated the cache + # while we were waiting on the lock. + if use_cache and (target / _COMPLETE_MARKER).exists(): + logger.info("Image cache hit for %s after lock wait (%s)", pull_ref, digest) + return _materialized(target, digest, pull_ref) + + if target.exists(): + # Stale partially-published target (e.g., crash after rename, before marker). + logger.warning("Removing stale cache target without .complete marker: %s", target) + shutil.rmtree(target) + + staging = cache_root / f"{_STAGING_PREFIX}{digest}-{uuid.uuid4().hex}" + rootfs_dir = staging / "rootfs" + try: + rootfs_dir.mkdir(parents=True) + logger.info("Extracting %s (%s) to %s", pull_ref, digest, rootfs_dir) + t0 = time.monotonic() + _crane_export_to_dir(pull_ref, rootfs_dir, crane_bin=crane_bin, tar_bin=tar_bin) + _crane_config_to_file(pull_ref, staging / "config.json", crane_bin=crane_bin) + atomic_write(staging / "pull_ref.txt", pull_ref) + os.replace(staging, target) + (target / _COMPLETE_MARKER).touch() + elapsed = time.monotonic() - t0 + logger.info("Materialized %s in %.1fs", pull_ref, elapsed) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + + return _materialized(target, digest, pull_ref) + + +async def materialize_image_bounded( + semaphore: asyncio.Semaphore, + pull_ref: str, + cache_root: Path, + *, + use_cache: bool = True, + wait_log_threshold_s: float = 1.0, + crane_bin: str = "crane", + tar_bin: str = "tar", +) -> MaterializedImage: + """Acquire a slot on *semaphore*, then run :func:`materialize_image` in a worker thread. + + Cold extractions are RAM+IO heavy; unbounded fan-out from a request handler + will OOM the host. Wrap each call in this helper to cap concurrency. + + Logs a notice when the wait for a slot exceeds *wait_log_threshold_s* so + operators can see queue saturation. The semaphore is always released — even + if the underlying extraction raises — because ``async with`` runs its exit + regardless. + """ + t_wait = time.monotonic() + async with semaphore: + wait_s = time.monotonic() - t_wait + if wait_s > wait_log_threshold_s: + logger.info( + "Waited %.1fs for an extraction slot before pulling %s", + wait_s, pull_ref, + ) + return await asyncio.to_thread( + materialize_image, + pull_ref, + cache_root, + use_cache=use_cache, + crane_bin=crane_bin, + tar_bin=tar_bin, + ) + + +def _materialized(target: Path, digest: str, pull_ref: str) -> MaterializedImage: + return MaterializedImage( + rootfs_path=target / "rootfs", + config_path=target / "config.json", + digest=digest, + pull_ref=pull_ref, + ) + + +def _resolve_digest(pull_ref: str, *, crane_bin: str) -> str: + try: + proc = subprocess.run( + # `--` stops flag parsing so a hostile pull_ref starting with `-` + # cannot smuggle a crane option. + [crane_bin, "digest", "--", pull_ref], + check=False, + capture_output=True, + text=True, + ) + except FileNotFoundError as e: + raise CraneNotInstalledError( + f"`{crane_bin}` not found on PATH. Install crane " + "(https://github.com/google/go-containerregistry) " + "or set crane_bin to a custom path." + ) from e + + if proc.returncode != 0: + _raise_for_crane_stderr(pull_ref, proc.stderr, action="digest") + + digest = proc.stdout.strip() + if not digest.startswith("sha256:"): + raise ImageExtractionError( + f"crane digest returned unexpected output for {pull_ref!r}: {digest!r}" + ) + return digest + + +def _crane_export_to_dir( + pull_ref: str, + rootfs_dir: Path, + *, + crane_bin: str, + tar_bin: str, +) -> None: + """Stream ``crane export -`` into ``tar -xf - -C ``. + + We shell out to the system ``tar`` rather than Python's :mod:`tarfile` + because PEP 706's data/tar filters trip on legitimate symlink chains in + real container rootfs (conda envs, gdb auto-load, terminfo cycles). + Both GNU and BSD tar strip leading ``/`` from member names by default; + BSD tar additionally refuses ``..`` traversal. The ``--`` after the + crane subcommand stops flag parsing on a hostile pull_ref. + """ + crane_proc = subprocess.Popen( + [crane_bin, "export", "--", pull_ref, "-"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert crane_proc.stdout is not None + tar_proc = subprocess.Popen( + [tar_bin, "-xf", "-", "-C", str(rootfs_dir)], + stdin=crane_proc.stdout, + stderr=subprocess.PIPE, + ) + # Let crane receive SIGPIPE if tar exits early. + crane_proc.stdout.close() + + with _ProgressReporter(rootfs_dir, pull_ref): + _, tar_stderr = tar_proc.communicate() + _, crane_stderr = crane_proc.communicate() + + if crane_proc.returncode != 0: + _raise_for_crane_stderr( + pull_ref, + crane_stderr.decode("utf-8", errors="replace"), + action="export", + ) + if tar_proc.returncode != 0: + msg = tar_stderr.decode("utf-8", errors="replace") + if "No space left on device" in msg: + raise ImageExtractionError(f"Disk full while extracting {pull_ref}: {msg.strip()}") + raise ImageExtractionError(f"tar failed extracting {pull_ref}: {msg.strip()}") + + +def _crane_config_to_file(pull_ref: str, config_path: Path, *, crane_bin: str) -> None: + proc = subprocess.run( + [crane_bin, "config", "--", pull_ref], + check=False, + capture_output=True, + ) + if proc.returncode != 0: + _raise_for_crane_stderr( + pull_ref, + proc.stderr.decode("utf-8", errors="replace"), + action="config", + ) + config_path.write_bytes(proc.stdout) + + +def _raise_for_crane_stderr(pull_ref: str, stderr: str, *, action: str) -> None: + """Map crane's stderr to a typed exception.""" + blob = (stderr or "").strip() + lowered = blob.lower() + if "unauthorized" in lowered or "authentication required" in lowered: + raise ImageAuthError( + f"Registry denied access to {pull_ref}. Run `crane auth login ` " + f"or configure ~/.docker/config.json. Underlying error: {blob}" + ) + if "manifest_unknown" in lowered or "manifest unknown" in lowered \ + or "name_unknown" in lowered or "not found" in lowered: + raise ImageNotFoundError(f"Image not found: {pull_ref}. Underlying error: {blob}") + raise ImageExtractionError(f"crane {action} failed for {pull_ref}: {blob}") + + +def _sweep_stale_staging(cache_root: Path) -> None: + """Remove abandoned ``.staging-*`` directories older than the threshold.""" + cutoff = time.time() - _STALE_STAGING_AGE_SECONDS + try: + entries = list(cache_root.iterdir()) + except FileNotFoundError: + return + for entry in entries: + if not entry.name.startswith(_STAGING_PREFIX): + continue + try: + mtime = entry.stat().st_mtime + except FileNotFoundError: + continue + if mtime < cutoff: + logger.info("Removing stale staging directory: %s", entry) + shutil.rmtree(entry, ignore_errors=True) + + +class _ProgressReporter: + """Background thread that periodically logs extraction progress. + + The crane | tar pipeline is silent for minutes on large images. While it + runs, this reporter walks the staging directory every few seconds and logs + the current byte count and file count so users can see forward progress. + """ + + def __init__(self, watch_dir: Path, pull_ref: str, + interval: float = _PROGRESS_INTERVAL_SECONDS) -> None: + self._watch_dir = watch_dir + self._pull_ref = pull_ref + self._interval = interval + self._stop = threading.Event() + self._thread: threading.Thread | None = None + self._t0 = 0.0 + + def __enter__(self) -> "_ProgressReporter": + if self._interval <= 0: + return self + self._t0 = time.monotonic() + self._thread = threading.Thread( + target=self._run, name="image-extract-progress", daemon=True, + ) + self._thread.start() + return self + + def __exit__(self, exc_type, exc, tb) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=2.0) + + def _run(self) -> None: + while not self._stop.wait(self._interval): + size, count = _dir_size(self._watch_dir) + elapsed = time.monotonic() - self._t0 + logger.info( + "Extracting %s ... %s in %d files (%ds elapsed)", + self._pull_ref, _human_bytes(size), count, int(elapsed), + ) + + +def _dir_size(path: Path) -> tuple[int, int]: + """Return (total_bytes, file_count) for everything under *path*.""" + total = 0 + count = 0 + for root, _dirs, files in os.walk(path, onerror=lambda _e: None): + for name in files: + try: + total += os.lstat(os.path.join(root, name)).st_size + count += 1 + except OSError: + pass + return total, count + + +def _human_bytes(n: float) -> str: + for unit in ("B", "KB", "MB", "GB", "TB"): + if n < 1024: + return f"{n:.1f} {unit}" + n /= 1024 + return f"{n:.1f} PB" diff --git a/src/vuln_analysis/utils/intel_retriever.py b/src/vuln_analysis/utils/intel_retriever.py deleted file mode 100644 index 9786497a0..000000000 --- a/src/vuln_analysis/utils/intel_retriever.py +++ /dev/null @@ -1,257 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging -import os -from contextlib import asynccontextmanager - -import aiohttp - -from ..data_models.cve_intel import CveIntel -from ..data_models.cve_intel import CveIntelEpss -from ..data_models.cve_intel import CveIntelNvd -from ..data_models.cve_intel import CveIntelRhsa -from ..data_models.cve_intel import CveIntelUbuntu -from .clients.first_client import FirstClient -from .clients.ghsa_client import GHSAClient -from .clients.nvd_client import NVDClient -from .clients.rhsa_client import RHSAClient -from .clients.ubuntu_client import UbuntuClient - -logger = logging.getLogger(__name__) - - -class IntelRetriever: - """ - A class that retrieves details about CVEs (Common Vulnerabilities and Exposures) from NIST and CVE Details websites. - """ - - def __init__(self, - session: aiohttp.ClientSession, - nist_api_key: str | None = None, - ghsa_api_key: str | None = None, - lang_code: str = 'en', - max_retries: int = 5, - retry_on_client_errors: bool = True, - intel_source_timeout: int | None = None, - request_timeout: int = 30): - """ - Initialize the NISTCVERetriever with URL templates for vulnerability and CVE details. - """ - - # Create a shared session object for all requests - self._session = session - self._intel_source_timeout = intel_source_timeout - self._request_timeout = request_timeout - self._max_retries = max_retries - - self._nvd_client = NVDClient(api_key=os.environ.get('NVD_API_KEY', nist_api_key), - session=self._session, - lang_code=lang_code, - retry_count=max_retries, - retry_on_client_errors=retry_on_client_errors) - self._first_client = FirstClient(session=self._session, - retry_count=max_retries, - retry_on_client_errors=retry_on_client_errors) - self._ghsa_client = GHSAClient(api_key=os.environ.get('GHSA_API_KEY', ghsa_api_key), - session=self._session, - retry_count=max_retries, - retry_on_client_errors=retry_on_client_errors) - self._rhsa_client = RHSAClient(session=self._session, - retry_count=max_retries, - retry_on_client_errors=retry_on_client_errors) - self._ubuntu_client = UbuntuClient(session=self._session, - retry_count=max_retries, - retry_on_client_errors=retry_on_client_errors) - - @asynccontextmanager - async def _get_session(self, session: aiohttp.ClientSession | None = None): - - if (session is not None): - yield session - elif (self._session is not None): - yield self._session - else: - async with aiohttp.ClientSession() as session: - yield session - - def _get_timeout_description(self) -> str: - """ - Get a human-readable description of the effective timeout. - - Returns: - A string describing the timeout configuration - """ - if self._intel_source_timeout is not None: - return f"{self._intel_source_timeout} seconds (intel_source_timeout={self._intel_source_timeout})" - - approx_timeout = self._request_timeout * self._max_retries - return (f"~{approx_timeout} seconds " - f"(request_timeout={self._request_timeout} * max_retries={self._max_retries})") - - async def _with_timeout(self, coro): - """ - Execute a coroutine with timeout if configured. - - Args: - coro: The coroutine to execute - - Returns: - The result of the coroutine - """ - if self._intel_source_timeout is not None: - async with asyncio.timeout(self._intel_source_timeout): - return await coro - else: - return await coro - - async def _get_ghsa_intel(self, vuln_id: str) -> CveIntel: - """ - Gets the GHSA intel and returns an intel object. Must be called first!!!! - """ - - intel = CveIntel(vuln_id=vuln_id) - - try: - intel.ghsa = await self._with_timeout(self._ghsa_client.get_intel(vuln_id=intel.vuln_id)) - - except Exception as e: - if isinstance(e, asyncio.TimeoutError): - logger.error("GHSA intel retrieval timed out for %s after %s", vuln_id, self._get_timeout_description()) - else: - logger.error("Error fetching GitHub security advisory for %s : %s", vuln_id, e) - - return intel - - async def _get_nvd_intel(self, intel: CveIntel) -> CveIntelNvd | None: - - if (not intel.has_cve_id()): - logger.warning("Skipping NVD retrieval since '%s' does not have an associated CVE ID", intel.vuln_id) - return None - - try: - intel.nvd = await self._with_timeout(self._nvd_client.get_intel(cve_id=intel.cve_id)) - - return intel.nvd - - except Exception as e: - if isinstance(e, asyncio.TimeoutError): - logger.error("NVD intel retrieval timed out for %s after %s", - intel.vuln_id, - self._get_timeout_description()) - else: - logger.error("Error fetching NVD intel for %s : %s", intel.vuln_id, e) - - return None - - async def _get_ubuntu_intel(self, intel: CveIntel) -> CveIntelUbuntu | None: - - if (not intel.has_cve_id()): - logger.warning("Skipping Ubuntu retrieval since '%s' does not have an associated CVE ID", intel.vuln_id) - return None - - try: - intel.ubuntu = await self._with_timeout(self._ubuntu_client.get_intel(cve_id=intel.cve_id)) - - return intel.ubuntu - - except Exception as e: - if isinstance(e, asyncio.TimeoutError): - logger.error("Ubuntu intel retrieval timed out for %s after %s", - intel.vuln_id, - self._get_timeout_description()) - else: - logger.error("Error fetching Ubuntu security advisory for %s : %s", intel.vuln_id, e) - - return None - - async def _get_rhsa_intel(self, intel: CveIntel) -> CveIntelRhsa | None: - - if (not intel.has_cve_id()): - logger.warning("Skipping RHSA retrieval since '%s' does not have an associated CVE ID", intel.vuln_id) - return None - - try: - intel.rhsa = await self._with_timeout(self._rhsa_client.get_intel(cve_id=intel.cve_id)) - - return intel.rhsa - - except Exception as e: - if isinstance(e, asyncio.TimeoutError): - logger.error("RHSA intel retrieval timed out for %s after %s", - intel.vuln_id, - self._get_timeout_description()) - else: - logger.error("Error fetching Red Hat security advisory for %s : %s", intel.vuln_id, e) - - return None - - async def _get_epss_score(self, intel: CveIntel) -> CveIntelEpss | None: - - if (not intel.has_cve_id()): - logger.warning("Skipping EPSS Score retrieval since '%s' does not have an associated CVE ID", intel.vuln_id) - return None - - try: - intel.epss = await self._with_timeout(self._first_client.get_intel(cve_id=intel.cve_id)) - - return intel.epss - except Exception as e: - if isinstance(e, asyncio.TimeoutError): - logger.error("EPSS intel retrieval timed out for %s after %s", - intel.vuln_id, - self._get_timeout_description()) - else: - logger.error("Error fetching EPSS score for %s : %s", intel.vuln_id, e) - - return None - - async def retrieve(self, vuln_id: str) -> CveIntel: - """ - Asynchronously retrieve all relevant details for a given CVE ID. - - Parameters - ---------- - vuln_id : str - The CVE identifier. Can be GHSA or NVD - - Returns - ------- - dict - A dictionary containing all the retrieved CVE details. - """ - - # Build the intel objects. Must start with GHSA to get the cve_id - intel = await self._get_ghsa_intel(vuln_id=vuln_id) - - if (not intel.has_cve_id()): - logger.warning( - "Skipping additional intel retrieval beyond GHSA since '%s' " - "does not have an associated CVE ID", - vuln_id) - return intel - - # Run all the coroutines concurrently - coros = [ - self._get_nvd_intel(intel=intel), - self._get_ubuntu_intel(intel=intel), - self._get_rhsa_intel(intel=intel), - self._get_epss_score(intel=intel) - ] - - await asyncio.gather(*coros) - - return intel diff --git a/src/vuln_analysis/utils/intel_utils.py b/src/vuln_analysis/utils/intel_utils.py deleted file mode 100644 index 81cdfb9d2..000000000 --- a/src/vuln_analysis/utils/intel_utils.py +++ /dev/null @@ -1,156 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from packaging.version import InvalidVersion -from packaging.version import parse as parse_version -from pydpkg import Dpkg -from pydpkg.exceptions import DpkgVersionError - -from vuln_analysis.data_models.cve_intel import CveIntelNvd - - -def update_version(incoming_version, current_version, compare): - """ - Updates the version based on the comparison result. - - Args: - incoming_version (str): The incoming version from NIST. - current_version (str): The current version observed. - compare (str): The comparison operator, either "older" or "newer". - - Returns: - str: The updated version. - """ - - if compare == "older": - compare_func = lambda x, y: x < y - dpkg_compare = lambda x: x == -1 - elif compare == "newer": - compare_func = lambda x, y: x > y - dpkg_compare = lambda x: x == 1 - - if incoming_version is None: - return current_version - if current_version is None: - return incoming_version - try: - if compare_func(parse_version(incoming_version), parse_version(current_version)): - updated = incoming_version - else: - updated = current_version - except InvalidVersion: - # Failed PEP440 versioning; moving on to Debian - try: - compare_result = Dpkg.compare_versions(incoming_version, current_version) - updated = (incoming_version if dpkg_compare(compare_result) else current_version) - except DpkgVersionError: - # Debian versioning failed; moving on to alpha - updated = (incoming_version if compare_func(incoming_version, current_version) else current_version) - return updated - - -def parse_cpe(cpe): - """ - Parses a Common Platform Enumeration (CPE) string. - - Args: - cpe (str): The CPE string. - - Returns: - tuple: A tuple containing the vendor, package, version, and system information. - """ - vendor, package, version, system = None, None, None, None - split_cpe = cpe.split(":") - if len(split_cpe) > 3: - vendor = split_cpe[3] if split_cpe[3] != "*" and split_cpe[3] != "-" else None - if len(split_cpe) > 4: - package = split_cpe[4] if split_cpe[4] != "*" and split_cpe[4] != "-" else None - if len(split_cpe) > 5: - version = split_cpe[5] if split_cpe[5] != "*" and split_cpe[5] != "-" else None - if len(split_cpe) > 10: - system = split_cpe[10] if split_cpe[10] != "*" and split_cpe[5] != "-" else None - return (vendor, package, version, system) - - -def parse_config_vendors(configurations: list): - """ - Parses a list of configurations into a sorted list of vendors. - - Args: - configurations (list): A list of version information inputs. - - Returns: - list: A list of vendors - """ - vendors = set() - for config in configurations: - nodes = config.get("nodes", []) - for node in nodes: - for cpe_match in node.get("cpeMatch", []): - vendor, _, _, _ = parse_cpe(cpe_match.get("criteria", "*:*:*:*:*:*:*:*:*:*:*:*:*")) - if vendor: - vendors.add(vendor.replace("_", " ").title()) - return sorted(vendors) - - -def parse(configurations: list): - """ - Parses a list of configurations into a list of Configuration - (version information) objects. - - Args: - configurations (list): A list of version information inputs. - - Returns: - list: A list of configurationn objects. - """ - version_info = [] - cache = set() - for config in configurations: - nodes = config.get("nodes", []) - for node in nodes: - for cpe_match in node.get("cpeMatch", []): - vendor, package, version, system = parse_cpe(cpe_match.get("criteria", "*:*:*:*:*:*:*:*:*:*:*:*:*")) - ver_start_exclude = cpe_match.get("versionStartExcluding") - ver_start_include = update_version(version, cpe_match.get("versionStartIncluding"), "older") - ver_end_include = update_version(version, cpe_match.get("versionEndIncluding"), "newer") - ver_end_exclude = cpe_match.get("versionEndExcluding") - info = [ - vendor, - package, - system, - ver_start_exclude, - ver_end_exclude, - ver_start_include, - ver_end_include, - ] - - if tuple(info) in cache: - continue - if package: - if any(info[2:]): - cache.add(tuple(info)) - obj = CveIntelNvd.Configuration( - vendor=vendor, - package=package, - system=system, - versionStartExcluding=ver_start_exclude, - versionEndExcluding=ver_end_exclude, - versionStartIncluding=ver_start_include, - versionEndIncluding=ver_end_include, - ) - version_info.append(obj) - - return version_info diff --git a/src/vuln_analysis/utils/js_extended_parser.py b/src/vuln_analysis/utils/js_extended_parser.py deleted file mode 100644 index ba597eb87..000000000 --- a/src/vuln_analysis/utils/js_extended_parser.py +++ /dev/null @@ -1,103 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from typing import Any -from typing import List -from typing import Tuple - -import esprima -from langchain_community.document_loaders.parsers.language.javascript import JavaScriptSegmenter - -logger = logging.getLogger(__name__) - - -class ExtendedJavaScriptSegmenter(JavaScriptSegmenter): - """Extended JavaScript segmenter that handles shebang and ES optional chaining.""" - - def __init__(self, code: str): - """Initialize the segmenter with preprocessed code.""" - super().__init__(code) - # Skip files with shebang (#!) line since they typically contain non-standard JavaScript syntax - if code.startswith("#!"): - logger.warning("File contains a shebang line. Skipping parsing.") - self.skip_file = True - # esprima-python parser limitation: cannot handle JavaScript optional chaining syntax, - # fallback to regular property access - else: - self.skip_file = False - self.code = self.code.replace("?.", ".") - - def _parse_with_fallback(self) -> Any: - """Try to parse code as script first, then as module if that fails.""" - try: - logger.debug("Attempting to parse as a script...") - return esprima.parseScript(self.code, loc=True) - except esprima.Error: - logger.debug("Script parsing failed. Trying module parsing...") - try: - return esprima.parseModule(self.code, loc=True) - except esprima.Error as e: - logger.error("Module parsing failed: %s", str(e)) - return None - - def extract_functions_classes(self) -> List[str]: - """Extract functions, classes and exports from the code.""" - if self.skip_file: - return [] - - tree = self._parse_with_fallback() - if tree is None: - return [] - - functions_classes = [] - for node in tree.body: - # Handle direct function/class declarations - if isinstance(node, (esprima.nodes.FunctionDeclaration, esprima.nodes.ClassDeclaration)): - functions_classes.append(self._extract_code(node)) - # Handle exported declarations - elif isinstance(node, esprima.nodes.ExportNamedDeclaration): - if isinstance(node.declaration, (esprima.nodes.FunctionDeclaration, esprima.nodes.ClassDeclaration)): - functions_classes.append(self._extract_code(node)) - - return functions_classes - - def simplify_code(self) -> str: - """Simplify the code by replacing function/class bodies with comments.""" - if self.skip_file: - return self.code - - tree = self._parse_with_fallback() - if tree is None: - return self.code - - simplified_lines = self.source_lines[:] - indices_to_del: List[Tuple[int, int]] = [] - - for node in tree.body: - if isinstance(node, (esprima.nodes.FunctionDeclaration, esprima.nodes.ClassDeclaration)): - start, end = node.loc.start.line - 1, node.loc.end.line - simplified_lines[start] = f"// Code for: {simplified_lines[start]}" - indices_to_del.append((start + 1, end)) - elif isinstance(node, esprima.nodes.ExportNamedDeclaration): - if isinstance(node.declaration, (esprima.nodes.FunctionDeclaration, esprima.nodes.ClassDeclaration)): - start, end = node.loc.start.line - 1, node.loc.end.line - simplified_lines[start] = f"// Code for: {simplified_lines[start]}" - indices_to_del.append((start + 1, end)) - - for start, end in reversed(indices_to_del): - del simplified_lines[start:end] - - return "\n".join(line for line in simplified_lines) diff --git a/src/vuln_analysis/utils/justification_parser.py b/src/vuln_analysis/utils/justification_parser.py deleted file mode 100644 index 8186b4369..000000000 --- a/src/vuln_analysis/utils/justification_parser.py +++ /dev/null @@ -1,118 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from textwrap import dedent - -logger = logging.getLogger(__name__) - - -class JustificationParser: - JUSTIFICATION_LABEL_COL_NAME = "justification_label" - JUSTIFICATION_REASON_COL_NAME = "justification" - AFFECTED_STATUS_COL_NAME = "affected_status" - - JUSTIFICATION_PROMPT = dedent(""" - The summary provided below (delimited with XML tags), generated by the software agent named Vulnerability - Analysis for Container Security, evaluates a specific CVE (Common Vulnerabilities and Exposures) against the - backdrop of a software package or environment information. This information may include a Software Bill of - Materials (SBOM), source code, and dependency documentation among others related to environments such as - container images. Vulnerability Analysis for Container Security's role is to analyze this data to ascertain if the CVE - impacts the software environment, determining the necessity for a patch. - - Your task is to review Vulnerability Analysis for Container Security's analysis and classify the situation into - one of the following categories based on the described scenario: - - false_positive: The association between the software package and the CVE is incorrect, either due to an - erroneous package identification or a mismatched CVE. - - code_not_present: The software product is unaffected as it does not contain the code or library that harbors - the vulnerability. - - code_not_reachable: During runtime, the vulnerable code is not executed. - - requires_configuration: The exploitability of this issue depends on whether a specific configuration option is - enabled or disabled. In this case, the configuration is set in a manner that prevents exploitability. - - requires_dependency: Exploitability is contingent upon a missing dependency. - - requires_environment: A specific environment, absent in this case, is required for exploitability. - - compiler_protected: Exploitability hinges on the setting or unsetting of a compiler flag. In this case, the - flag is set in a manner that prevents exploitability. - - runtime_protected: Mechanisms are in place that prevent exploits during runtime. - - perimeter_protected: Protective measures block attacks at the physical, logical, or network perimeters. - - mitigating_control_protected: Implemented controls mitigate the likelihood or impact of the vulnerability. - - uncertain: Not enough information to determine the package's exploitability status. - - vulnerable: the package is actually vulnerable to the CVE and needs to be patched. - - Response only with the category name on the first line, and reasoning on the second line. - {summary} - """).strip("\n") - - RAW_TO_FINAL_JUSTIFICATION_MAP = { - "false_positive": "false_positive", - "code_not_present": "code_not_present", - "code_not_reachable": "code_not_reachable", - "requires_configuration": "requires_configuration", - "requires_dependency": "requires_dependency", - "requires_environment": "requires_environment", - "compiler_protected": "protected_by_compiler", - "runtime_protected": "protected_at_runtime", - "perimeter_protected": "protected_at_perimeter", - "mitigating_control_protected": "protected_by_mitigating_control", - "uncertain": "uncertain", - "vulnerable": "vulnerable", - } - - JUSTIFICATION_TO_AFFECTED_STATUS_MAP = { - "false_positive": "FALSE", - "code_not_present": "FALSE", - "code_not_reachable": "FALSE", - "requires_configuration": "FALSE", - "requires_dependency": "FALSE", - "requires_environment": "FALSE", - "protected_by_compiler": "FALSE", - "protected_at_runtime": "FALSE", - "protected_at_perimeter": "FALSE", - "protected_by_mitigating_control": "FALSE", - "uncertain": "UNKNOWN", - "vulnerable": "TRUE", - } - - async def _parse_justification(self, justifications: list[str]) -> dict[str, list[str]]: - labels: list[str] = [] - reasons: list[str] = [] - affected_status: list[str] = [] - - split_justifications = [j.split("\n") for j in justifications] - - for j in split_justifications: - if len(j) < 2: - raise ValueError( - f"Invalid justification format: {j}. Must be the label and the reason separated by a newline") - - justification_label_raw = j[0].strip() - justification_label = self.RAW_TO_FINAL_JUSTIFICATION_MAP.get(justification_label_raw, - justification_label_raw) - labels.append(justification_label) - reasons.append("\n".join(j[1:]).strip()) - try: - affected_status.append(self.JUSTIFICATION_TO_AFFECTED_STATUS_MAP[justification_label]) - except KeyError: - logger.error( - "Invalid justification label: '%s', setting affected_status='UNKNOWN'", - justification_label, - ) - affected_status.append("UNKNOWN") - - return { - self.JUSTIFICATION_LABEL_COL_NAME: labels, - self.JUSTIFICATION_REASON_COL_NAME: reasons, - self.AFFECTED_STATUS_COL_NAME: affected_status, - } diff --git a/src/vuln_analysis/utils/llm_engine_utils.py b/src/vuln_analysis/utils/llm_engine_utils.py deleted file mode 100644 index 78311b92a..000000000 --- a/src/vuln_analysis/utils/llm_engine_utils.py +++ /dev/null @@ -1,298 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import typing - -from ordered_set import OrderedSet - -from vuln_analysis.data_models.input import AgentMorpheusEngineInput -from vuln_analysis.data_models.output import AgentMorpheusEngineOutput -from vuln_analysis.data_models.output import AgentMorpheusOutput -from vuln_analysis.data_models.output import ChecklistItemOutput -from vuln_analysis.data_models.output import JustificationOutput -from vuln_analysis.data_models.state import AgentMorpheusEngineState -from vuln_analysis.data_models.state import LLMEngineSkipReason - -logger = logging.getLogger(__name__) - - -def determine_engine_skip_reasons(message: AgentMorpheusEngineInput, - missing_source_action: str) -> dict[str, LLMEngineSkipReason | None]: - """ - Determine skip reasons for vulnerabilities in LLM engine processing. - - Args: - message: The input message containing vulnerability and image information - missing_source_action: Action to take when source analysis is unavailable - - Returns: - Dictionary mapping vuln_id to skip reason (None = processable, LLMEngineSkipReason = reason to skip) - """ - assert message.info.intel is not None, "The input message must have intel information" - assert len(message.input.scan.vulns) == len(message.info.intel), \ - "The number of intel objects must match the number of vulnerabilities" - if message.info.vulnerable_dependencies is not None: - assert len(message.input.scan.vulns) == len(message.info.vulnerable_dependencies), \ - "The number of intel objects must match the number of vulnerabilities" - - skip_reasons = {} - vuln_ids = [vuln.vuln_id for vuln in message.input.scan.vulns] - - # Check for global skip conditions first - - # Check for SBOM package availability - if not message.info.sbom or not message.info.sbom.packages: - return {vuln_id: LLMEngineSkipReason.NO_SBOM_PACKAGES for vuln_id in vuln_ids} - - # Check for source_info and source vdb/index availability - has_source_info = message.input.image.source_info is not None - has_vdbs = (message.info.vdb and any( - [message.info.vdb.code_vdb_path, message.info.vdb.doc_vdb_path, message.info.vdb.code_index_path])) - - if not has_source_info or not has_vdbs: - if missing_source_action == "error": - raise RuntimeError( - "Source info or VDBs are missing and missing_source_action is set to 'error'. " - "To allow the pipeline to continue, change the setting to 'skip_agent' or 'continue_with_warning'.") - if missing_source_action == "skip_agent": - logger.warning("Skipping LLM engine processing for all vulns. Source info or VDBs are missing and " - "missing_source_action is set to 'skip_agent'.") - return {vuln_id: LLMEngineSkipReason.MISSING_SOURCE for vuln_id in vuln_ids} - if missing_source_action == "continue_with_warning": - logger.warning("Continuing with LLM engine processing despite missing source info or VDBs since " - "missing_source_action is set to 'continue_with_warning'. Analysis will be degraded.") - - # Per-vulnerability checks - - # Check for vulnerable dependencies - if message.info.vulnerable_dependencies is None: # VulnerableDependencyChecker was skipped, run all vulnerabilities - vdc_run_agent = [True for _ in vuln_ids] - else: - vdc_run_agent = [ - len(v.vulnerable_sbom_packages) > 0 or len(v.vuln_package_intel_sources) == 0 - for v in message.info.vulnerable_dependencies - ] - # Check for sufficient intel - sufficient_intel_run_agent = [intel.has_sufficient_intel_for_agent for intel in message.info.intel] - - # Determine if each vulnerability should be skipped - for i, vuln_id in enumerate(vuln_ids): - - # Ensure we only process the first instance of each vuln_id in case of duplicates - if vuln_id in skip_reasons: - continue - - has_vulnerable_deps = vdc_run_agent[i] - has_sufficient_intel = sufficient_intel_run_agent[i] - - if not has_sufficient_intel: - skip_reasons[vuln_id] = LLMEngineSkipReason.INSUFFICIENT_INTEL - elif not has_vulnerable_deps: - skip_reasons[vuln_id] = LLMEngineSkipReason.NO_VULNERABLE_PACKAGES - else: - skip_reasons[vuln_id] = None - - return skip_reasons - - -def preprocess_engine_input(message: AgentMorpheusEngineInput, - missing_source_action: str = "continue_with_warning") -> AgentMorpheusEngineState: - """ - Preprocess the input for the LLM Engine. - - Args: - message: The input message containing vulnerability and image information - missing_source_action: Action to take when source analysis is unavailable - - Returns: - AgentMorpheusEngineState containing inputs that should be processed by the LLM Engine - """ - assert message.info.intel is not None, "The input message must have intel information" - skip_reasons = determine_engine_skip_reasons(message, missing_source_action) - - # Get set of vuln_ids that should be processed by the agent (skip_reasons is None) - vulns_for_agent = {vuln_id for vuln_id, skip_reason in skip_reasons.items() if skip_reason is None} - - # Drop duplicate vuln_ids - unique_vulns = list(OrderedSet(vulns_for_agent)) - if len(vulns_for_agent) > len(unique_vulns): - logger.warning( - "Input contains duplicate vuln_ids. Passing only the first instance of each vuln_id to the LLM Engine.") - vulns_for_agent = unique_vulns - - # Filter intel for processable vulnerabilities, taking only the first instance of each vuln_id - filtered_intel = {} - for i in message.info.intel: - if i.vuln_id in vulns_for_agent and i.vuln_id not in filtered_intel: - filtered_intel[i.vuln_id] = i - - logger.info("Passing %d vuln_id(s) to the LLM Engine", len(vulns_for_agent)) - - return AgentMorpheusEngineState(code_vdb_path=message.info.vdb.code_vdb_path if message.info.vdb else None, - doc_vdb_path=message.info.vdb.doc_vdb_path if message.info.vdb else None, - code_index_path=message.info.vdb.code_index_path if message.info.vdb else None, - cve_intel=list(filtered_intel.values()), - skip_reasons=skip_reasons) - - -def parse_agent_morpheus_engine_output(vuln_id: str, - checklist_results: list[dict[str, typing.Any]], - summary: str, - justification: dict[str, str]) -> AgentMorpheusEngineOutput: - """ - Parse the output fields for a single vulnerability into an AgentMorpheusEngineOutput object. - """ - # Convert list of checklist item dicts to list of ChecklistItemOutput objects - checklist_output = [ - ChecklistItemOutput(input=item["input"], response=item["output"], intermediate_steps=item["intermediate_steps"]) - for item in checklist_results - ] - - # Combine justification model outputs into a single JustificationOutput object - justification_output = JustificationOutput(label=justification["justification_label"], - reason=justification["justification"], - status=justification["affected_status"]) - - return AgentMorpheusEngineOutput(vuln_id=vuln_id, - checklist=checklist_output, - summary=summary, - justification=justification_output) - - -def build_deficient_intel_output(vuln_id: str) -> AgentMorpheusEngineOutput: - summary = ("There is insufficient intel available to determine vulnerability. " - "This is either due to the CVE not existing or there is not enough " - "gathered intel for the agent to make an informed decision.") - justification = JustificationOutput(label="insufficient_intel", - reason="Insufficient intel available for CVE", - status="UNKNOWN") - return AgentMorpheusEngineOutput( - vuln_id=vuln_id, - checklist=[ - ChecklistItemOutput(input="Agent bypassed: Insufficient intel gathered. No checklist generated.", - response=summary, - intermediate_steps=None) - ], - summary=summary, - justification=justification) - - -def build_no_vuln_packages_output(vuln_id: str) -> AgentMorpheusEngineOutput: - summary = ("The VulnerableDependencyChecker did not find any vulnerable packages " - "or dependencies in the SBOM.") - justification = JustificationOutput(label="false_positive", - reason="No vulnerable packages or dependencies were detected in the SBOM.", - status="FALSE") - return AgentMorpheusEngineOutput( - vuln_id=vuln_id, - checklist=[ - ChecklistItemOutput(input="Agent bypassed: no vulnerable packages detected. Checklist not generated.", - response=("The VulnerableDependencyChecker did not find any vulnerable packages " - "or dependencies in the SBOM and so the agent was bypassed."), - intermediate_steps=None) - ], - summary=summary, - justification=justification) - - -def build_no_sbom_output(vuln_id: str) -> AgentMorpheusEngineOutput: - summary = ("There were no SBOM packages found for the image. This is either due to " - "an invalid SBOM input or empty SBOM. There is not enough information " - "to make an informed decision.") - justification = JustificationOutput(label="no_sbom_packages", - reason="No SBOM packages found for image.", - status="UNKNOWN") - return AgentMorpheusEngineOutput(vuln_id=vuln_id, - checklist=[ - ChecklistItemOutput( - input="Agent bypassed: no SBOM packages found. Checklist not generated.", - response=summary, - intermediate_steps=None) - ], - summary=summary, - justification=justification) - - -def build_missing_source_action_output(vuln_id: str) -> AgentMorpheusEngineOutput: - summary = ("Analysis is unavailable due to missing source_info, inaccessible repositories, or missing VDBs. " - "Intel was collected and dependency checking was performed but agent analysis was skipped. " - "Please ensure valid source_info for comprehensive code analysis.") - justification = JustificationOutput(label="missing_source", - reason="Missing source info, inaccessible repositories, or missing VDBs.", - status="UNKNOWN") - return AgentMorpheusEngineOutput( - vuln_id=vuln_id, - checklist=[ - ChecklistItemOutput( - input="Agent bypassed: analysis unavailable due to missing source_info, inaccessible repositories, " - "or missing VDBs. Checklist not generated.", - response=summary, - intermediate_steps=None) - ], - summary=summary, - justification=justification) - - -def postprocess_engine_output(message: AgentMorpheusEngineInput, - result: AgentMorpheusEngineState) -> AgentMorpheusOutput: - """ - Postprocess the engine output - - Args: - message: The input message containing vulnerability and image information - result: The LLM Engine output results - - Returns: - Final AgentMorpheusOutput object containing results for all vulnerabilities, including ones that were skipped - """ - input_vuln_ids = [vuln.vuln_id for vuln in message.input.scan.vulns] - - # For each vuln_id, get LLM Engine output if it exists - # or create placeholder output if it skipped the workflow - output: list[AgentMorpheusEngineOutput] = [] - engine_output_vuln_ids = list(result.final_summaries.keys()) - skip_reasons = result.skip_reasons - - for vuln_id in input_vuln_ids: - if vuln_id in engine_output_vuln_ids: - output.append( - parse_agent_morpheus_engine_output(vuln_id=vuln_id, - checklist_results=result.checklist_results[vuln_id], - summary=result.final_summaries[vuln_id], - justification=result.justifications[vuln_id])) - else: - # Handle skipped vulnerabilities based on skip reason - skip_reason = skip_reasons[vuln_id] - if skip_reason == LLMEngineSkipReason.NO_SBOM_PACKAGES: - output.append(build_no_sbom_output(vuln_id)) - elif skip_reason == LLMEngineSkipReason.MISSING_SOURCE: - output.append(build_missing_source_action_output(vuln_id)) - elif skip_reason == LLMEngineSkipReason.INSUFFICIENT_INTEL: - output.append(build_deficient_intel_output(vuln_id)) - elif skip_reason == LLMEngineSkipReason.NO_VULNERABLE_PACKAGES: - output.append(build_no_vuln_packages_output(vuln_id)) - elif skip_reason is None: - assert False, f"Vuln ID {vuln_id} was not skipped but there is no LLM Engine output." - else: - assert False, f"Vuln ID {vuln_id} has an unknown skip reason: {skip_reason}" - - for out in output: - logger.info("Vulnerability '%s' affected status: %s. Label: %s", - out.vuln_id, - out.justification.status, - out.justification.label) - - return AgentMorpheusOutput(input=message.input, info=message.info, output=output) diff --git a/src/vuln_analysis/utils/output_formatter.py b/src/vuln_analysis/utils/output_formatter.py deleted file mode 100644 index 6604236ee..000000000 --- a/src/vuln_analysis/utils/output_formatter.py +++ /dev/null @@ -1,687 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import ast -import html -import os - -from dateutil.parser import parse - -from vuln_analysis.data_models.output import AgentMorpheusOutput -from vuln_analysis.utils.data_utils import safe_getattr - - -def generate_vulnerability_reports(model_dict: AgentMorpheusOutput, output_dir): - """ - Creates a markdown file for each CVE ID in the markdown content dictionary. - - Parameters - ---------- - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - output_dir : str - The directory where the markdown files will be created. - - Returns - ------- - None - """ - markdown_content = _transform_to_markdown(model_dict) - - # Get the container directory name based on the image name and tag - container_dir_name = (model_dict.input.image.name.split("/")[-1].replace(".", "_") + "_" + - model_dict.input.image.tag.replace(".", "_")) - - # Create a subdirectory named after the prefixed container ID inside the output_dir - container_dir = os.path.join(output_dir, f"vulnerability_reports_{container_dir_name}") - - if not os.path.exists(container_dir): - os.makedirs(container_dir, exist_ok=True) - - for cve_id, content in markdown_content.items(): - file_name = f"{cve_id}.md" - file_path = os.path.join(container_dir, file_name) - with open(file_path, "w") as f: - f.write("\n".join(content)) - - -def _transform_to_markdown(model_dict: AgentMorpheusOutput): - """ - Convert JSON data to Markdown content. - - Parameters - ---------- - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - dict - Markdown content for each CVE ID. - """ - cve_ids = [vuln.vuln_id for vuln in model_dict.input.scan.vulns] - markdown_content = {} - for cve_id in cve_ids: - markdown_content[cve_id] = [] - - _add_header(markdown_content, model_dict) - _add_cve_intel(markdown_content, model_dict) - _add_vulnerability_analysis(markdown_content, model_dict) - _add_vulnerable_sboms(markdown_content, model_dict) - _add_table_of_contents(markdown_content, model_dict) - _add_checklist_info(markdown_content, model_dict) - _add_references(markdown_content, model_dict) - return markdown_content - - -def _add_header(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add header to Markdown content. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - input_image = model_dict.input.image - # iterate over a list of dict objects, with vuln_id and ghsa being 2 keys in each element - for output in model_dict.output: - cve_id = output.vuln_id - markdown_content[cve_id].append(f"# Vulnerability Analysis Report for {cve_id}") - markdown_content[cve_id].append(f"> **Container Analyzed:** `{input_image.name}:{input_image.tag}`\n\n") - # Only add SBOM info if it is a file location - if input_image.sbom_info.type == "file": - markdown_content[cve_id].append(f"> **SBOM Info:** `{input_image.sbom_info}`\n\n") - markdown_content[cve_id].append(f"> **Status:** {_get_expoiltability_text(output.justification.status)}") - - -def _add_cve_intel(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add CVE intelligence details to Markdown content. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - for intel_obj in model_dict.info.intel: - cve_id = intel_obj.vuln_id - cve_description = _normalize_content(_get_cve_description(intel_obj)) - - markdown_content[cve_id].append( - f"## {_process_cve_title(cve_id, intel_obj)} ") - # add a disputed tag if nvd.disputed is true - safe_getattr(intel_obj, "nvd.disputed", None) and markdown_content[cve_id].append( - f'\n**`Disputed`**[source](https://nvd.nist.gov/vuln/detail/{cve_id})' - ) - - cve_description and markdown_content[cve_id].append(f"\n\n {cve_description} [references](#ref) \n") - - cve_severity_ratings = _get_cve_severity(intel_obj) - cve_severity_ratings and markdown_content[cve_id].append(f"\n{cve_severity_ratings} \n") - markdown_content[cve_id].append(add_epss_score(intel_obj)) - - -def _get_cve_severity(intel_obj): - """ - Extract CVE severity and construct a markdown table. - - Parameters - ---------- - intel_obj : object - An object containing CVE severity details from multiple sources. - - Returns - ------- - str - A markdown-formatted table summarizing CVE severity and vector strings. - """ - # Extracting data from GHSA - ghsa_severity = safe_getattr(intel_obj, "ghsa.severity", "N/A").upper() - ghsa_cvss_score = safe_getattr(intel_obj, "ghsa.cvss.score", "N/A") - ghsa_vector = safe_getattr(intel_obj, "ghsa.cvss.vector_string", "N/A") - ghsa_published_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "ghsa.published_at", "N/A")) - ghsa_updated_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "ghsa.updated_at", "N/A")) - - # Extracting data from NVD - nvd_severity = safe_getattr(intel_obj, "nvd.cvss_severity", "N/A").upper() - nvd_cvss_score = safe_getattr(intel_obj, "nvd.cvss_base_score", "N/A") - nvd_vector = safe_getattr(intel_obj, "nvd.cvss_vector", "N/A") - nvd_published_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "nvd.published_at", "N/A")) - nvd_updated_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "nvd.updated_at", "N/A")) - - # Extracting data from Red Hat (RHSA) - rhsa_severity = safe_getattr(intel_obj, "rhsa.threat_severity", "N/A").upper() - rhsa_cvss_score = safe_getattr(intel_obj, "rhsa.cvss3.cvss3_base_score", "N/A") - rhsa_vector = safe_getattr(intel_obj, "rhsa.cvss3.cvss3_scoring_vector", "N/A") - rhsa_published_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "rhsa.public_date", "N/A")) - rhsa_updated_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "rhsa.updated_at", "N/A")) - - # Extracting data from Ubuntu - ubuntu_severity = safe_getattr(intel_obj, "ubuntu.impact.baseMetricV3.cvssV3.baseSeverity", "N/A").upper() - ubuntu_cvss_score = safe_getattr(intel_obj, "ubuntu.impact.baseMetricV3.cvssV3.baseScore", "N/A") - ubuntu_vector = safe_getattr(intel_obj, "ubuntu.impact.baseMetricV3.cvssV3.vectorString", "N/A") - ubuntu_published_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "ubuntu.published", "N/A")) - ubuntu_updated_at = _convert_timestamp_to_human_readable(safe_getattr(intel_obj, "ubuntu.updated_at", "N/A")) - - # Constructing the markdown table - markdown_table = ["### Severity and CVSS Score\n"] - markdown_table.append("| Source | Severity | CVSS Score | Vector | Published At | Last Updated |\n") - markdown_table.append("|---|---|---|---|---|---|\n") - markdown_table.append( - f"| {_get_source_url('GHSA', intel_obj)} |{ghsa_severity}|{ghsa_cvss_score}|{ghsa_vector}|{ghsa_published_at}|{ghsa_updated_at}\n" - ) - markdown_table.append( - f"| {_get_source_url('NVD', intel_obj)} |{nvd_severity}|{nvd_cvss_score}|{nvd_vector}|{nvd_published_at}|{nvd_updated_at}|\n" - ) - markdown_table.append( - f"| {_get_source_url('RHSA', intel_obj)} |{rhsa_severity}|{rhsa_cvss_score}|{rhsa_vector}|{rhsa_published_at}|{rhsa_updated_at}|\n" - ) - markdown_table.append( - f"| {_get_source_url('Ubuntu', intel_obj)} |{ubuntu_severity}|{ubuntu_cvss_score}|{ubuntu_vector}|{ubuntu_published_at}|{ubuntu_updated_at}|\n" - ) - - return "".join(markdown_table) - - -def add_epss_score(intel_obj): - """ - Retrieve EPSS data for the cve. - - Parameters - ---------- - intel_obj : object - An object with CVE information from multiple sources. - - Returns - ------- - str - A markdown-formatted string with a table for EPSS data - """ - epss = safe_getattr(intel_obj, "epss.epss", None) - epss_percentile = safe_getattr(intel_obj, "epss.percentile", None) - if epss or epss_percentile: - # Create a markdown table for EPSS data - table = "### EPSS Score\n" - table += "| EPSS | Percentile |\n| --- | --- |\n" - table += f"| {epss} | {epss_percentile} |\n" - - # Append the markdown table to the specified CVE ID - return table - return "" - - -def _get_cve_description(intel_obj): - """ - Retrieve a description for a CVE from multiple sources. - - Parameters - ---------- - intel_obj : object - An object containing CVE details from various sources (e.g., GHSA, NVD). - - Returns - ------- - str or None - A string description of the CVE or None if no description is found. - """ - if intel_obj.ghsa and intel_obj.ghsa.description: - return intel_obj.ghsa.description - if intel_obj.rhsa and intel_obj.rhsa.details: - return intel_obj.rhsa.details - if intel_obj.ubuntu and intel_obj.ubuntu.description: - return intel_obj.ubuntu.description - if intel_obj.nvd and intel_obj.nvd.cve_description: - return intel_obj.nvd.cve_description - - return None - - -def _add_table_of_contents(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add a table of contents for checklists per CVE. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - for entry in model_dict.output: - cve_id = entry.vuln_id - checklist = entry.checklist - markdown_content[cve_id].append("### Checklist ") - if checklist: - steps = { - item.input.split(":")[0]: - [intermediate_step.tool_name for intermediate_step in item.intermediate_steps or []] - for item in checklist - } - for i, (step, intermediate_steps) in enumerate(steps.items(), start=1): - markdown_content[cve_id].append(f"{i}. [{step}](#checklist-step-{i})\n") - for j, intermediate_step in enumerate(intermediate_steps, start=1): - if intermediate_step == "_Exception": - continue - markdown_content[cve_id].append(f"\t {j}. [{intermediate_step}](#checklist-step-{i}.{j})") - - -def _add_checklist_info(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add detailed information for checklists associated with each CVE. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - for entry in model_dict.output: - cve_id = entry.vuln_id - checklist = entry.checklist - if checklist: - markdown_content[cve_id].append(f"\n## Checklist Details ") - for i, item in enumerate(checklist, start=1): - input_text = item.input - response = item.response - markdown_content[cve_id].append( - f"\n## Step {i} : {input_text.split(':')[0]}\n" - ) - markdown_content[cve_id].append(f"\n> **Input**: *{input_text}*") - markdown_content[cve_id].append(f"\n> **Response**: *{response}*") - intermediate_steps = item.intermediate_steps - if intermediate_steps: - for j, step in enumerate(intermediate_steps, start=1): - tool_name = step.tool_name - if tool_name == "_Exception": - continue # skip intermediate steps with _Exception keyword as the tool_name - action_log = step.action_log - tool_input = step.tool_input - tool_output = step.tool_output - markdown_content[cve_id].append( - f"\n### Step {i}.{j} : *{tool_name}*" - ) - markdown_content[cve_id].append(f"\n\n#### Action Log \n
{action_log} 
") - markdown_content[cve_id].append(f"\n\n#### Tool Input \n
{tool_input} 
") - markdown_content[cve_id].append( - f"\n\n#### Tool Output \n{_process_tool_output(tool_output)}\n\n") - if j != len(intermediate_steps): - markdown_content[cve_id].append("\n[back to top](#checklist-toc)") - elif not intermediate_steps and i != len(checklist): - markdown_content[cve_id].append("\n[back to top](#checklist-toc)") - - markdown_content[cve_id].append("\n[back to top](#checklist-toc)") - - -def _process_tool_output(content): - """ - Process the tool output content and return a renderable markdown object. - - Parameters - ---------- - content : dict or str - JSON or string object representing tool output. - - Returns - ------- - str - Renderable markdown representation of the tool output. - """ - if isinstance(content, str): - try: - # Check if the string contains a list - content = ast.literal_eval(content) - # If not, raise an error to return the content as is - if not isinstance(content, list): - raise ValueError(f"Handling for tool output type {type(content)} is not implemented.") - except Exception: - return f"
{content}
" - if isinstance(content, list): - # Check if the list contains source documents - if len(content) > 0 and isinstance(content[0], dict) and content[0].get("type", None) == "Document": - content = {"source_documents": content} - # If not, format and return the list as is - else: - list_md = '\n'.join(f'- {item}' for item in content) or [] - return f"
{list_md}
" - - # Format source documents - result = content.get("result", "") - content_markdown = "" - if result: - content_markdown += f"
{result}
" - metadata_keys = set() - for item in content.get("source_documents", []): - if "metadata" in item: - metadata_keys.update(item["metadata"].keys()) - table = ("\n\n Source Documents \n\n | ID | Type | " + " |... ".join(metadata_keys) + " | Page Content |\n") - table += "| --- " * (len(metadata_keys) + 3) + "|\n" - for item in content.get("source_documents", []): - row = [str(item.get("id", "")), str(item.get("type", ""))] - for key in metadata_keys: - row.append(str(item.get("metadata", {}).get(key, ""))) - # Retrieve and sanitize page content - page_content = item.get("page_content", "") - # # Remove extra whitespace - page_content = " ".join(page_content.split()) - # Escape special characters - page_content = html.escape(page_content).replace('|', '\\|').replace('*', '\\*').replace('_', '\\_') - row.append(f"
View Content{_format_as_markdown(page_content)}
") - table += "| " + " | ".join(row) + " |\n" - return content_markdown + table - - -def _add_vulnerability_analysis(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add vulnerability analysis details to Markdown content. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - for entry in model_dict.output: - cve_id = entry.vuln_id - summary = entry.summary - justification = entry.justification - exploitability = justification.status - exploitability_text = _get_expoiltability_text(exploitability) - markdown_content[cve_id].append("## Vulnerability Analysis\n") - markdown_content[cve_id].append( - f"### Summary ({exploitability_text})\n{summary}") - markdown_content[cve_id].append(f"\n### Justification \n") - markdown_content[cve_id].append(f"\n>label: {justification.label}") - markdown_content[cve_id].append(f"\n{justification.reason}") - - -def _add_vulnerable_sboms(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add information about vulnerable SBOM dependencies to Markdown content. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing vulnerability information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - if model_dict.info.vulnerable_dependencies is None: - return - - vulnerable_sboms_per_cve_id = { - i.vuln_id: i.vulnerable_sbom_packages - for i in model_dict.info.vulnerable_dependencies - } - - for cve_id, vulnerable_sboms in vulnerable_sboms_per_cve_id.items(): - if len(vulnerable_sboms) == 0: - continue - markdown_content[cve_id].append("\n---\n### Vulnerable SBOM Dependencies \n") - - # Add table header - markdown_content[cve_id].append( - "\n| SBOM Name | SBOM Version | Dependency Name | Dependency Version | Relation | System |\n| --- | --- | --- | --- | --- | --- |" - ) - - # Create a set to track unique rows - unique_rows = set() - - # Add rows for each SBOM entry - for sbom in vulnerable_sboms: - dependency = sbom.vulnerable_dependency_package - row = f"| {sbom.name} | {sbom.version} | {dependency.name} | {dependency.version} | {dependency.relation} | {dependency.system} |" - # Check if the row is unique before adding it - if row not in unique_rows: - markdown_content[cve_id].append(row) - unique_rows.add(row) # Add the row to the set of unique rows - - -def _add_references(markdown_content, model_dict: AgentMorpheusOutput): - """ - Add references for a CVE from all available sources in the intel object. - - Parameters - ---------- - markdown_content : dict - Markdown content for each CVE ID. - model_dict : AgentMorpheusOutput - JSON data containing references information. - - Returns - ------- - None - This function modifies `markdown_content` in place. - """ - for intel_obj in model_dict.info.intel: - cve_id = intel_obj.vuln_id - markdown_content[cve_id].append( - "\n---\n## References \nHere are key references for further details on this vulnerability:" - ) - references = set() - for source in ["ghsa", "nvd", "rhsa", "ubuntu"]: - references.update(_get_formatted_references(safe_getattr(intel_obj, f"{source}.references", []))) - - references_content = "\n".join(["- " + reference for reference in list(references)]) - markdown_content[cve_id].append(references_content) - # Add a back-to-top link after the references - markdown_content[cve_id].append("\n[back to top](#cve-intro)\n") - - -def _get_formatted_references(references: list): - """ - Format and clean up references by splitting multiline strings into individual entries. - - Parameters - ---------- - references : list - A list of reference strings or objects. - - Returns - ------- - list - A cleaned and formatted list of references. - """ - for index, element in enumerate(references): - # Check if the element is a string and contains '\n' - if isinstance(element, str) and "\n" in element: - # Split the string by '\n' and replace it in the list - split_strings = element.split("\n") - references[index:index + 1] = split_strings - return references - - -def _get_expoiltability_text(exploitability: str): - """ - Get a color-coded text label based on the exploitability status. - - Parameters - ---------- - exploitability : str - Exploitability status (e.g., "TRUE", "FALSE", or unknown). - - Returns - ------- - str - HTML-formatted string with a color-coded label indicating exploitability. - """ - color = "#9E9E9E" # Gray - exploitability_text = "Exploitability Unknown" - if exploitability == "TRUE": - color = "#F44336" # Red - exploitability_text = "Exploitable" - elif exploitability == "FALSE": - color = "#4CAF50" # Green - exploitability_text = "Not Exploitable" - return f"{exploitability_text}" - - -def _normalize_content(input_data: str | list): - """ - Normalize input data to handle different formats (string or list). - - Parameters - ---------- - input_data : str or list - The input data to normalize, which can be a string or a list of - strings. - - Returns - ------- - str - A string representing the normalized input. - """ - if isinstance(input_data, list): - input_data = "\n".join([f"- {item}" for item in input_data]) - - return input_data - - -def _format_as_markdown(content: str | list): - """ - Format content as Markdown if it contains common Markdown indicators. - - Parameters - ---------- - content : str or list - The content to format, which can be a string or a list of strings. - - Returns - ------- - str - The formatted content. If Markdown indicators are detected, the content - is wrapped in triple backticks (code block). Otherwise, it is returned - as-is. - """ - content = _normalize_content(content) - # Simple check for common markdown characters - markdown_indicators = [ - "\n", # Newline - "\n#", # Header - ">", # Blockquote - "|" # Table formatting - ] - - if any(indicator in content for indicator in markdown_indicators): - # Return the string wrapped in triple backticks (code block) - return f"```{content}```" - - # If it's just a normal string, return it as-is - return content - - -def _convert_timestamp_to_human_readable(timestamp: str | None): - """ - Convert a timestamp string into a human-readable format. - - Parameters - ---------- - timestamp : str or None - The timestamp string in various formats. - - Returns - ------- - str or None - The formatted timestamp or None if conversion fails. - """ - try: - return parse(timestamp).strftime("%Y-%m-%d") - except ValueError: - return timestamp or "N/A" - - -def _process_cve_title(cve_id, intel_obj): - """ - Process and generate a title for a CVE entry. - - Parameters - ---------- - cve_id : str - The CVE ID or GHSA ID associated with the vulnerability. - intel_obj : object - JSON object containing vulnerability details. - - Returns - ------- - str - A formatted title string for the CVE entry. - """ - if "GHSA" in cve_id: - return f"{cve_id} \n> CVE-ID: {safe_getattr(intel_obj, 'ghsa.cve_id')}" - elif "CVE" in cve_id: - ghsa_id = safe_getattr(intel_obj, 'ghsa.ghsa_id') - return f"{cve_id} \n> GHSA-ID: {safe_getattr(intel_obj, 'ghsa.ghsa_id')}" if ghsa_id else cve_id - - -def _get_source_url(source, intel_obj): - """ - Process and generate a title for a CVE entry. - - Parameters - ---------- - cve_id : str - The CVE ID or GHSA ID associated with the vulnerability. - intel_obj : object - JSON object containing vulnerability details. - - Returns - ------- - str - A formatted title string for the CVE entry. - """ - if source == "GHSA": - return f"[**GHSA**](https://github.com/advisories/{safe_getattr(intel_obj, 'ghsa.ghsa_id')})" - elif source == "NVD": - return f"[**NVD**](https://nvd.nist.gov/vuln/detail/{safe_getattr(intel_obj, 'nvd.cve_id')})" - elif source == "RHSA": - return f"[**RHSA**](https://access.redhat.com/security/cve/{safe_getattr(intel_obj, 'rhsa.name')})" - elif source == "Ubuntu": - return f"[**Ubuntu**](https://ubuntu.com/security/{safe_getattr(intel_obj, 'ubuntu.id')})" diff --git a/src/vuln_analysis/utils/pre_triage.py b/src/vuln_analysis/utils/pre_triage.py new file mode 100644 index 000000000..6dcaa5850 --- /dev/null +++ b/src/vuln_analysis/utils/pre_triage.py @@ -0,0 +1,1883 @@ +"""Deterministic pre-triage for the Exploitability Analyzer. + +Performs Steps 0-1 of the exploitability analysis deterministically: + 1. Parse vulnerability and container reports (regex on known markdown schemas) + 2. Discover the container filesystem layout (fixed probe recipe) + 3. Verify the vulnerable package on the filesystem (ecosystem-specific checks) + 4. Compare installed version against the vulnerable range + 5. Check distro backports (Debian/Ubuntu system packages) + 6. Decide: early exit (NOT APPLICABLE / ENVIRONMENT ISOLATED) or CONTINUE + +If the decision is early exit, the caller generates a template report with zero +LLM calls. If CONTINUE, the enriched PreTriageResult is formatted and passed +directly to the Phase 2 deep-analysis agent. +""" + +from __future__ import annotations + +import json +import logging +import os +import re +import subprocess +from pathlib import Path + +from packaging.version import InvalidVersion, Version + +from vuln_analysis.data_models.exploitability_analyzer import ( + AdditionalPackageResult, + AffectedPackageEntry, + AffectedRange, + ExploitabilityAnalyzerInput, + FilesystemLayout, + PackageInstallation, + PackageVerification, + ParsedContainerReport, + ParsedVulnReport, + PreTriageResult, +) +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Direct filesystem helpers (bypass LLM tool truncation limits) +# --------------------------------------------------------------------------- + + +def _raw_listdir(directory: str) -> list[str]: + """List filenames in a directory directly.""" + try: + return sorted(os.listdir(directory)) + except (OSError, PermissionError): + return [] + + +def _safe_read_text(path: str | Path, max_bytes: int = 0) -> str: + """Read a file as text, returning empty string on error.""" + try: + content = Path(path).read_text(encoding="utf-8", errors="ignore") + if max_bytes and len(content) > max_bytes: + return content[:max_bytes] + return content + except (OSError, PermissionError): + return "" + + +def _glob_find(pattern: str, root: str) -> list[str]: + """Return relative paths matching *pattern* under *root*.""" + root_path = Path(root) + results: list[str] = [] + try: + for match in root_path.glob(pattern): + if match.is_file() or match.is_dir(): + try: + results.append(str(match.relative_to(root_path))) + except ValueError: + results.append(str(match)) + except (OSError, PermissionError): + pass + return results + + +# --------------------------------------------------------------------------- +# Package name aliases (CVE report name -> filesystem search names) +# --------------------------------------------------------------------------- + +PACKAGE_ALIASES: dict[str, list[str]] = { + "cpython": ["python", "python3"], + "pytorch": ["torch"], + "beautifulsoup4": ["beautifulsoup4", "bs4"], + "opencv-python": ["opencv-python", "cv2"], + "opencv-python-headless": ["opencv-python-headless", "cv2"], + "pillow": ["pillow", "pil"], +} + +_STDLIB_KEYWORDS = frozenset({ + "module", "stdlib", "standard library", "built-in", +}) + +_STDLIB_MODULES = frozenset({ + "http.client", "http.server", "http.cookiejar", "http.cookies", + "tarfile", "zipfile", "xml.etree", "xml.sax", "xml.dom", + "email", "urllib", "urllib.parse", "urllib.request", + "ssl", "hashlib", "hmac", "ctypes", "multiprocessing", + "asyncio", "socket", "ftplib", "imaplib", "poplib", "smtplib", + "logging", "tempfile", "shutil", "os", "subprocess", "pathlib", + "json", "csv", "configparser", "sqlite3", "dbm", +}) + + +# =================================================================== +# 1. Report Parsers +# =================================================================== + + +def parse_vulnerability_report(content: str) -> ParsedVulnReport: + """Parse structured fields from a vulnerability report markdown file.""" + cve_id = extract_markdown_table_field(content, "CVE ID") or "UNKNOWN" + package_raw = extract_markdown_table_field(content, "Affected Package") or "" + vulnerable_versions = extract_markdown_table_field(content, "Vulnerable Versions") or "" + fixed_in = extract_markdown_table_field(content, "Fixed In") or "" + cvss_raw = extract_markdown_table_field(content, "CVSS v3.1") or "" + cvss_vector = extract_markdown_table_field(content, "CVSS Vector") or None + cwe = extract_markdown_table_field(content, "CWE") or None + + cvss_score: float | None = None + cvss_severity: str | None = None + cvss_m = re.match(r"([\d.]+)\s*(\w+)?", cvss_raw) + if cvss_m: + try: + cvss_score = float(cvss_m.group(1)) + except ValueError: + pass + cvss_severity = cvss_m.group(2) + + package_name, ecosystem_hint = _parse_package_field(package_raw) + + vulnerable_function = _extract_vulnerable_function(content) + + affected_ranges = _parse_affected_ranges(content) + + affected_packages = _parse_affected_packages(content) + + return ParsedVulnReport( + cve_id=cve_id, + package_name=package_name, + package_raw=package_raw, + ecosystem_hint=ecosystem_hint, + vulnerable_versions=vulnerable_versions, + fixed_in=fixed_in, + cvss_score=cvss_score, + cvss_severity=cvss_severity, + cvss_vector=cvss_vector, + cwe=cwe, + vulnerable_function=vulnerable_function, + affected_ranges=affected_ranges, + affected_packages=affected_packages, + ) + + +def _parse_package_field(raw: str) -> tuple[str, str | None]: + """Split 'CPython (http.client module)' into ('cpython', 'http.client module').""" + hint: str | None = None + paren_m = re.search(r"\((.+?)\)", raw) + if paren_m: + hint = paren_m.group(1).strip() + base = re.sub(r"\s*\(.*?\)", "", raw).strip() + cleaned = base.strip("`").strip().lower() + + words = cleaned.split() + if len(words) <= 1: + return cleaned, hint + + noise = {"python", "module", "standard", "library", "the", "in", "of", "for"} + candidates = [w for w in words if w not in noise] + if candidates: + return candidates[0], hint + + return cleaned, hint + + +def _extract_vulnerable_function(content: str) -> str | None: + """Extract the vulnerable function from the Technical Details section.""" + m = re.search( + r"\*\*Function:?\*\*\s*[`]*([^`\n]+)[`]*", + content, + re.MULTILINE, + ) + if m: + return m.group(1).strip() + return None + + +def _parse_affected_ranges(content: str) -> list[AffectedRange]: + """Parse the 'Affected Version Ranges' table(s) if present.""" + header_m = re.search( + r"#{2,3}\s*Affected Version Ranges", + content, + re.IGNORECASE, + ) + if not header_m: + return [] + + section = content[header_m.end():] + next_heading = re.search(r"\n#{1,3}\s+[^#]", section) + if next_heading: + section = section[: next_heading.start()] + + ranges: list[AffectedRange] = [] + in_table = False + rows_after_separator = 0 + + for line in section.splitlines(): + stripped = line.strip() + if not stripped.startswith("|"): + if in_table: + in_table = False + rows_after_separator = 0 + continue + + if re.match(r"\|[-\s|]+\|", stripped): + in_table = True + rows_after_separator = 0 + continue + + if not in_table: + in_table = True + continue + + rows_after_separator += 1 + cells = [c.strip().strip("*") for c in stripped.split("|")] + cells = [c for c in cells if c] + if len(cells) < 3: + continue + + branch = cells[0].strip() + vuln_range = cells[1].strip() + first_fixed = cells[2].strip() + + lower, upper = _split_range(vuln_range) + ranges.append(AffectedRange( + branch=branch, + lower=lower, + upper=upper, + first_fixed=first_fixed, + )) + + return ranges + + +def _split_range(range_str: str) -> tuple[str, str]: + """Split '>= 3.12.0, < 3.12.11' into ('>= 3.12.0', '< 3.12.11').""" + parts = [p.strip() for p in range_str.split(",")] + lower = parts[0] if parts else range_str + upper = parts[1] if len(parts) > 1 else "" + return lower, upper + + +def _parse_affected_packages(content: str) -> list[AffectedPackageEntry]: + """Parse the ``### Affected Packages`` table if present. + + Returns ``[]`` when the section is absent (backward compat with old reports). + """ + header_m = re.search( + r"#{2,3}\s*Affected Packages", + content, + re.IGNORECASE, + ) + if not header_m: + return [] + + section = content[header_m.end():] + next_heading = re.search(r"\n#{1,3}\s+[^#]", section) + if next_heading: + section = section[: next_heading.start()] + + entries: list[AffectedPackageEntry] = [] + in_table = False + + for line in section.splitlines(): + stripped = line.strip() + if not stripped.startswith("|"): + if in_table: + in_table = False + continue + + if re.match(r"\|[-\s|]+\|", stripped): + in_table = True + continue + + if not in_table: + in_table = True + continue + + cells = [c.strip().strip("*`") for c in stripped.split("|")] + cells = [c for c in cells if c] + if len(cells) < 4: + continue + + package = cells[0].strip() + ecosystem = cells[1].strip() or None + vuln_versions = cells[2].strip() + fixed_in = cells[3].strip() + + if not package or package.lower() in ("package", "---"): + continue + + entries.append(AffectedPackageEntry( + package=package, + ecosystem=ecosystem, + vulnerable_versions=vuln_versions, + fixed_in=fixed_in, + )) + + return entries + + +def parse_container_report(content: str) -> ParsedContainerReport: + """Parse structured fields from a container analysis report.""" + image_name = extract_markdown_table_field(content, "Image Name") or "" + image_tag = extract_markdown_table_field(content, "Image Tag") + if not image_tag and ":" in image_name.rsplit("/", 1)[-1]: + image_name, image_tag = image_name.rsplit(":", 1) + container_type = extract_markdown_table_field(content, "Container Type") or "Unknown" + primary_language = extract_markdown_table_field(content, "Primary Language") or "Unknown" + base_os_raw = extract_markdown_table_field(content, "Base Image") or "" + if not base_os_raw: + base_os_raw = extract_markdown_table_field(content, "Base OS") or "Unknown" + runtime_user = _extract_runtime_user(content) + default_env = _extract_default_environment(content) + entry_points = _extract_entry_points(content) + + return ParsedContainerReport( + image_name=image_name, + image_tag=image_tag, + container_type=container_type, + primary_language=primary_language, + base_os=base_os_raw, + default_environment=default_env, + runtime_user=runtime_user, + entry_points=entry_points, + ) + + +def _extract_runtime_user(content: str) -> str: + for pattern in [ + r"\*\*USER directive\*\*\s*\|\s*(.+?)(?:\s*\||\s*$)", + r"USER directive\s*\|\s*(.+?)(?:\s*\||\s*$)", + ]: + m = re.search(pattern, content, re.MULTILINE) + if m: + val = m.group(1).strip() + if "not set" in val.lower() or "root" in val.lower(): + return "root" + return val + + for pattern in [ + r"\*\*Runs as non-root\*\*\s*\|\s*(.+?)(?:\s*\||\s*$)", + r"Runs as non-root\s*\|\s*(.+?)(?:\s*\||\s*$)", + ]: + m2 = re.search(pattern, content, re.MULTILINE) + if m2 and m2.group(1).strip().lower().startswith("no"): + return "root" + + return "unknown" + + +def _extract_default_environment(content: str) -> str | None: + env_section = re.search( + r"#{2,3}\s*Environment Isolation(.*?)(?=\n## [^#]|\Z)", + content, + re.MULTILINE | re.DOTALL, + ) + if not env_section: + return None + + block = env_section.group(1) + + m = re.search( + r"\|\s*(\w+)\s*\|\s*conda\s*\|\s*Yes\s*\|", + block, + re.IGNORECASE, + ) + if m: + return m.group(1).strip() + + m2 = re.search(r"conda activate\s+(\S+)", block) + if m2: + return m2.group(1).strip() + + return None + + +def _extract_entry_points(content: str) -> list[str]: + attack_section = re.search( + r"#{2,3}\s*Entry Points(.*?)(?=^#{2,3}\s|\Z)", + content, + re.MULTILINE | re.DOTALL, + ) + if not attack_section: + return [] + + block = attack_section.group(1) + entries: list[str] = [] + for line in block.splitlines(): + stripped = line.strip() + if not stripped.startswith("|") or stripped.startswith("||") or "---" in stripped: + continue + cells = [c.strip() for c in stripped.split("|")] + cells = [c for c in cells if c] + if len(cells) >= 2 and cells[0] not in ("Entry Point", "---"): + entries.append(cells[0]) + return entries + + +# =================================================================== +# 2. Package Name Resolution +# =================================================================== + + +_LANG_SUFFIXES = ("-python", "-py", "-python3", "-py3", "-js", "-java", "-go", "-rust") +_LANG_PREFIXES = ("python-", "py-", "python3-", "py3-", "node-", "golang-", "rust-") + +_GO_MODULE_PREFIXES = ( + "github.com/", + "golang.org/", + "gopkg.in/", + "k8s.io/", + "sigs.k8s.io/", + "go.uber.org/", + "go.opencensus.io/", + "google.golang.org/", +) + + +def resolve_package_names( + package_name: str, + ecosystem_hint: str | None, +) -> tuple[list[str], str]: + """Resolve a CVE package name to filesystem search names and ecosystem. + + Returns ``(search_names, detected_ecosystem)``. + """ + lower = package_name.lower().strip() + + if _is_stdlib(lower, ecosystem_hint): + return [lower] + PACKAGE_ALIASES.get(lower, []), "stdlib" + + search_names: list[str] = [lower] + if lower in PACKAGE_ALIASES: + search_names.extend(PACKAGE_ALIASES[lower]) + for alias_key, alias_vals in PACKAGE_ALIASES.items(): + if lower in alias_vals and alias_key not in search_names: + search_names.append(alias_key) + + for suffix in _LANG_SUFFIXES: + if lower.endswith(suffix): + stripped = lower[: -len(suffix)] + if stripped and stripped not in search_names: + search_names.append(stripped) + + for prefix in _LANG_PREFIXES: + if lower.startswith(prefix): + stripped = lower[len(prefix):] + if stripped and stripped not in search_names: + search_names.append(stripped) + + # Strip org/owner prefixes (e.g. "huggingface/transformers" -> "transformers"). + if "/" in lower and not any(lower.startswith(pfx) for pfx in _GO_MODULE_PREFIXES): + bare = lower.rsplit("/", 1)[-1] + if bare and bare not in search_names: + search_names.append(bare) + + search_names = list(dict.fromkeys(search_names)) + + if ecosystem_hint: + hint_lower = ecosystem_hint.lower().strip() + if any(hint_lower.startswith(pfx) for pfx in _GO_MODULE_PREFIXES): + module_search_names = list(dict.fromkeys([hint_lower] + search_names)) + return module_search_names, "go" + + if any(lower.startswith(pfx) for pfx in _GO_MODULE_PREFIXES): + return search_names, "go" + + ecosystem = "pip" + return search_names, ecosystem + + +_CPYTHON_NAMES = frozenset({"cpython", "python", "python3"}) + + +def _is_stdlib(package_name: str, ecosystem_hint: str | None) -> bool: + if ecosystem_hint: + hint_lower = ecosystem_hint.lower() + if any(kw in hint_lower for kw in _STDLIB_KEYWORDS): + return True + if any(name in hint_lower for name in _CPYTHON_NAMES): + return True + for token in hint_lower.replace(",", " ").split(): + if token in _STDLIB_MODULES: + return True + if package_name in _CPYTHON_NAMES: + return True + if package_name in _STDLIB_MODULES: + return True + return False + + +# =================================================================== +# 3. Filesystem Discovery +# =================================================================== + + +def discover_filesystem_layout(fs_root: str) -> FilesystemLayout: + """Run a fixed set of discovery probes against the container filesystem.""" + layout = FilesystemLayout() + fs = Path(fs_root) + + conda_envs_dir = fs / "opt" / "conda" / "envs" + if conda_envs_dir.is_dir(): + layout.conda_envs = [ + d.name for d in sorted(conda_envs_dir.iterdir()) if d.is_dir() + ] + + for probe in [ + "opt/conda/envs/*/lib/python*/site-packages", + "usr/lib/python*/site-packages", + "usr/local/lib/python*/site-packages", + "opt/conda/lib/python*/site-packages", + ]: + for sp in fs.glob(probe): + if sp.is_dir(): + try: + layout.site_packages_paths.append(str(sp.relative_to(fs))) + except ValueError: + layout.site_packages_paths.append(str(sp)) + layout.site_packages_paths = list(dict.fromkeys(layout.site_packages_paths)) + + for probe in ("workspace", "app", "srv"): + probe_path = fs / probe + if probe_path.is_dir(): + layout.app_code_dirs.append(probe) + + for pattern in ("**/entrypoint*", "**/docker-entrypoint*"): + for ep in fs.glob(pattern): + if ep.is_file(): + try: + rel = str(ep.relative_to(fs)) + except ValueError: + rel = str(ep) + layout.entrypoint_scripts.append(rel) + layout.entrypoint_scripts = list(dict.fromkeys(layout.entrypoint_scripts)) + + bin_dir = fs / "usr" / "local" / "bin" + if bin_dir.is_dir(): + try: + layout.key_binaries = [ + e.name for e in sorted(bin_dir.iterdir()) if e.is_file() + ][:200] + except (OSError, PermissionError): + pass + + python_paths: list[str] = [] + for conda_env in layout.conda_envs: + p = fs / "opt" / "conda" / "envs" / conda_env + if p.is_dir(): + python_paths.append(os.path.join("opt", "conda", "envs", conda_env)) + conda_base = fs / "opt" / "conda" + if conda_base.is_dir(): + python_paths.append("opt/conda") + if python_paths: + layout.language_ecosystem_paths["python"] = python_paths + + node_dir = fs / "usr" / "local" / "lib" / "node_modules" + if node_dir.is_dir(): + layout.language_ecosystem_paths.setdefault("nodejs", []).append( + "usr/local/lib/node_modules" + ) + + go_dir = fs / "usr" / "local" / "go" + if go_dir.is_dir(): + layout.language_ecosystem_paths.setdefault("go", []).append("usr/local/go") + + jvm_dir = fs / "usr" / "lib" / "jvm" + if jvm_dir.is_dir(): + layout.language_ecosystem_paths.setdefault("java", []).append("usr/lib/jvm") + + return layout + + +# =================================================================== +# 4. Package Verification +# =================================================================== + + +def verify_package( + fs_root: str, + search_names: list[str], + ecosystem: str, + layout: FilesystemLayout, + default_env: str | None, +) -> PackageVerification: + """Verify the vulnerable package on the container filesystem.""" + if ecosystem == "stdlib": + result = _verify_stdlib(fs_root, search_names, layout, default_env) + elif ecosystem in ("pip", "conda"): + result = _verify_python_package(fs_root, search_names, layout, default_env) + elif ecosystem == "deb": + result = _verify_deb_package(fs_root, search_names) + elif ecosystem == "apk": + result = _verify_apk_package(fs_root, search_names) + elif ecosystem == "npm": + result = _verify_npm_package(fs_root, search_names) + elif ecosystem == "go": + result = _verify_go_package(fs_root, search_names) + else: + result = PackageVerification( + found=False, + notes=[f"Ecosystem '{ecosystem}' not handled by pre-triage; deferring to Phase 2."], + ) + result.searched_names = list(search_names) + return result + + +def _verify_stdlib( + fs_root: str, + search_names: list[str], + layout: FilesystemLayout, + default_env: str | None, +) -> PackageVerification: + """For stdlib modules, the installed version IS the Python interpreter version.""" + installations: list[PackageInstallation] = [] + + for conda_env in layout.conda_envs: + meta_dir = os.path.join( + fs_root, "opt", "conda", "envs", conda_env, "conda-meta" + ) + for fname in _raw_listdir(meta_dir): + if fname.startswith("python-") and fname.endswith(".json"): + version_m = re.match(r"python-([\d.]+[a-z\d]*)", fname) + if version_m: + installations.append(PackageInstallation( + version=version_m.group(1), + location=os.path.join(meta_dir, fname), + source="conda", + environment=conda_env, + )) + + base_meta = os.path.join(fs_root, "opt", "conda", "conda-meta") + for fname in _raw_listdir(base_meta): + if fname.startswith("python-") and fname.endswith(".json"): + version_m = re.match(r"python-([\d.]+[a-z\d]*)", fname) + if version_m: + installations.append(PackageInstallation( + version=version_m.group(1), + location=os.path.join(base_meta, fname), + source="conda", + environment="base", + )) + + active = None + for inst in installations: + if inst.environment == default_env: + active = inst + break + + return PackageVerification( + found=bool(installations), + installations=installations, + active_env_installation=active, + notes=["Stdlib module — version matches the Python interpreter."], + ) + + +def _verify_python_package( + fs_root: str, + search_names: list[str], + layout: FilesystemLayout, + default_env: str | None, +) -> PackageVerification: + """Check pip dist-info and conda-meta for a Python package.""" + installations: list[PackageInstallation] = [] + notes: list[str] = [] + fs = Path(fs_root) + + for sp_rel in layout.site_packages_paths: + sp = fs / sp_rel + if not sp.is_dir(): + continue + for name in search_names: + for dist_info in sp.glob(f"{name}*.dist-info"): + metadata_file = dist_info / "METADATA" + if not metadata_file.is_file(): + continue + content = _safe_read_text(metadata_file) + if not content: + continue + + pkg_name_m = re.search(r"^Name:\s*(.+)$", content, re.MULTILINE) + version_m = re.search(r"^Version:\s*(.+)$", content, re.MULTILINE) + if not version_m: + continue + + found_name = pkg_name_m.group(1).strip() if pkg_name_m else name + version = version_m.group(1).strip() + + if not _names_match(found_name, search_names): + continue + + env_name = _env_from_path(sp_rel, layout) + installations.append(PackageInstallation( + version=version, + location=str(dist_info.relative_to(fs)) if dist_info.is_relative_to(fs) else str(dist_info), + source="pip", + environment=env_name, + )) + + for name in search_names: + for conda_env in layout.conda_envs + ["base"]: + if conda_env == "base": + meta_dir = os.path.join(fs_root, "opt", "conda", "conda-meta") + else: + meta_dir = os.path.join( + fs_root, "opt", "conda", "envs", conda_env, "conda-meta" + ) + for fname in _raw_listdir(meta_dir): + if not fname.endswith(".json"): + continue + name_lower = name.lower().replace("-", "_") + fname_lower = fname.lower().replace("-", "_") + if fname_lower.startswith(name_lower + "-") or fname_lower.startswith( + name_lower + "_" + ): + version_m = re.match( + re.escape(name_lower) + r"[_-]([\d.]+[a-z\d._]*)", + fname_lower, + ) + if not version_m: + continue + already = any( + inst.environment == conda_env and inst.source == "conda" + and inst.version == version_m.group(1) + for inst in installations + ) + if already: + continue + installations.append(PackageInstallation( + version=version_m.group(1), + location=os.path.join(meta_dir, fname), + source="conda", + environment=conda_env, + )) + + seen: set[tuple[str, str, str | None]] = set() + deduped: list[PackageInstallation] = [] + for inst in installations: + key = (inst.version, inst.source, inst.environment) + if key not in seen: + seen.add(key) + deduped.append(inst) + installations = deduped + + active = None + for inst in installations: + if inst.environment == default_env: + active = inst + break + + return PackageVerification( + found=bool(installations), + installations=installations, + active_env_installation=active, + notes=notes, + ) + + +def _names_match(found: str, search_names: list[str]) -> bool: + """Case-insensitive, dash/underscore-normalized name comparison.""" + normalized_found = found.lower().replace("-", "_").replace(".", "_") + for sn in search_names: + normalized_sn = sn.lower().replace("-", "_").replace(".", "_") + if normalized_found == normalized_sn or normalized_found.startswith(normalized_sn): + return True + return False + + +def _env_from_path(rel_path: str, layout: FilesystemLayout) -> str | None: + """Infer the conda environment name from a relative filesystem path.""" + m = re.search(r"opt/conda/envs/([^/]+)/", rel_path) + if m: + return m.group(1) + if "opt/conda/" in rel_path and "/envs/" not in rel_path: + return "base" + return None + + +def _normalize_distro_version(raw: str) -> str: + """Extract the upstream version from a distro package version string.""" + v = raw.strip() + if ":" in v: + v = v.split(":", 1)[1] + if "-" in v: + v = v.rsplit("-", 1)[0] + return v + + +def _verify_deb_package( + fs_root: str, + search_names: list[str], +) -> PackageVerification: + """Check /var/lib/dpkg/status for a Debian/Ubuntu system package. + + Two-pass lookup: + 1. Exact match on ``Package:`` field. + 2. Fallback: match on ``Source:`` field to find binary sub-packages. + """ + status_path = os.path.join(fs_root, "var", "lib", "dpkg", "status") + installations: list[PackageInstallation] = [] + + status_content = _safe_read_text(status_path) + if not status_content: + return PackageVerification(found=False) + + # --- Pass 1: exact Package name match --- + for name in search_names: + pkg_pattern = re.compile( + rf"^Package:\s*{re.escape(name)}\s*$", + re.MULTILINE, + ) + for pkg_match in pkg_pattern.finditer(status_content): + block_start = pkg_match.start() + block_end = status_content.find("\n\n", block_start) + block = ( + status_content[block_start:block_end] + if block_end != -1 + else status_content[block_start: block_start + 500] + ) + vm = re.search(r"^Version:\s*(.+)$", block, re.MULTILINE) + if vm: + installations.append(PackageInstallation( + version=_normalize_distro_version(vm.group(1)), + location=status_path, + source="deb", + )) + if installations: + break + + if installations: + return PackageVerification( + found=True, + installations=installations, + active_env_installation=installations[0] if installations else None, + ) + + # --- Pass 2: Source field fallback --- + notes: list[str] = [] + for name in search_names: + for block in status_content.split("\n\n"): + src_m = re.search(r"^Source:\s*(\S+)", block, re.MULTILINE) + if not src_m or src_m.group(1).strip().lower() != name.lower(): + continue + pm = re.search(r"^Package:\s*(.+)$", block, re.MULTILINE) + vm = re.search(r"^Version:\s*(.+)$", block, re.MULTILINE) + if pm and vm: + binary_name = pm.group(1).strip() + raw_version = vm.group(1).strip() + installations.append(PackageInstallation( + version=_normalize_distro_version(raw_version), + location=status_path, + source="deb", + )) + notes.append( + f"Source match: binary package '{binary_name}' " + f"({raw_version}) from source '{name}'" + ) + if installations: + logger.info( + "Pre-triage: deb Source-field fallback found %d " + "sub-package(s): %s", + len(installations), + "; ".join(notes), + ) + break + + return PackageVerification( + found=bool(installations), + installations=installations, + active_env_installation=installations[0] if installations else None, + notes=notes, + ) + + +def _verify_apk_package( + fs_root: str, + search_names: list[str], +) -> PackageVerification: + """Check /lib/apk/db/installed for an Alpine system package.""" + db_path = os.path.join(fs_root, "lib", "apk", "db", "installed") + installations: list[PackageInstallation] = [] + + content = _safe_read_text(db_path) + if not content: + return PackageVerification(found=False) + + for name in search_names: + for block in content.split("\n\n"): + p_m = re.search(r"^P:(.+)$", block, re.MULTILINE) + v_m = re.search(r"^V:(.+)$", block, re.MULTILINE) + if p_m and v_m: + if p_m.group(1).strip().lower() == name.lower(): + installations.append(PackageInstallation( + version=_normalize_distro_version(v_m.group(1)), + location=db_path, + source="apk", + )) + + return PackageVerification( + found=bool(installations), + installations=installations, + active_env_installation=installations[0] if installations else None, + ) + + +def _verify_npm_package( + fs_root: str, + search_names: list[str], +) -> PackageVerification: + """Check node_modules for an npm package.""" + installations: list[PackageInstallation] = [] + fs = Path(fs_root) + + for name in search_names: + for pkg_json_path in fs.glob(f"**/node_modules/{name}/package.json"): + content = _safe_read_text(pkg_json_path) + if not content: + continue + try: + data = json.loads(content) + version = data.get("version", "") + if version: + try: + rel = str(pkg_json_path.relative_to(fs)) + except ValueError: + rel = str(pkg_json_path) + installations.append(PackageInstallation( + version=version, + location=rel, + source="npm", + )) + except json.JSONDecodeError: + continue + + return PackageVerification( + found=bool(installations), + installations=installations, + active_env_installation=installations[0] if installations else None, + ) + + +def _is_go_elf(fpath: str) -> bool: + """Fast check whether an ELF binary is a Go binary. + + Uses ``grep -c`` on the raw binary to search for the ``go1.`` + marker embedded in every Go binary. + """ + try: + result = subprocess.run( + ["grep", "-c", "go1\\.", fpath], + capture_output=True, text=True, timeout=5, + ) + return result.returncode == 0 and result.stdout.strip() != "0" + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def _verify_go_package( + fs_root: str, + search_names: list[str], +) -> PackageVerification: + """Scan Go binaries on the filesystem for an embedded Go module. + + Preferred method: ``go version -m`` (precise). + Fallback: ``strings`` grep for dep lines in Go build info. + """ + import shutil + + installations: list[PackageInstallation] = [] + notes: list[str] = [] + + _ELF_MAGIC = b"\x7fELF" + _GO_BINARY_CANDIDATES = [ + "go", + os.path.expanduser("~/go-install/bin/go"), + "/usr/local/go/bin/go", + "/snap/bin/go", + ] + + go_bin: str | None = None + for candidate in _GO_BINARY_CANDIDATES: + if shutil.which(candidate) or os.path.isfile(candidate): + go_bin = candidate + break + + if not go_bin: + notes.append( + "Go binary ('go') not found on host — using strings-based " + "fallback to scan Go binaries for module build info." + ) + + bin_dirs = [ + os.path.join(fs_root, d) + for d in ("usr/bin", "usr/local/bin", "usr/sbin", "bin", "sbin", + "opt", "usr/local/sbin", "usr/src") + if os.path.isdir(os.path.join(fs_root, d)) + ] + if not bin_dirs: + bin_dirs = [fs_root] + + go_binaries: list[Path] = [] + files_checked = 0 + MAX_FILES = 5000 + + for search_dir in bin_dirs: + for root_dir, _dirs, files in os.walk(search_dir): + for fname in files: + if files_checked >= MAX_FILES: + break + fpath = os.path.join(root_dir, fname) + files_checked += 1 + try: + with open(fpath, "rb") as f: + magic = f.read(4) + if magic != _ELF_MAGIC: + continue + except (PermissionError, OSError): + continue + if _is_go_elf(fpath): + go_binaries.append(Path(fpath)) + if files_checked >= MAX_FILES: + break + if files_checked >= MAX_FILES: + break + + if not go_binaries: + notes.append( + f"No Go binaries found in container filesystem " + f"(searched {files_checked} files). Go module verification " + f"will continue in Phase 2 with find_go_binaries." + ) + return PackageVerification( + found=False, + installations=[], + notes=notes, + ) + + notes.append(f"Found {len(go_binaries)} Go binary/binaries in container filesystem.") + + if go_bin: + installations = _scan_go_binaries_with_go( + go_bin, go_binaries, search_names, fs_root, notes, + ) + if not installations: + installations = _scan_go_binaries_with_strings( + go_binaries, search_names, fs_root, notes, + ) + + if not installations: + notes.append( + "Go module not found in any Go binary on the filesystem. " + "Use find_go_binaries in Phase 2 to do a broader search " + "if the pre-triage search scope was insufficient." + ) + + return PackageVerification( + found=bool(installations), + installations=installations, + active_env_installation=installations[0] if installations else None, + notes=notes, + ) + + +def _scan_go_binaries_with_go( + go_bin: str, + go_binaries: list[Path], + search_names: list[str], + fs_root: str, + notes: list[str], +) -> list[PackageInstallation]: + """Scan Go binaries using ``go version -m`` (preferred, precise).""" + installations: list[PackageInstallation] = [] + for binary in go_binaries[:20]: + try: + result = subprocess.run( + [go_bin, "version", "-m", str(binary)], + capture_output=True, text=True, timeout=15, + ) + if result.returncode != 0 or not result.stdout.strip(): + continue + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + + inst = _match_go_dep_lines( + result.stdout.splitlines(), search_names, binary, fs_root, + ) + if inst: + notes.append( + f"Found Go module {inst.environment or ''} v{inst.version} " + f"compiled into {inst.location} (via go version -m)" + ) + installations.append(inst) + break + return installations + + +def _scan_go_binaries_with_strings( + go_binaries: list[Path], + search_names: list[str], + fs_root: str, + notes: list[str], +) -> list[PackageInstallation]: + """Fallback: scan Go binaries using ``strings`` for build info dep lines.""" + installations: list[PackageInstallation] = [] + for binary in go_binaries[:30]: + try: + result = subprocess.run( + ["strings", "-n", "10", str(binary)], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + continue + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + + dep_lines = [ + line for line in result.stdout.splitlines() + if line.strip().startswith("dep\t") or line.strip().startswith("dep ") + ] + inst = _match_go_dep_lines( + dep_lines, search_names, binary, fs_root, + ) + if inst: + notes.append( + f"Found Go module dep line for v{inst.version} " + f"compiled into {inst.location} (via strings fallback)" + ) + installations.append(inst) + break + return installations + + +def _match_go_dep_lines( + lines: list[str], + search_names: list[str], + binary: Path, + fs_root: str, +) -> PackageInstallation | None: + """Parse ``dep`` lines from Go build info and match against search names.""" + for line in lines: + stripped = line.strip() + if not stripped.startswith("dep\t") and not stripped.startswith("dep "): + continue + parts = stripped.split("\t") + if len(parts) < 3: + parts = stripped.split() + if len(parts) < 3: + continue + module_name = parts[1] + module_version = parts[2].lstrip("v") + for name in search_names: + if name.lower() in module_name.lower(): + try: + rel = "/" + str(binary.relative_to(fs_root)) + except ValueError: + rel = str(binary) + return PackageInstallation( + version=module_version, + location=rel, + source="go-binary", + environment=module_name, + ) + return None + + +# =================================================================== +# 4b. Reverse Dependency Enumeration (pip only) +# =================================================================== + + +_REQUIRES_DIST_NAME_RE = re.compile( + r"^Requires-Dist:\s*([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)", + re.MULTILINE, +) + + +def _compute_reverse_dependencies( + fs_root: str, + pkg: PackageVerification, + search_names: list[str], +) -> list[str]: + """Find packages in the same site-packages that depend on the vulnerable package. + + Scans every ``*.dist-info/METADATA`` in the site-packages directory of the + primary installation and checks ``Requires-Dist:`` lines for a name match. + Returns ``["name==version", ...]`` sorted. + """ + primary = pkg.active_env_installation + if not primary or primary.source != "pip": + return [] + + site_packages_rel = os.path.dirname(os.path.dirname(primary.location)) + site_packages_abs = os.path.join(fs_root, site_packages_rel) + + if not os.path.isdir(site_packages_abs): + return [] + + rdeps: list[str] = [] + + try: + entries = os.listdir(site_packages_abs) + except (OSError, PermissionError): + return [] + + for entry in entries: + if not entry.endswith(".dist-info"): + continue + metadata_path = os.path.join(site_packages_abs, entry, "METADATA") + if not os.path.isfile(metadata_path): + continue + + content = _safe_read_text(metadata_path) + if not content: + continue + + dep_names = _REQUIRES_DIST_NAME_RE.findall(content) + if not dep_names: + continue + + matched = False + for dep_name_tuple in dep_names: + dep_name = dep_name_tuple[0] if isinstance(dep_name_tuple, tuple) else dep_name_tuple + if _names_match(dep_name, search_names): + matched = True + break + + if not matched: + continue + + name_m = re.search(r"^Name:\s*(.+)$", content, re.MULTILINE) + version_m = re.search(r"^Version:\s*(.+)$", content, re.MULTILINE) + if name_m and version_m: + rdeps.append( + f"{name_m.group(1).strip()}=={version_m.group(1).strip()}" + ) + + rdeps.sort() + logger.info( + "Pre-triage: found %d reverse dependencies for %s", + len(rdeps), + search_names[0] if search_names else "unknown", + ) + return rdeps + + +# =================================================================== +# 5. Version Comparison +# =================================================================== + + +def is_version_vulnerable( + installed_version: str, + affected_ranges: list[AffectedRange], + fixed_in: str, +) -> bool | None: + """Determine if the installed version falls within a vulnerable range. + + Returns True (vulnerable), False (patched), or None (uncertain). + """ + try: + installed = Version(installed_version) + except InvalidVersion: + return None + + if affected_ranges: + best_match = _match_branch(installed, affected_ranges) + if best_match is not None: + return _check_range(installed, best_match) + + if fixed_in and "see" not in fixed_in.lower(): + fixed_versions = re.split(r"[,;]", fixed_in) + for fv in fixed_versions: + fv = fv.strip() + if not fv: + continue + try: + fixed = Version(fv) + except InvalidVersion: + continue + if installed >= fixed: + return False + + if affected_ranges: + no_fix_ranges = [ + r for r in affected_ranges + if "no" in r.first_fixed.lower() + ] + for r in no_fix_ranges: + if _matches_lower_bound(installed, r): + return True + + return None + + +def _match_branch( + installed: Version, + ranges: list[AffectedRange], +) -> AffectedRange | None: + """Find the affected-range row whose branch matches the installed version.""" + best: AffectedRange | None = None + best_specificity = 0 + + for r in ranges: + branch = r.branch.strip().strip("*").strip() + if not branch: + continue + + if branch.startswith("<=") or "all" in branch.lower(): + continue + + branch_clean = re.sub(r"\s*\(.*?\)", "", branch).strip() + try: + branch_parts = [int(x) for x in branch_clean.split(".") if x.strip().isdigit()] + except ValueError: + continue + + if len(branch_parts) < 2: + continue + + installed_parts = list(installed.release) + if installed_parts[: len(branch_parts)] == branch_parts: + if len(branch_parts) > best_specificity: + best = r + best_specificity = len(branch_parts) + + if best is not None: + return best + + for r in ranges: + branch = r.branch.strip().strip("*").strip() + if branch.startswith("<=") or "all" in branch.lower(): + return r + + return None + + +def _check_range(installed: Version, r: AffectedRange) -> bool | None: + """Check if *installed* is within the vulnerable range of *r*.""" + if "no" in r.first_fixed.lower(): + return True + + try: + fixed = Version(r.first_fixed) + except InvalidVersion: + return None + + if installed >= fixed: + return False + + if r.lower: + lower_m = re.search(r"[\d][\d.a-z]*", r.lower) + if lower_m: + try: + lower_v = Version(lower_m.group()) + if installed < lower_v: + return None + except InvalidVersion: + pass + + return True + + +def _matches_lower_bound(installed: Version, r: AffectedRange) -> bool: + if not r.lower: + return True + lower_m = re.search(r"[\d][\d.a-z]*", r.lower) + if lower_m: + try: + return installed >= Version(lower_m.group()) + except InvalidVersion: + pass + return True + + +# =================================================================== +# 6. Distro Backport Check +# =================================================================== + + +def check_backport( + package_name: str, + cve_id: str, + base_os: str, + fs_root: str, +) -> tuple[str | None, str | None]: + """Check for distro backported patches. Returns (status, evidence).""" + os_lower = base_os.lower() + if "debian" not in os_lower and "ubuntu" not in os_lower: + return None, None + + fs = Path(fs_root) + evidence_parts: list[str] = [] + status = "inconclusive" + + changelog_dir = fs / "usr" / "share" / "doc" + if changelog_dir.exists(): + try: + changelogs = list(changelog_dir.glob(f"{package_name}*/changelog.Debian.gz")) + if not changelogs: + changelogs = list(changelog_dir.glob(f"*{package_name}*/changelog.Debian.gz")) + for changelog in changelogs[:3]: + try: + result = subprocess.run( + ["zgrep", "-i", cve_id, str(changelog)], + capture_output=True, text=True, timeout=30, + ) + if result.returncode == 0 and result.stdout.strip(): + evidence_parts.append( + f"Found {cve_id} in {changelog.relative_to(fs)}:\n" + f"{result.stdout.strip()[:500]}" + ) + status = "patched" + except (FileNotFoundError, subprocess.TimeoutExpired): + evidence_parts.append("zgrep not available or timed out.") + except Exception as exc: + evidence_parts.append(f"Error searching changelogs: {exc}") + + dpkg_status = fs / "var" / "lib" / "dpkg" / "status" + if dpkg_status.exists(): + content = _safe_read_text(dpkg_status) + if content: + pkg_pattern = re.compile(rf"^Package:\s*{re.escape(package_name)}\s*$", re.MULTILINE) + match = pkg_pattern.search(content) + if match: + block_start = match.start() + block_end = content.find("\n\n", block_start) + block = content[block_start:block_end] if block_end != -1 else content[block_start:block_start + 500] + version_match = re.search(r"^Version:\s*(.+)$", block, re.MULTILINE) + if version_match: + distro_version = version_match.group(1).strip() + if "deb" in distro_version or "ubuntu" in distro_version: + evidence_parts.append(f"Distro version: {distro_version}") + + evidence = "; ".join(evidence_parts) if evidence_parts else None + return status if evidence_parts else None, evidence + + +# =================================================================== +# 7. LLM Package Name Resolution (fallback) +# =================================================================== + +_PKG_RESOLVE_PROMPT = ( + "A CVE advisory refers to a package named \"{pkg_name}\".\n" + "What are the likely pip/conda package names that this corresponds to " + "when installed on a Linux system? Consider common naming conventions " + "where CVE advisories use names like 'protobuf-python' but the actual " + "pip package is 'protobuf', or 'python-cryptography' but the pip " + "package is 'cryptography'.\n\n" + "Return ONLY a JSON array of lowercase strings, e.g. " + '[\"protobuf\", \"google-protobuf\"]. ' + "Do NOT include the original name. Do NOT include any explanation." +) + + +def _llm_resolve_package_name( + package_name: str, + *, + model: str, + api_key: str, + base_url: str, +) -> list[str]: + """Ask the LLM for likely pip/conda names given a CVE package name. + + Uses a single synchronous chat completion call (no tools). + Returns a (possibly empty) list of alternative package names. + """ + try: + from openai import OpenAI + client = OpenAI(api_key=api_key, base_url=base_url) + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "user", + "content": _PKG_RESOLVE_PROMPT.format(pkg_name=package_name)}, + ], + temperature=0.0, + max_tokens=256, + ) + text = (response.choices[0].message.content or "").strip() + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + candidates = json.loads(text) + if isinstance(candidates, list): + return [ + c.lower().strip() + for c in candidates + if isinstance(c, str) and c.strip() + ] + except Exception as exc: + logger.warning("LLM package-name resolution failed: %s", exc) + return [] + + +# =================================================================== +# 8. Main Orchestrator +# =================================================================== + + +def _verify_additional_packages( + vuln_report: ParsedVulnReport, + fs_root: str, + layout: FilesystemLayout, + default_env: str | None, + primary_search_names: list[str], +) -> list[AdditionalPackageResult]: + """Verify each additional affected package listed in the CVE report. + + Skips entries whose package name overlaps with the primary search names. + """ + if not vuln_report.affected_packages: + return [] + + primary_lower = {n.lower() for n in primary_search_names} + results: list[AdditionalPackageResult] = [] + + for entry in vuln_report.affected_packages: + pkg_lower = entry.package.lower().strip() + if pkg_lower in primary_lower: + continue + bare = pkg_lower.rsplit("/", 1)[-1] if "/" in pkg_lower else pkg_lower + if bare in primary_lower: + continue + + extra_search, extra_eco = resolve_package_names(pkg_lower, entry.ecosystem) + logger.info( + "Pre-triage: verifying additional package '%s' " + "(search=%s, eco=%s)", + entry.package, + extra_search, + extra_eco, + ) + extra_pkg = verify_package( + fs_root, extra_search, extra_eco, layout, default_env, + ) + if not extra_pkg.found and extra_eco not in ("deb", "apk"): + for fb_eco, fb_fn in [("deb", _verify_deb_package), + ("apk", _verify_apk_package)]: + fb_pkg = fb_fn(fs_root, extra_search) + if fb_pkg.found: + extra_pkg = fb_pkg + extra_pkg.searched_names = list(extra_search) + break + + logger.info( + "Pre-triage: additional package '%s' found=%s", + entry.package, + extra_pkg.found, + ) + results.append(AdditionalPackageResult( + entry=entry, + verification=extra_pkg, + )) + + return results + + +def run_pre_triage( + inp: ExploitabilityAnalyzerInput, + *, + llm_model: str | None = None, + llm_api_key: str | None = None, + llm_base_url: str | None = None, +) -> PreTriageResult: + """Run the full deterministic pre-triage pipeline. + + Parses reports, discovers the layout, verifies the package, compares + versions, and decides whether to early-exit. + + When *llm_model* / *llm_api_key* / *llm_base_url* are provided, a single + LLM call is used as a last-resort fallback for package-name resolution if + the deterministic name variants all fail to match. + """ + vuln_content = Path(inp.vulnerability_report_path).read_text(encoding="utf-8") + container_content = Path(inp.container_report_path).read_text(encoding="utf-8") + + vuln_report = parse_vulnerability_report(vuln_content) + container_report = parse_container_report(container_content) + + logger.info( + "Pre-triage: CVE=%s package=%s ecosystem_hint=%s container_type=%s", + vuln_report.cve_id, + vuln_report.package_name, + vuln_report.ecosystem_hint, + container_report.container_type, + ) + if vuln_report.affected_packages: + logger.info( + "Pre-triage: %d affected packages listed: %s", + len(vuln_report.affected_packages), + [e.package for e in vuln_report.affected_packages], + ) + + layout = discover_filesystem_layout(inp.filesystem_path) + + search_names, ecosystem = resolve_package_names( + vuln_report.package_name, + vuln_report.ecosystem_hint, + ) + + logger.info( + "Pre-triage: search_names=%s ecosystem=%s", + search_names, + ecosystem, + ) + + pkg = verify_package( + inp.filesystem_path, + search_names, + ecosystem, + layout, + container_report.default_environment, + ) + + # Fallback: try system package managers if primary ecosystem missed it + if not pkg.found and ecosystem not in ("deb", "apk"): + for fallback_eco, fallback_fn in [ + ("deb", _verify_deb_package), + ("apk", _verify_apk_package), + ]: + fallback_pkg = fallback_fn(inp.filesystem_path, search_names) + if fallback_pkg.found: + pkg = fallback_pkg + pkg.searched_names = list(search_names) + ecosystem = fallback_eco + logger.info( + "Pre-triage: system package fallback found via %s", + fallback_eco, + ) + break + + logger.info( + "Pre-triage: found=%s installations=%d", + pkg.found, + len(pkg.installations), + ) + + # --- Decision: package not found --- + llm_calls = 0 + if not pkg.found and llm_model and llm_api_key and llm_base_url: + logger.info( + "Pre-triage: package '%s' not found — attempting LLM name resolution", + vuln_report.package_raw, + ) + alt_names = _llm_resolve_package_name( + vuln_report.package_raw, + model=llm_model, + api_key=llm_api_key, + base_url=llm_base_url, + ) + llm_calls = 1 + if alt_names: + logger.info("LLM suggested alternatives: %s", alt_names) + already_tried = {n.lower() for n in search_names} + new_names = [n for n in alt_names if n.lower() not in already_tried] + if new_names: + combined = list(dict.fromkeys(search_names + new_names)) + pkg = verify_package( + inp.filesystem_path, + combined, + ecosystem, + layout, + container_report.default_environment, + ) + search_names = combined + logger.info( + "LLM name-resolution retry: found=%s installations=%d", + pkg.found, + len(pkg.installations), + ) + + # --- Verify additional affected packages --- + additional = _verify_additional_packages( + vuln_report, + inp.filesystem_path, + layout, + container_report.default_environment, + search_names, + ) + + if not pkg.found: + additional_found = [a for a in additional if a.verification.found] + extra_note = "" + if additional_found: + extra_note = ( + " However, additional affected package(s) WERE found: " + + ", ".join( + f"{a.entry.package} (v{a.verification.installations[0].version})" + for a in additional_found + if a.verification.installations + ) + + ". Phase 2 agent MUST analyze these." + ) + + result = PreTriageResult( + vuln_report=vuln_report, + container_report=container_report, + filesystem_layout=layout, + package_verification=pkg, + early_exit=False, + package_not_found_note=( + f"Package '{vuln_report.package_raw}' (searched as: " + f"{', '.join(search_names)}) not found by deterministic search. " + f"Phase 2 agent should independently verify." + + extra_note + ), + additional_package_verifications=additional, + ) + result.llm_call_count = llm_calls + return result + + result = _decide_after_verification( + vuln_report, container_report, layout, pkg, + ecosystem, search_names, additional, inp.filesystem_path, + ) + result.llm_call_count = llm_calls + + if ( + not result.early_exit + and ecosystem in ("pip", "conda") + and pkg.found + and pkg.active_env_installation + and pkg.active_env_installation.source == "pip" + ): + result.reverse_dependencies = _compute_reverse_dependencies( + inp.filesystem_path, pkg, search_names, + ) + + return result + + +def _any_additional_vulnerable( + additional: list[AdditionalPackageResult], +) -> bool: + """Return True if any additional package was found on the filesystem.""" + return any(a.verification.found for a in additional) + + +def _decide_after_verification( + vuln_report: ParsedVulnReport, + container_report: ParsedContainerReport, + layout: FilesystemLayout, + pkg: PackageVerification, + ecosystem: str, + search_names: list[str], + additional: list[AdditionalPackageResult] | None = None, + fs_root: str = "", +) -> PreTriageResult: + """Version / backport / env-isolation checks after a successful package find.""" + if additional is None: + additional = [] + + any_vulnerable = False + all_patched = True + + for inst in pkg.installations: + result = is_version_vulnerable( + inst.version, + vuln_report.affected_ranges, + vuln_report.fixed_in, + ) + if result is True: + any_vulnerable = True + all_patched = False + elif result is None: + all_patched = False + + pkg.is_vulnerable = any_vulnerable if all_patched or any_vulnerable else None + + if ecosystem == "deb" and any_vulnerable and fs_root: + bp_status, bp_evidence = check_backport( + search_names[0], + vuln_report.cve_id, + container_report.base_os, + fs_root, + ) + if bp_status: + pkg.backport_status = bp_status + pkg.backport_evidence = bp_evidence + if bp_status == "patched": + any_vulnerable = False + all_patched = True + pkg.is_vulnerable = False + + has_extra_vuln = _any_additional_vulnerable(additional) + + if all_patched and not any_vulnerable: + if has_extra_vuln: + logger.info( + "Pre-triage: primary package patched but additional " + "affected package(s) found — skipping early exit", + ) + else: + versions_summary = ", ".join( + f"{inst.version} ({inst.source}, env={inst.environment})" + for inst in pkg.installations + ) + reason = ( + f"All installed versions of '{vuln_report.package_raw}' are patched: " + f"{versions_summary}." + ) + if pkg.backport_status == "patched": + reason += f" Distro backport confirmed: {pkg.backport_evidence}" + return PreTriageResult( + vuln_report=vuln_report, + container_report=container_report, + filesystem_layout=layout, + package_verification=pkg, + early_exit=True, + early_exit_verdict="NOT APPLICABLE", + early_exit_reason=reason, + additional_package_verifications=additional, + ) + + if any_vulnerable and pkg.active_env_installation and not has_extra_vuln: + active_vuln = is_version_vulnerable( + pkg.active_env_installation.version, + vuln_report.affected_ranges, + vuln_report.fixed_in, + ) + if active_vuln is False: + vuln_in = [ + inst for inst in pkg.installations + if is_version_vulnerable( + inst.version, + vuln_report.affected_ranges, + vuln_report.fixed_in, + ) is True + ] + vuln_envs = ", ".join( + f"{inst.environment or 'unknown'} (v{inst.version})" + for inst in vuln_in + ) + return PreTriageResult( + vuln_report=vuln_report, + container_report=container_report, + filesystem_layout=layout, + package_verification=pkg, + early_exit=True, + early_exit_verdict="ENVIRONMENT ISOLATED", + early_exit_reason=( + f"Active environment '{container_report.default_environment}' " + f"has patched version {pkg.active_env_installation.version}. " + f"Vulnerable version(s) found only in non-default " + f"environment(s): {vuln_envs}." + ), + additional_package_verifications=additional, + ) + + logger.info( + "Pre-triage: CONTINUE to Phase 2 (any_vulnerable=%s, uncertain=%s, " + "additional_found=%s)", + any_vulnerable, + not all_patched and not any_vulnerable, + has_extra_vuln, + ) + + return PreTriageResult( + vuln_report=vuln_report, + container_report=container_report, + filesystem_layout=layout, + package_verification=pkg, + early_exit=False, + additional_package_verifications=additional, + ) diff --git a/src/vuln_analysis/utils/prompting.py b/src/vuln_analysis/utils/prompting.py deleted file mode 100644 index b746bd3c7..000000000 --- a/src/vuln_analysis/utils/prompting.py +++ /dev/null @@ -1,308 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Some examples to be optionally passed along in the agent prompt. -from abc import ABC -from abc import abstractmethod -from textwrap import dedent - -# pylint: disable=line-too-long - -SUMMARY_PROMPT = """Summarize the exploitability investigation results of a Common Vulnerabilities and Exposures (CVE) \ -based on the provided Checklist and Findings. Write a concise paragraph focusing only on checklist items with \ -definitive answers. Begin your response by clearly stating whether the CVE is exploitable. Disregard any ambiguous \ -checklist items. -Checklist and Findings: -{response}""" - -# Define a system prompt that sets the context for the language model's task. This prompt positions the assistant -# as a powerful entity capable of investigating CVE impacts on container images. -AGENT_SYS_PROMPT = ( - "You are a very powerful assistant who helps investigate the impact of reported Common Vulnerabilities and " - "Exposures (CVE) on container images. Information about the container image under investigation is " - "stored in vector databases available to you via tools.") - -# Based on original prompt: https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/agents/mrkl/prompt.py -AGENT_PROMPT_TEMPLATE = """If the input is not a question, formulate it into a question first. Include intermediate thought in the final answer. You have access to the following tools: - -{tools} - -Use the following format (start each response with one of the following prefixes: [Question, Thought, Action, Action Input, Final Answer]): - -Question: the input question you must answer -Thought: you should always think about what to do -Action: the action to take, should be one of [{tool_names}] -Action Input: the input to the action -Observation: the result of the action -... (this Thought/Action/Action Input/Observation can repeat N times) -Thought: I now know the final answer -Final Answer: the final answer to the original input question - -Begin! - -Question: {input} -Thought:{agent_scratchpad}""" - -AGENT_EXAMPLES_FOR_PROMPT = """Example 1: - - Question: 1. Identify the version of Python: Check which version(s) of Python are installed in the container image. The vulnerability affects versions up to and including 3.11.3. - Thought: I should check if Python is installed. I will check the Software Bill of Materials. - Action: SBOM Package Checker - Action Input: Python - Observation: 3.10.0 - Thought: Python 3.10.0 is installed, I need to check if the installed version is vulnerable. To do this I'll compare the installed version to the vulnerable version. - Action: container software version comparator - Action Input: 3.10.0, 3.11.3 - Observation: True - Thought: The installed software is vulnerable. I now know the answer. - Final Answer: Python version 3.10.0 is installed and is vulnerable to the CVE. - - Example 2: - - Question: Assess the threat that CVE-20xx-xxxxx poses to the container. - Thought: I should search for more information on CVE-20xx-xxxxx. - Action: Internet Search - Action Input: What is CVE-20xx-xxxxx? - Observation: CVE-20xx-xxxxx causes memory leaks and possible denial of service attack vectors when using urllib.parse - Thought: I should check the code base of the container for instances of urllib.parse - Action: Container Image QA System - Action Input: Is urllib.parse present in the code? - Observation: - Question: Is urllib.parse present in the code? - Helpful answer: No, that function is not called in the code. - Thought: Since the function is not called in the code, the container is not vulnerable. I know the final answer. - Final Answer: The function urllib.parse is not present in the code, so the container is not vulnerable. - - Example 3: - - Question: Check if the container is using Java Runtime Environment (JRE). If it is not using JRE, then it is not vulnerable to CVE-xxxx-xxxxx. - Thought: I should check if JRE is installed. I will check the Software Bill of Materials. - Action: SBOM Package Checker - Action Input: JRE - Observation: False - Thought: JRE is not present in the container. I now know the answer. - Final Answer: JRE is not installed in the container. Therefore, it is not vulnerable to CVE-20xx-xxxxx. - - Example 4: - - Question: Check if the container is using Apache. If it is not using Apache, then it is not vulnerable to CVE-xxxx-xxxxx. - Thought: I should check if Apache is installed. I will check the Software Bill of Materials. - Action: SBOM Package Checker - Action Input: Apache - Observation: 1.0.1 - Thought: Apache is present in the container. I now know the answer. - Final Answer: Apache is installed in the container. Therefore, it is potentially vulnerable to CVE-20xx-xxxxx. - - """ - - -def get_agent_prompt(sys_prompt: str | None = None, prompt_examples: bool = False) -> str: - """ - Get the agent prompt based on the system prompt and whether to include examples. - """ - sys_prompt = sys_prompt or AGENT_SYS_PROMPT - - if prompt_examples: - prompt_template = AGENT_PROMPT_TEMPLATE.replace("Begin!\n\n", AGENT_EXAMPLES_FOR_PROMPT + "Begin!\n\n") - else: - prompt_template = AGENT_PROMPT_TEMPLATE - - return f'{sys_prompt} {prompt_template}' - - -class PromptBuilder(ABC): - - @abstractmethod - def build_prompt(self) -> str: - pass - - -class IfPromptBuilder(PromptBuilder): - - def __init__(self, prop_name: str, description: str) -> None: - - self.prop_name = prop_name - self.description = description - - def build_prompt(self) -> str: - - f_string = dedent(f""" - {{% if {self.prop_name} %}} - - {self.description}{{{{{self.prop_name} | string() | truncate(1024)}}}} - {{% endif %}} - """).strip() - - return f_string - - -class IfElsePromptBuilder(PromptBuilder): - - def __init__(self, first_prop_name: str, second_prop_name: str, description: str) -> None: - - self.first_prop_name = first_prop_name - self.second_prop_name = second_prop_name - self.description = description - - def build_prompt(self) -> str: - - f_string = dedent(f""" - {{% if {self.first_prop_name} %}} - - {self.description}{{{{{self.first_prop_name} | string() | truncate(1024)}}}} - {{% elif {self.second_prop_name} %}} - - {self.description}{{{{{self.second_prop_name} | string() | truncate(1024)}}}} - {{% endif %}} - """).strip() - - return f_string - - -# Add additional intel fields for the checklist prompt here (field name, description) -additional_intel_fields: list[PromptBuilder] = [ - # Keep these at the top - IfPromptBuilder('cve_id', 'CVE ID: '), - IfElsePromptBuilder('nvd_cve_description', "ghsa_description", 'CVE Description: '), - - # Sort these alphabetically - IfElsePromptBuilder('nvd_cvss_vector', "ghsa_cvss_vector_string", 'CVSS Vector: '), - IfElsePromptBuilder('nvd_cwe_name', "ghsa_cwes", 'CWE Name: '), - IfPromptBuilder('ghsa_vulnerabilities', 'GHSA Details: '), - IfPromptBuilder('nvd_configurations', 'Known Affected Software: '), - IfPromptBuilder('nvd_cwe_description', 'CWE Description: '), - IfPromptBuilder('nvd_cwe_extended_description', ''), - IfPromptBuilder('nvd_vendor_names', 'Notable Vulnerable Software Vendors: '), - IfPromptBuilder('rhsa_bugzilla_description', 'RHSA Description: '), - IfPromptBuilder('rhsa_details', 'RHSA Details: '), - IfPromptBuilder('rhsa_package_state', 'RHSA Affected Packages: '), - IfPromptBuilder('rhsa_statement', 'RHSA Statement: '), - # IfPromptBuilder('ubuntu_notices', 'Ubuntu Priority Reason: '), # Disabling for now since its very long - IfPromptBuilder('ubuntu_ubuntu_description', 'Ubuntu Security Note: '), - IfPromptBuilder('vulnerable_dependencies', 'Identified Vulnerable Dependencies: '), -] - -additional_intel_prompting = '\n'.join([pb.build_prompt() for pb in additional_intel_fields]) - -ex_questions = [ - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2023-24329\n- CVE description: An issue in the urllib.parse component of Python before 3.11.4 allows attackers to bypass blocklisting methods by supplying a URL that starts with blank characters.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N\n- CWE Name: CWE-20: Improper Input Validation (4.14)\n- CWE Description: The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.\nInput validation is a frequently-used technique for checking potentially dangerous inputs in order to ensure that the inputs are safe for processing within the code, or when communicating with other components. When software does not validate input properly, an attacker is able to craft the input in a form that is not expected by the rest of the application. This will lead to parts of the system receiving unintended input, which may result in altered control flow, arbitrary control of a resource, or arbitrary code execution. Input validation is not the only technique for processing input, however. Other techniques attempt to transform potentially-dangerous input into something safe, such as filtering (CWE-790) - which attempts to remove dangerous inputs - or encoding/escaping (CWE-116), which attempts to ensure that the input is not misinterpreted when it is included in output to another component. Other techniques exist as well (see CWE-138 for more examples.) Input validation can be applied to: raw data - strings, numbers, parameters, file contents, etc. metadata - information about the raw data, such as headers or size Data can be simple or structured. Structured data can be composed of many nested layers, composed of combinations of metadata and raw data, with other simple or structured data. Many properties of raw data or metadata may need to be validated upon entry into the code, such as: specified quantities such as size, length, frequency, price, rate, number of operations, time, etc. implied or derived quantities, such as the actual size of a file instead of a specified size indexes, offsets, or positions into more complex data structures symbolic keys or other elements into hash tables, associative arrays, etc. well-formedness, i.e. syntactic correctness - compliance with expected syntax lexical token correctness - compliance with rules for what is treated as a token specified or derived type - the actual type of the input (or what the input appears to be) consistency - between individual data elements, between raw data and metadata, between references, etc. conformance to domain-specific rules, e.g. business logic equivalence - ensuring that equivalent inputs are treated the same authenticity, ownership, or other attestations about the input, e.g. a cryptographic signature to prove the source of the data Implied or derived properties of data must often be calculated or inferred by the code itself. Errors in deriving properties may be considered a contributing factor to improper input validation. Note that "input validation" has very different meanings to different people, or within different classification schemes. Caution must be used when referencing this CWE entry or mapping to it. For example, some weaknesses might involve inadvertently giving control to an attacker over an input when they should not be able to provide an input at all, but sometimes this is referred to as input validation. Finally, it is important to emphasize that the distinctions between input validation and output escaping are often blurred, and developers must be careful to understand the difference, including how input validation is not always sufficient to prevent vulnerabilities, especially when less stringent data types must be supported, such as free-form text. Consider a SQL injection scenario in which a person\'s last name is inserted into a query. The name "O\'Reilly" would likely pass the validation step since it is a common last name in the English language. However, this valid name cannot be directly inserted into the database because it contains the "\'" apostrophe character, which would need to be escaped or otherwise transformed. In this case, removing the apostrophe might reduce the risk of SQL injection, but it would produce incorrect behavior because the wrong name would be recorded.\n- Notable Vulnerable Software Vendors: [\'Fedoraproject\', \'Netapp\', \'Python\']\n\nExample {idx}: Checklist:\n[\n\t"Review Python Usage: Is the Python installation actively used by applications within the container? Check for scripts or applications that rely on Python, particularly those that might parse URLs using `urllib.parse`.",\n\t"Inspect URL Parsing Logic: Does the codebase contain any usage of `urllib.parse` or similar URL parsing mechanisms? Focus on how URLs are handled and whether there is a reliance on blocklisting methods to filter out potentially harmful URLs. This is crucial since the CVE describes a bypass of blocklisting methods via URLs starting with blank characters.",\n\t"Evaluate Input Validation Practices: Assess the robustness of input validation practices within the application. Since the CVE involves improper input validation, are inputs, especially URLs, properly sanitized and validated against unexpected or malicious data?"\n]', - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2023-36632\n- CVE description: The legacy email.utils.parseaddr function in Python through 3.11.4 allows attackers to trigger "RecursionError: maximum recursion depth exceeded while calling a Python object" via a crafted argument. This argument is plausibly an untrusted value from an application\'s input data that was supposed to contain a name and an e-mail address. NOTE: email.utils.parseaddr is categorized as a Legacy API in the documentation of the Python email package. Applications should instead use the email.parser.BytesParser or email.parser.Parser class. NOTE: the vendor\'s perspective is that this is neither a vulnerability nor a bug. The email package is intended to have size limits and to throw an exception when limits are exceeded; they were exceeded by the example demonstration code.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n- CWE Name: CWE-674: Uncontrolled Recursion (4.14)\n- CWE Description: The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack.\n- Notable Vulnerable Software Vendors: [\'Python\']\n\nExample {idx}: Checklist:\n[\n\t"Identify Usage of `email.utils.parseaddr`: Review the application code within the container image to check if the `email.utils.parseaddr` function is being used. This function is the specific target of the vulnerability. Does the codebase contain the string \'email.utils.parseaddr\'?",\n\t"Assess Input Data Handling: Does the application using `email.utils.parseaddr` process potentially untrusted input data that could include crafted arguments designed to exploit this vulnerability? Evaluate how the application handles exceptions like `RecursionError`, as the exploit triggers this specific error.",\n\t"Review Alternative Implementations: Has the application already migrated to recommended alternatives such as `email.parser.BytesParser` or `email.parser.Parser` in the code base? If not, recommend transitioning away from the legacy `email.utils.parseaddr` to these safer alternatives."\n]', - "Example {idx}: CVE Details:\n- CVE ID: CVE-2023-50447\n- CVE description: Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter).\n- CVSS Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\n- CWE Name: CWE-94: Improper Control of Generation of Code ('Code Injection') (4.14)\n- CWE Description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.\nWhen a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution. Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.\n- Notable Vulnerable Software Vendors: ['Debian', 'Python']\n\nExample {idx}: Checklist:\n[\n\t\"Evaluate Use of PIL.ImageMath.eval: Does any application within the container use the PIL.ImageMath.eval function with user-supplied input in the environment parameter? This is the method and parameter where the vulnerability exists.\",\n\t\"Assess Data Input Sources: Do the applications using Pillow receive input directly from untrusted sources (e.g., user uploads, external APIs)? This will help in understanding the risk exposure and potential for exploitation.\",\n\t\"Security Controls and Sanitization: Review the application's input validation and sanitization measures. Since the vulnerability allows for arbitrary code execution via code injection, is the input properly sanitized before being processed?\"\n]", - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2023-5363\n- CVE description: Issue summary: A bug has been identified in the processing of key and initialisation vector (IV) lengths. This can lead to potential truncation or overruns during the initialisation of some symmetric ciphers. Impact summary: A truncation in the IV can result in non-uniqueness, which could result in loss of confidentiality for some cipher modes. When calling EVP_EncryptInit_ex2(), EVP_DecryptInit_ex2() or EVP_CipherInit_ex2() the provided OSSL_PARAM array is processed after the key and IV have been established. Any alterations to the key length, via the "keylen" parameter or the IV length, via the "ivlen" parameter, within the OSSL_PARAM array will not take effect as intended, potentially causing truncation or overreading of these values. The following ciphers and cipher modes are impacted: RC2, RC4, RC5, CCM, GCM and OCB. For the CCM, GCM and OCB cipher modes, truncation of the IV can result in loss of confidentiality. For example, when following NIST\'s SP 800-38D section 8.2.1 guidance for constructing a deterministic IV for AES in GCM mode, truncation of the counter portion could lead to IV reuse. Both truncations and overruns of the key and overruns of the IV will produce incorrect results and could, in some cases, trigger a memory exception. However, these issues are not currently assessed as security critical.\nChanging the key and/or IV lengths is not considered to be a common operation and the vulnerable API was recently introduced. Furthermore it is likely that application developers will have spotted this problem during testing since decryption would fail unless both peers in the communication were similarly vulnerable. For these reasons we expect the probability of an application being vulnerable to this to be quite low. However if an application is vulnerable then this issue is considered very serious. For these reasons we have assessed this issue as Moderate severity overall.\nThe OpenSSL SSL/TLS implementation is not affected by this issue. The OpenSSL 3.0 and 3.1 FIPS providers are not affected by this because the issue lies outside of the FIPS provider boundary. OpenSSL 3.1 and 3.0 are vulnerable to this issue.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\n- Notable Vulnerable Software Vendors: [\'Debian\', \'Netapp\', \'Openssl\']\n\nExample {idx}: Checklist:\n[\n\t"Check OpenSSL Version: What version of OpenSSL is running in the container image? The vulnerability specifically affects OpenSSL versions 3.0 and 3.1. Is the version running in the container within the vulnerability range? If the container is running these versions, it may be vulnerable.",\n\t"Identify Affected Cipher Modes: Does the application within the container image use any of the affected cipher modes: RC2, RC4, RC5, CCM, GCM, or OCB? Special attention should be given to applications using CCM, GCM, and OCB modes as these are particularly noted for potential loss of confidentiality due to IV truncation.",\n\t"Review Cryptographic Operations: Does the code or configuration of applications using OpenSSL have any instances where `EVP_EncryptInit_ex2()`, `EVP_DecryptInit_ex2()`, or `EVP_CipherInit_ex2()` are called? Are there any modifications to the `keylen` or `ivlen` parameters after initialization which might not be taking effect as intended?",\n\t"Check for Custom Cryptographic Implementations: Since changing the key and/or IV lengths is not a common operation and the issue is in a recently introduced API, it\'s crucial to identify if any custom cryptographic implementations might be performing such operations. This is less likely but should be checked especially in bespoke or highly customized applications. Are there any custom cryptographic implemenations changing the key and/or IV lengths?"\n]', - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2024-2961\n- CVE description: The iconv() function in the GNU C Library versions 2.39 and older may overflow the output buffer passed to it by up to 4 bytes when converting strings to the ISO-2022-CN-EXT character set, which may be used to crash an application or overwrite a neighbouring variable.\n- CWE Name: CWE-787: Out-of-bounds Write (4.14)\n- CWE Description: The product writes data past the end, or before the beginning, of the intended buffer.\nTypically, this can result in corruption of data, a crash, or code execution. The product may modify an index or perform pointer arithmetic that references a memory location that is outside of the boundaries of the buffer. A subsequent write operation then produces undefined or unexpected results.\n- Notable Vulnerable Software Vendors: [\'GNU\']\n\nExample {idx}: Checklist:\n[\n\t"Identify Usage of `iconv()` Function: Review the application code or dependencies. Is the `iconv()` function used? Look particularly for conversions involving the ISO-2022-CN-EXT character set. This function is the specific target of the vulnerability.",\n\t"Assess Data Handling and Boundary Conditions: Since the vulnerability involves an out-of-bounds write, it\'s crucial to analyze how data boundaries are handled in the code. Are there any custom implementations or patches that might mitigate boundary issues around buffer sizes?",\n\t"Review Application\'s Character Encoding Needs: Does the application specifically need to handle the ISO-2022-CN-EXT character set? If not, consider disabling this character set or using alternative safe functions or libraries for character set conversions.",\n\t"Evaluate Network Exposure and Attack Surface: Are the affected services exposed to the network? If so, this could increase the risk of exploitation. Additionally, if the application using the `iconv()` function is accessible externally, the risk is higher."\n]', - "Example {idx}: CVE Details:\n- CVE ID: GHSA-8ghj-p4vj-mr35\n- CVE description: An issue was discovered in Pillow before 10.0.0. It is a Denial of Service that uncontrollably allocates memory to process a given task, potentially causing a service to crash by having it run out of memory. This occurs for truetype in ImageFont when textlength in an ImageDraw instance operates on a long text argument.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n- CWE Name: CWE-770: Allocation of Resources Without Limits or Throttling (4.14)\n- CWE Description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.\n- Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily. Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.\n- Notable Vulnerable Software Vendors: ['Fedoraproject', 'Python']\n- GHSA Summary: Pillow Denial of Service vulnerability\n- GHSA Details: [<'first_patched_version': '10.0.0', 'package': <'ecosystem': 'pip', 'name': 'pillow'>, 'vulnerable_functions': ['PIL.ImageFont'], 'vulnerable_version_range': '>= 0, < 10.0.0'>]\n- GHSA CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n\nExample {idx}: Checklist:\n[\n\t\"Assess Usage of Vulnerable Functions: Specifically, the vulnerability is related to the `PIL.ImageFont` module when processing long text arguments. Does the application code or dependencies use this module and functionality? If your applications use this module to process user-supplied or uncontrolled text inputs, they are likely at risk.\",\n\t\"Evaluate Resource Limits: The vulnerability leads to a denial of service through memory exhaustion. Are there any resource limits set at the container level (e.g., using Docker or Kubernetes settings) that might mitigate the impact of such an attack? Consider setting or reviewing memory limits to prevent a single container from consuming all available system resources.\"\n]", -] - -ex_statements = [ - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2023-24329\n- CVE description: An issue in the urllib.parse component of Python before 3.11.4 allows attackers to bypass blocklisting methods by supplying a URL that starts with blank characters.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N\n- CWE Name: CWE-20: Improper Input Validation (4.14)\n- CWE Description: The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.\nInput validation is a frequently-used technique for checking potentially dangerous inputs in order to ensure that the inputs are safe for processing within the code, or when communicating with other components. When software does not validate input properly, an attacker is able to craft the input in a form that is not expected by the rest of the application. This will lead to parts of the system receiving unintended input, which may result in altered control flow, arbitrary control of a resource, or arbitrary code execution. Input validation is not the only technique for processing input, however. Other techniques attempt to transform potentially-dangerous input into something safe, such as filtering (CWE-790) - which attempts to remove dangerous inputs - or encoding/escaping (CWE-116), which attempts to ensure that the input is not misinterpreted when it is included in output to another component. Other techniques exist as well (see CWE-138 for more examples.) Input validation can be applied to: raw data - strings, numbers, parameters, file contents, etc. metadata - information about the raw data, such as headers or size Data can be simple or structured. Structured data can be composed of many nested layers, composed of combinations of metadata and raw data, with other simple or structured data. Many properties of raw data or metadata may need to be validated upon entry into the code, such as: specified quantities such as size, length, frequency, price, rate, number of operations, time, etc. implied or derived quantities, such as the actual size of a file instead of a specified size indexes, offsets, or positions into more complex data structures symbolic keys or other elements into hash tables, associative arrays, etc. well-formedness, i.e. syntactic correctness - compliance with expected syntax lexical token correctness - compliance with rules for what is treated as a token specified or derived type - the actual type of the input (or what the input appears to be) consistency - between individual data elements, between raw data and metadata, between references, etc. conformance to domain-specific rules, e.g. business logic equivalence - ensuring that equivalent inputs are treated the same authenticity, ownership, or other attestations about the input, e.g. a cryptographic signature to prove the source of the data Implied or derived properties of data must often be calculated or inferred by the code itself. Errors in deriving properties may be considered a contributing factor to improper input validation. Note that "input validation" has very different meanings to different people, or within different classification schemes. Caution must be used when referencing this CWE entry or mapping to it. For example, some weaknesses might involve inadvertently giving control to an attacker over an input when they should not be able to provide an input at all, but sometimes this is referred to as input validation. Finally, it is important to emphasize that the distinctions between input validation and output escaping are often blurred, and developers must be careful to understand the difference, including how input validation is not always sufficient to prevent vulnerabilities, especially when less stringent data types must be supported, such as free-form text. Consider a SQL injection scenario in which a person\'s last name is inserted into a query. The name "O\'Reilly" would likely pass the validation step since it is a common last name in the English language. However, this valid name cannot be directly inserted into the database because it contains the "\'" apostrophe character, which would need to be escaped or otherwise transformed. In this case, removing the apostrophe might reduce the risk of SQL injection, but it would produce incorrect behavior because the wrong name would be recorded.\n- Notable Vulnerable Software Vendors: [\'Fedoraproject\', \'Netapp\', \'Python\']\n\nExample {idx}: Checklist:\n[\n\t"Check Python Version: Determine the version of Python installed in the container image. The vulnerability affects Python versions before 3.11.4. Use commands like `python --version` or `python3 --version` to check the installed version. If the version is below 3.11.4, the container may be vulnerable.",\n\t"Review Python Usage: Identify if the Python installation is actively used by applications within the container. Check for scripts or applications that rely on Python, particularly those that might parse URLs using `urllib.parse`.",\n\t"Inspect URL Parsing Logic: Examine the codebase for any usage of `urllib.parse` or similar URL parsing mechanisms. Focus on how URLs are handled and whether there is a reliance on blocklisting methods to filter out potentially harmful URLs. This is crucial since the CVE describes a bypass of blocklisting methods via URLs starting with blank characters.",\n\t"Evaluate Input Validation Practices: Assess the robustness of input validation practices within the application. Since the CVE involves improper input validation, ensure that inputs, especially URLs, are properly sanitized and validated against unexpected or malicious data."\n]', - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2023-36632\n- CVE description: The legacy email.utils.parseaddr function in Python through 3.11.4 allows attackers to trigger "RecursionError: maximum recursion depth exceeded while calling a Python object" via a crafted argument. This argument is plausibly an untrusted value from an application\'s input data that was supposed to contain a name and an e-mail address. NOTE: email.utils.parseaddr is categorized as a Legacy API in the documentation of the Python email package. Applications should instead use the email.parser.BytesParser or email.parser.Parser class. NOTE: the vendor\'s perspective is that this is neither a vulnerability nor a bug. The email package is intended to have size limits and to throw an exception when limits are exceeded; they were exceeded by the example demonstration code.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n- CWE Name: CWE-674: Uncontrolled Recursion (4.14)\n- CWE Description: The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack.\n- Notable Vulnerable Software Vendors: [\'Python\']\n\nExample {idx}: Checklist:\n[\n\t"Identify Usage of `email.utils.parseaddr`: Review the application code within the container image to check if the `email.utils.parseaddr` function is being used. This function is the specific target of the vulnerability. Consider searching the codebase for the string \'email.utils.parseaddr\' to find direct usages.",\n\t"Assess Input Data Handling: Determine if the application using `email.utils.parseaddr` processes potentially untrusted input data that could include crafted arguments designed to exploit this vulnerability. Evaluate how the application handles exceptions like `RecursionError`, as the exploit triggers this specific error.",\n\t"Review Alternative Implementations: Check if the application has already migrated to recommended alternatives such as `email.parser.BytesParser` or `email.parser.Parser`. If not, recommend transitioning away from the legacy `email.utils.parseaddr` to these safer alternatives."\n]', - "Example {idx}: CVE Details:\n- CVE ID: CVE-2023-50447\n- CVE description: Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter).\n- CVSS Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\n- CWE Name: CWE-94: Improper Control of Generation of Code ('Code Injection') (4.14)\n- CWE Description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.\nWhen a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution. Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.\n- Notable Vulnerable Software Vendors: ['Debian', 'Python']\n\nExample {idx}: Checklist:\n[\n\t\"Evaluate Use of PIL.ImageMath.eval: Specifically investigate whether any application within the container uses the PIL.ImageMath.eval function with user-supplied input in the environment parameter. This is the method and parameter where the vulnerability exists.\",\n\t\"Assess Data Input Sources: Determine if the applications using Pillow are receiving input directly from untrusted sources (e.g., user uploads, external APIs). This will help in understanding the risk exposure and potential for exploitation.\",\n\t\"Security Controls and Sanitization: Review the application's input validation and sanitization measures. Since the vulnerability allows for arbitrary code execution via code injection, ensuring that input is properly sanitized before being processed can mitigate potential exploitation.\"\n]", - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2023-5363\n- CVE description: Issue summary: A bug has been identified in the processing of key and initialisation vector (IV) lengths. This can lead to potential truncation or overruns during the initialisation of some symmetric ciphers. Impact summary: A truncation in the IV can result in non-uniqueness, which could result in loss of confidentiality for some cipher modes. When calling EVP_EncryptInit_ex2(), EVP_DecryptInit_ex2() or EVP_CipherInit_ex2() the provided OSSL_PARAM array is processed after the key and IV have been established. Any alterations to the key length, via the "keylen" parameter or the IV length, via the "ivlen" parameter, within the OSSL_PARAM array will not take effect as intended, potentially causing truncation or overreading of these values. The following ciphers and cipher modes are impacted: RC2, RC4, RC5, CCM, GCM and OCB. For the CCM, GCM and OCB cipher modes, truncation of the IV can result in loss of confidentiality. For example, when following NIST\'s SP 800-38D section 8.2.1 guidance for constructing a deterministic IV for AES in GCM mode, truncation of the counter portion could lead to IV reuse. Both truncations and overruns of the key and overruns of the IV will produce incorrect results and could, in some cases, trigger a memory exception. However, these issues are not currently assessed as security critical.\nChanging the key and/or IV lengths is not considered to be a common operation and the vulnerable API was recently introduced. Furthermore it is likely that application developers will have spotted this problem during testing since decryption would fail unless both peers in the communication were similarly vulnerable. For these reasons we expect the probability of an application being vulnerable to this to be quite low. However if an application is vulnerable then this issue is considered very serious. For these reasons we have assessed this issue as Moderate severity overall.\nThe OpenSSL SSL/TLS implementation is not affected by this issue. The OpenSSL 3.0 and 3.1 FIPS providers are not affected by this because the issue lies outside of the FIPS provider boundary. OpenSSL 3.1 and 3.0 are vulnerable to this issue.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\n- Notable Vulnerable Software Vendors: [\'Debian\', \'Netapp\', \'Openssl\']\n\nExample {idx}: Checklist:\n[\n\t"Identify Affected Cipher Modes: Determine if the application within the container image uses any of the affected cipher modes: RC2, RC4, RC5, CCM, GCM, or OCB. Special attention should be given to applications using CCM, GCM, and OCB modes as these are particularly noted for potential loss of confidentiality due to IV truncation.",\n\t"Review Cryptographic Operations: Examine the code or configuration of applications using OpenSSL for any instances where `EVP_EncryptInit_ex2()`, `EVP_DecryptInit_ex2()`, or `EVP_CipherInit_ex2()` are called. Check if there are any modifications to the `keylen` or `ivlen` parameters after initialization which might not be taking effect as intended.",\n\t"Check for Custom Cryptographic Implementations: Since changing the key and/or IV lengths is not a common operation and the issue is in a recently introduced API, it\'s crucial to identify if any custom cryptographic implementations might be performing such operations. This is less likely but should be checked especially in bespoke or highly customized applications."\n]', - 'Example {idx}: CVE Details:\n- CVE ID: CVE-2024-2961\n- CVE description: The iconv() function in the GNU C Library versions 2.39 and older may overflow the output buffer passed to it by up to 4 bytes when converting strings to the ISO-2022-CN-EXT character set, which may be used to crash an application or overwrite a neighbouring variable.\n- CWE Name: CWE-787: Out-of-bounds Write (4.14)\n- CWE Description: The product writes data past the end, or before the beginning, of the intended buffer.\nTypically, this can result in corruption of data, a crash, or code execution. The product may modify an index or perform pointer arithmetic that references a memory location that is outside of the boundaries of the buffer. A subsequent write operation then produces undefined or unexpected results.\n- Notable Vulnerable Software Vendors: [\'GNU\']\n\nExample {idx}: Checklist:\n[\n\t"Identify Usage of `iconv()` Function: Review the application code or dependencies to check if the `iconv()` function is used, particularly for conversions involving the ISO-2022-CN-EXT character set. This function is the specific target of the vulnerability.",\n\t"Assess Data Handling and Boundary Conditions: Since the vulnerability involves an out-of-bounds write, it\'s crucial to analyze how data boundaries are handled in the code. Look for any custom implementations or patches that might mitigate boundary issues around buffer sizes.",\n\t"Review Application\'s Character Encoding Needs: Determine if the application specifically needs to handle the ISO-2022-CN-EXT character set. If not, consider disabling this character set or using alternative safe functions or libraries for character set conversions.",\n\t"Evaluate Network Exposure and Attack Surface: Consider whether the affected services are exposed to the network, which could increase the risk of exploitation. If the application using the `iconv()` function is accessible externally, the risk is higher."\n]', - "Example {idx}: CVE Details:\n- CVE ID: GHSA-8ghj-p4vj-mr35\n- CVE description: An issue was discovered in Pillow before 10.0.0. It is a Denial of Service that uncontrollably allocates memory to process a given task, potentially causing a service to crash by having it run out of memory. This occurs for truetype in ImageFont when textlength in an ImageDraw instance operates on a long text argument.\n- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n- CWE Name: CWE-770: Allocation of Resources Without Limits or Throttling (4.14)\n- CWE Description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.\n- Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily. Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.\n- Notable Vulnerable Software Vendors: ['Fedoraproject', 'Python']\n- GHSA Summary: Pillow Denial of Service vulnerability\n- GHSA Details: [<'first_patched_version': '10.0.0', 'package': <'ecosystem': 'pip', 'name': 'pillow'>, 'vulnerable_functions': ['PIL.ImageFont'], 'vulnerable_version_range': '>= 0, < 10.0.0'>]\n- GHSA CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n\nExample {idx}: Checklist:\n[\n\t\"Assess Usage of Vulnerable Functions: Specifically, the vulnerability is related to the `PIL.ImageFont` module when processing long text arguments. Review the application code or dependencies to see if this module and functionality are used. If your applications use this module to process user-supplied or uncontrolled text inputs, they are likely at risk.\",\n\t\"Evaluate Resource Limits: Since the vulnerability leads to a denial of service through memory exhaustion, check if there are any resource limits set at the container level (e.g., using Docker or Kubernetes settings) that might mitigate the impact of such an attack. Consider setting or reviewing memory limits to prevent a single container from consuming all available system resources.\"\n]", -] - -FEW_SHOT = """Generate a checklist for a security analyst to use when assessing the exploitability of a specific CVE within a containerized environment. Use the provided examples as a guide to understand how to construct a checklist from a given set of CVE details, then apply this understanding to create a specific checklist for the CVE details provided below. All output should be a comma separated list enclosed in square brackets with each list item enclosed in quotes. - -Example 1: CVE Details: -- CVE ID: CVE-2024-23334 -- CVE Description: aiohttp is an asynchronous HTTP client/server framework for asyncio and Python. When using aiohttp as a web server and configuring static routes, it is necessary to specify the root path for static files. Additionally, the option 'follow_symlinks' can be used to determine whether to follow symbolic links outside the static root directory. When 'follow_symlinks' is set to True, there is no validation to check if reading a file is within the root directory. This can lead to directory traversal vulnerabilities, resulting in unauthorized access to arbitrary files on the system, even when symlinks are not present. Disabling follow_symlinks and using a reverse proxy are encouraged mitigations. Version 3.9.2 fixes this issue. -- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N -- CWE Name: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (4.14) -- CWE Description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. -Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin", which may also be useful in accessing unexpected files. This is referred to as absolute path traversal. In many programming languages, the injection of a null byte (the 0 or NUL) may allow an attacker to truncate a generated filename to widen the scope of attack. For example, the product may add ".txt" to any pathname, thus limiting the attacker to text files, but a null injection may effectively remove this restriction. -- Notable Vulnerable Software Vendors: ['Aiohttp', 'Fedoraproject'] - -Example 1: Checklist: -[ - "Vulnerable package check. Does the project use the aiohttp library, which is the affected package? If aiohttp is not a dependency in your project, then your code is not vulnerable to this CVE.", - "Vulnerable version check. Is the version of aiohttp that the project depends on vulnerable? According to the vulnerability details, versions before 3.9.2 are vulnerable., - "Review code to check for vulnerability mitigation. Is the 'follow_symlinks' option set to False to mitigate the risk of directory traversal vulnerabilities?" -] - -Example 2: CVE Details: -- CVE ID: CVE-2022-2309 -- CVE description: NULL Pointer Dereference allows attackers to cause a denial of service (or application crash). This only applies when lxml (version 4.9.0 and earlier) is used together with libxml2 2.9.10 through 2.9.14. libxml2 2.9.9 and earlier are not affected. It allows triggering crashes through forged input data, given a vulnerable code sequence in the application. The vulnerability is caused by the iterwalk function (also used by the canonicalize function). Such code shouldn't be in wide-spread use, given that parsing + iterwalk would usually be replaced with the more efficient iterparse function. However, an XML converter that serialises to C14N would also be vulnerable, for example, and there are legitimate use cases for this code sequence. If untrusted input is received (also remotely) and processed via iterwalk function, a crash can be triggered. -- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H -- CWE Name: CWE-476: NULL Pointer Dereference (4.14) -- CWE Description: A NULL pointer dereference occurs when the application dereferences a pointer that it expects to be valid, but is NULL, typically causing a crash or exit. -NULL pointer dereference issues can occur through a number of flaws, including race conditions, and simple programming omissions. -- Notable Vulnerable Software Vendors: ['Fedoraproject', 'Lxml', 'Xmlsoft'] - -Example 2: Checklist: -[ - “Vulnerable package check. Does the project use the lxml library, which is the affected package? If lxml is not a dependency in your project, then your code is not vulnerable to this CVE.”, - “Vulnerable version check. Is the version of lxml that the project depends on vulnerable? According to the vulnerability details, versions 4.9.0 and earlier are vulnerable.”, - “Vulnerable version check of connected dependency. Is the version of libxml, the connected dependency, that the project depends on vulnerable? The package is only vulnerable if libxml 2.9.10 through 2.9.14 is also present.”, - “Review code for vulnerable functionality. The library is vulnerable through its `iterwalk` function, which is also utilized by the `canonicalize` function. Are either of these functions used in your code base?” -] - -Given CVE Details: -""" + additional_intel_prompting - -MOD_FEW_SHOT = """Generate a checklist for a security analyst to use when assessing the exploitability of a specific CVE within a containerized environment. Use the provided examples as a guide to understand how to construct a checklist from a given set of CVE details, then apply this understanding to create a specific checklist for the CVE details provided below. All output should be a comma separated list enclosed in square brackets with each list item enclosed in quotes. - -{examples} - -Given CVE Details: -""" - -investigation_guideline = """1. Is the flagged component in the product? Determine if the container image includes the flagged package version. Verify the presence of the library or software in question by checking the container's software bill of materials (SBOM). -2. Is the vulnerable code of the flagged component in the product? Check if an application or any dependency within the container image uses a function or a component of the library that contains the vulnerability. Check whether a patch has been applied or if the vulnerable code has been removed. -3. Is the vulnerable code of the flagged component executed by the product? Analyze the application's execution paths to confirm whether the vulnerable code is executed during normal operation. Examine the application's dependencies to ensure there are no indirect execution paths that could trigger the vulnerable code. -4. If the vulnerable code is executed, is it exploitable? Investigate whether the exploitability of the issue depends on a specific configuration option and if this configuration is enabled. -5. If the vulnerable code is executed, is it exploitable? Determine if the exploitability relies on a library dependency and verify the existence of this dependency in the product. -6. If the vulnerable code is executed, is it exploitable? Research if the exploitability of the issue depends on a specific environment and verify whether this environment is absent or present in the container. -7. Does other protection exist? Compiler Flags: Assess whether the exploitability hinges on the setting or unsetting of compiler flags. -8. Does other protection exist? Runtime Protections: Investigate the presence of mechanisms that prevent exploits during runtime. -9. Does other protection exist? Perimeter Defenses: Explore protective measures that block attacks at the physical, logical, or network perimeters. -10. Does other protection exist? Mitigating Controls: Identify any mitigating controls in place to prevent exploitability. -""" - -ZERO_SHOT = f"""Your task is to create a checklist for a security analyst to help determine if a given CVE is -exploitable in a containerized environment. Use the information provided below, structured within XML tags for both CVE -details and investigation guidelines. The checklist should convert the steps from the investigation guidelines into -actionable steps specific to the CVE. Each step should be self-contained, including details specific to the CVE, and -start with an action verb to clearly state the step the analyst needs to take. Avoid references to the CVE ID, focusing -on providing detailed investigative directions within the steps themselves. -All output should be a comma separated list enclosed in square brackets with each list item enclosed in quotes.\n -{additional_intel_prompting}\n{investigation_guideline}""" - -_ONE_SHOT = """This is an example of (1) CVE information, and (2) a checklist produced to determine if a given CVE is exploitable in a containerized environment: -(1) CVE Information: -CVE Description: DISPUTED: In Apache Batik 1.x before 1.10, when deserializing subclass of `AbstractDocument`, the class takes a string from the inputStream as the class name which then use it to call the no-arg constructor of the class. Fix was to check the class type before calling newInstance in deserialization. -CVSS Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H -CWE Name: CWE-502: Deserialization of Untrusted Data (4.11) -CWE Description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid. It is often convenient to serialize objects for communication or to save them for later use. However, deserialized data or code can often be modified without using the provided accessor functions if it does not use cryptography to protect itself. Furthermore, any cryptography would still be client-side security -- which is a dangerous security assumption. Data that is untrusted can not be trusted to be well-formed. When developers place no restrictions on gadget chains, or series of instances and method invocations that can self-execute during the deserialization process (i.e., before the object is returned to the caller), it is sometimes possible for attackers to leverage them to perform unauthorized actions, like generating a shell. -Notable Vulnerable Software Vendors: ['Oracle', 'Apache', 'Canonical', 'Debian'] -(2) Checklist: -Based on the information available, CVE-2018-8013 is a critical vulnerability that involves the deserialization of untrusted data in Apache Batik before version 1.10. The Common Vulnerability Scoring System (CVSS) 3.x score is 9.8, indicating a critical severity. -All output should be a comma separated list enclosed in square brackets with each list item enclosed in quotes. -Here's an example of preliminary checklist similiar to what you should create to determine if a container image is vulnerable to this exploit: -["1. Check the Apache Batik version: The vulnerability affects Apache Batik versions 1.0 to 1.9.1. If your container image is running Apache Batik, verify the version. If it's between 1.0 and 1.9.1, it's vulnerable and you should update to version 1.10 or later.", -"2. Check for any software using vulnerable Apache Batik versions: It's important to note that other applications might use Apache Batik and thus be vulnerable. Check all applications within your container image for dependencies on vulnerable versions of Apache Batik. Some notable software includes certain versions of Ubuntu Linux, Debian Linux, and multiple Oracle applications.", -"3. Evaluate the deserialization risk: Since the vulnerability involves deserialization of untrusted data, you should consider whether your applications are performing such operations. If they are, consider if these deserialization operations are using untrusted, user-supplied data. If so, this is a potential attack vector.", -"4. Network exposure: The vulnerability has a network attack vector. Consider whether your container image has network exposure, especially if the Apache Batik service or any service using it is exposed to the internet.", -"5. Disputed status: CVE-2018-8013 is disputed and may not be a vulnerability."] - -Given the following information about {{cve_id}}, make preliminary checklist for a security analyst to follow to determine whether a container image is vulnerable to this exploit. -""" + additional_intel_prompting - - -def get_mod_examples(type='questions', choices=[0, 1]): - if type == 'questions': - ex_list = [q for idx, q in enumerate(ex_questions) if idx in choices] - else: - ex_list = [s for idx, s in enumerate(ex_statements) if idx in choices] - - examples = '\n'.join(q.format(idx=idx + 1) for idx, q in enumerate(ex_list)) - return examples diff --git a/src/vuln_analysis/utils/report_generator.py b/src/vuln_analysis/utils/report_generator.py new file mode 100644 index 000000000..2dc865aab --- /dev/null +++ b/src/vuln_analysis/utils/report_generator.py @@ -0,0 +1,362 @@ +"""Template-based report generation for early-exit exploitability verdicts. + +When the deterministic pre-triage determines that a CVE is NOT APPLICABLE +or ENVIRONMENT ISOLATED, this module generates the full markdown report +from a template — with zero LLM calls. + +The report format matches the same schema consumed by the downstream +VEX Categorizer agent. +""" + +from __future__ import annotations + +from datetime import date + +from vuln_analysis.data_models.exploitability_analyzer import ( + ExploitabilityAnalyzerInput, + PreTriageResult, +) + + +def generate_early_exit_report( + result: PreTriageResult, + inp: ExploitabilityAnalyzerInput, +) -> str: + """Generate a complete markdown exploitability report for an early exit. + + The caller is responsible for ensuring ``result.early_exit`` is True + before calling this function. + """ + vr = result.vuln_report + cr = result.container_report + pkg = result.package_verification + additional = result.additional_package_verifications + verdict = result.early_exit_verdict or "NOT APPLICABLE" + reason = result.early_exit_reason or "" + + # --- Step 1 evidence --- + if pkg.found and pkg.installations: + primary = pkg.active_env_installation or pkg.installations[0] + fs_version = primary.version + fs_location = primary.location + pkg_in_fs = "Yes" + version_vulnerable = "No" if pkg.is_vulnerable is False else "Yes" + in_active_env_str = _format_active_env(primary, cr.default_environment) + else: + fs_version = "N/A" + fs_location = "not found" + pkg_in_fs = "No" + version_vulnerable = "N/A" + in_active_env_str = "N/A" + + installed_version = fs_version + backport_str = _format_backport(pkg) + + # --- Conditions matrix --- + pkg_present_icon = _icon(pkg.found) + version_vuln_icon = _icon(pkg.is_vulnerable is True) + env_icon = "N/A" if not pkg.found else _icon( + pkg.active_env_installation is not None + and pkg.is_vulnerable is not False + ) + + skip_reason = reason if reason else f"Package is {verdict.lower().replace('_', ' ')}." + + # --- Vulnerable range string --- + if vr.affected_ranges: + range_parts = [] + for r in vr.affected_ranges: + range_parts.append(f"{r.branch}: {r.lower}, {r.upper}" if r.upper else f"{r.branch}: {r.lower}") + vuln_range_str = "; ".join(range_parts) + else: + vuln_range_str = vr.vulnerable_versions + + # --- Entry points summary --- + entry_points_str = ", ".join(cr.entry_points) if cr.entry_points else "N/A" + container_ref = f"{cr.image_name}:{cr.image_tag}" if cr.image_tag else cr.image_name + + report = f"""# Container Exploitability Analysis + +## Summary + +| Field | Value | +|-------|-------| +| **CVE ID** | {vr.cve_id} | +| **Container** | {container_ref} | +| **Container Report** | {inp.container_report_path} | +| **Vulnerability Report** | {inp.vulnerability_report_path} | +| **Filesystem Path** | {inp.filesystem_path} | +| **Analysis Date** | {date.today().isoformat()} | +| **Verdict** | {verdict} | +| **Confidence** | High | +| **Risk Level** | Informational | + +## Input Summary + +### Container Context (from Container Report) + +| Attribute | Value | +|-----------|-------| +| **Container Type** | {cr.container_type} | +| **Primary Language** | {cr.primary_language} | +| **Base OS** | {cr.base_os} | +| **Default Environment** | {cr.default_environment or "N/A"} | +| **Runtime User** | {cr.runtime_user} | +| **Key Entry Points** | {entry_points_str} | + +### Vulnerability Context (from CVE Report) + +| Attribute | Value | +|-----------|-------| +| **Vulnerable Package** | {vr.package_raw} | +| **Vulnerable Versions** | {vr.vulnerable_versions} | +| **Fixed Version** | {vr.fixed_in} | +| **Vulnerable Function** | {vr.vulnerable_function or "Not specified — module-level analysis"} | +| **CVSS Score** | {_format_cvss(vr.cvss_score, vr.cvss_severity)} | +| **CWE** | {vr.cwe or "N/A"} | + +### Exploitation Hypothesis + +For this CVE to be exploitable in this container: +1. {vr.package_raw} must be installed at a vulnerable version +2. The package must be in the active runtime environment +3. The vulnerable code path must be reachable from the application + +{_additional_packages_section(vr, additional)}## Analysis + +### Step 1: Vulnerable Component Presence + +| Check | Result | Evidence | +|-------|--------|----------| +| **Package in Filesystem** | {pkg_in_fs} | {fs_location} | +| **Installed Version** | {installed_version} | Filesystem verification | +| **Vulnerable Range** | {vuln_range_str} | From CVE report | +| **Distro Backport Check** | {backport_str} | {_backport_evidence(pkg)} | +| **Version is Vulnerable** | {version_vulnerable} | {_version_evidence(pkg, vr)} | +| **In Active Environment** | {in_active_env_str} | {_env_evidence(pkg, cr)} | + +{_additional_packages_step1(additional)}### Step 2: Code Reachability + +Skipped — {skip_reason} + +### Step 3: Data Flow and Trigger Conditions + +Skipped — {skip_reason} + +### Step 4: Security Controls and Impact + +Skipped — {skip_reason} + +## Exploitability Assessment + +### Conditions Matrix + +| Condition | Satisfied | Explanation | +|-----------|-----------|-------------| +| Vulnerable package present and confirmed | {pkg_present_icon} | {_pkg_present_explanation(pkg, vr)} | +| Installed version vulnerable (backport checked) | {version_vuln_icon} | {_version_vuln_explanation(pkg, vr)} | +| Package in active environment | {env_icon} | {_env_explanation(pkg, cr, verdict)} | +| Vulnerable code reachable | N/A | Analysis stopped at Step 1 | +| Code in default execution path | N/A | Analysis stopped at Step 1 | +| Trigger conditions satisfiable | N/A | Analysis stopped at Step 1 | +| Attacker can control input | N/A | Analysis stopped at Step 1 | +| Not blocked by security controls | N/A | Analysis stopped at Step 1 | + +### Container Type Consideration + +**Container Type:** {cr.container_type} + +{_container_type_note(cr, verdict)} + +### Verdict: {verdict} + +**Confidence:** High + +**Justification:** +{reason} + +### Attack Scenario + +Not applicable — the vulnerability cannot be triggered in this container. + +## References + +- Container Report: {inp.container_report_path} +- Vulnerability Report: {inp.vulnerability_report_path} +- Filesystem: {inp.filesystem_path} +""" + return report.strip() + "\n" + + +# --------------------------------------------------------------------------- +# Formatting helpers +# --------------------------------------------------------------------------- + + +def _icon(condition: bool) -> str: + return "\u2705" if condition else "\u274c" + + +def _format_cvss(score: float | None, severity: str | None) -> str: + if score is not None and severity: + return f"{score} {severity}" + if score is not None: + return str(score) + return "N/A" + + +def _format_backport(pkg) -> str: + if pkg.backport_status == "patched": + return "Patched" + if pkg.backport_status == "not_patched": + return "Not patched" + if pkg.backport_status == "inconclusive": + return "Inconclusive" + if pkg.backport_status is None: + return "N/A" + return pkg.backport_status + + +def _backport_evidence(pkg) -> str: + if pkg.backport_evidence: + return pkg.backport_evidence[:200] + return "N/A" + + +def _format_active_env(inst, default_env: str | None) -> str: + if inst.environment == default_env: + return f"Yes ({inst.environment})" + if inst.environment: + return f"No (in {inst.environment}, default is {default_env})" + return "Yes" + + +def _version_evidence(pkg, vr) -> str: + if not pkg.found: + return "Package not present" + if pkg.is_vulnerable is False: + inst = pkg.active_env_installation or pkg.installations[0] + return f"Version {inst.version} is patched (fixed in {vr.fixed_in})" + if pkg.is_vulnerable is True: + inst = pkg.active_env_installation or pkg.installations[0] + return f"Version {inst.version} is within vulnerable range" + return "Version comparison uncertain" + + +def _env_evidence(pkg, cr) -> str: + if not pkg.found: + return "Package not present" + if pkg.active_env_installation: + inst = pkg.active_env_installation + return f"Found in {inst.environment} environment ({inst.source}, v{inst.version})" + vuln_envs = [ + f"{inst.environment or 'unknown'} (v{inst.version})" + for inst in pkg.installations + ] + return ( + f"Not in default environment '{cr.default_environment}'; " + f"found in: {', '.join(vuln_envs)}" + ) + + +def _pkg_present_explanation(pkg, vr) -> str: + if pkg.found: + return f"Found {len(pkg.installations)} installation(s) on filesystem" + return f"'{vr.package_raw}' not found on filesystem" + + +def _version_vuln_explanation(pkg, vr) -> str: + if not pkg.found: + return "Package not present" + if pkg.is_vulnerable is False: + return "All installed versions are patched" + if pkg.is_vulnerable is True: + return "At least one installed version is within the vulnerable range" + return "Unable to determine vulnerability status" + + +def _env_explanation(pkg, cr, verdict) -> str: + if not pkg.found: + return "Package not present" + if verdict == "ENVIRONMENT ISOLATED": + return ( + f"Vulnerable version only in non-default environment; " + f"default is '{cr.default_environment}'" + ) + if pkg.active_env_installation: + return f"Found in active environment '{pkg.active_env_installation.environment}'" + return "Environment check not applicable" + + +def _additional_packages_section(vr, additional) -> str: + """Render the 'Additional Affected Packages' block for the Vulnerability Context.""" + if not additional: + return "" + lines = [ + "### Additional Affected Packages", + "", + "This CVE also affects the following packages (verified during pre-triage):", + "", + "| Package | Ecosystem | Vulnerable Versions | Fixed In | Found | Status |", + "|---------|-----------|--------------------:|----------|-------|--------|", + ] + for apr in additional: + e = apr.entry + v = apr.verification + eco = e.ecosystem or "\u2014" + found = "Yes" if v.found else "No" + if v.found and v.is_vulnerable is False: + status = "Patched" + elif v.found and v.is_vulnerable is True: + status = "Vulnerable" + elif v.found: + status = "Found (status uncertain)" + else: + status = "Not found" + lines.append( + f"| {e.package} | {eco} | {e.vulnerable_versions} | {e.fixed_in} " + f"| {found} | {status} |" + ) + lines.append("") + return "\n".join(lines) + "\n" + + +def _additional_packages_step1(additional) -> str: + """Render supplemental Step 1 rows for additional affected packages.""" + if not additional: + return "" + lines = [ + "#### Additional Affected Packages \u2014 Component Presence", + "", + ] + for apr in additional: + e = apr.entry + v = apr.verification + if v.found and v.installations: + inst = v.active_env_installation or v.installations[0] + lines.append( + f"- **{e.package}**: Found at {inst.location} " + f"(v{inst.version}, " + f"{'vulnerable' if v.is_vulnerable else 'patched'})" + ) + else: + lines.append(f"- **{e.package}**: Not found on filesystem") + lines.append("") + return "\n".join(lines) + "\n" + + +def _container_type_note(cr, verdict) -> str: + if verdict == "NOT APPLICABLE": + return ( + f"The container is classified as {cr.container_type}. " + f"Analysis did not proceed to reachability because the vulnerable " + f"component is not present or is patched." + ) + if verdict == "ENVIRONMENT ISOLATED": + return ( + f"The container is classified as {cr.container_type}. " + f"The vulnerable version exists in a non-default environment. " + f"The default environment ('{cr.default_environment}') has a patched version. " + f"Switching to the non-default environment requires deliberate action." + ) + return f"The container is classified as {cr.container_type}." diff --git a/src/vuln_analysis/utils/report_parser.py b/src/vuln_analysis/utils/report_parser.py new file mode 100644 index 000000000..4a245a73f --- /dev/null +++ b/src/vuln_analysis/utils/report_parser.py @@ -0,0 +1,11 @@ +"""Shared utility for extracting fields from markdown table rows.""" + +from __future__ import annotations + +import re + + +def extract_markdown_table_field(content: str, field_name: str) -> str | None: + pattern = rf"\*\*{re.escape(field_name)}\*\*\s*\|\s*(.+?)(?:\s*\||\s*$)" + match = re.search(pattern, content, re.MULTILINE) + return match.group(1).strip().strip("`") if match else None diff --git a/src/vuln_analysis/utils/serp_api_wrapper.py b/src/vuln_analysis/utils/serp_api_wrapper.py deleted file mode 100644 index 2c4db8250..000000000 --- a/src/vuln_analysis/utils/serp_api_wrapper.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import aiohttp -from langchain.utils.env import get_from_env -from langchain_community.utilities import SerpAPIWrapper -from pydantic import model_validator - -from vuln_analysis.utils.async_http_utils import retry_async -from vuln_analysis.utils.url_utils import url_join - - -class MorpheusSerpAPIWrapper(SerpAPIWrapper): - - base_url: str = "https://serpapi.com" - max_retries: int = 10 - - @model_validator(mode="after") - def validate_base_url(self) -> "MorpheusSerpAPIWrapper": - """Validate the base URL from the environment.""" - self.base_url = get_from_env(key="base_url", env_key="SERPAPI_BASE_URL", default=self.base_url) - if not self.base_url: - raise ValueError("SERPAPI_BASE_URL must not be empty") - # Update the base URL for search_engine - self.search_engine.BACKEND = self.base_url - return self - - @retry_async() - async def _session_get_with_retry(self, session: aiohttp.ClientSession, url: str, params: dict) -> dict: - - async with session.get(url, params=params) as response: - response.raise_for_status() # Check HTTP status codes - res = await response.json() - - # Check for SerpAPI error in response JSON - if isinstance(res, dict) and 'error' in res: - error_msg = res['error'] - # Check if it's an authentication/API key error - if any(keyword in error_msg.lower() for keyword in ['api key', 'apikey', 'invalid key', 'unauthorized']): - raise ValueError( - f"SERPAPI authentication error: {error_msg}. " - f"Please check your SERPAPI_API_KEY environment variable." - ) - else: - raise ValueError(f"SERPAPI error: {error_msg}") - - return res - - # Override the method with hardcoded URL - async def aresults(self, query: str) -> dict: - """Use aiohttp to run query through SerpAPI and return the results async.""" - - def construct_url_and_params() -> tuple[str, dict[str, str]]: - params = self.get_params(query) - params["source"] = "python" - if self.serpapi_api_key: - params["serp_api_key"] = self.serpapi_api_key - params["output"] = "json" - - # Use the base path for the URL (add a "/" to ensure they get joined) - url = url_join(self.base_url, "search") - return url, params - - url, params = construct_url_and_params() - if not self.aiosession: - async with aiohttp.ClientSession() as session: - res = await self._session_get_with_retry(session, url, params) - else: - res = await self._session_get_with_retry(self.aiosession, url, params) - - return res - - @staticmethod - def _process_response(res: dict) -> str: - """Process SerpAPI response with better error handling.""" - # First check if the response itself contains an error - if isinstance(res, dict) and 'error' in res: - error_msg = res['error'] - # Don't hide authentication errors - if any(keyword in error_msg.lower() for keyword in ['api key', 'apikey', 'invalid key', 'unauthorized']): - raise ValueError( - f"SERPAPI_API_KEY authentication failed: {error_msg}. " - f"Please verify the SERPAPI_API_KEY environment variable is set correctly." - ) - raise ValueError(f"SerpAPI error: {error_msg}") - - try: - return SerpAPIWrapper._process_response(res) - except ValueError as e: - # Re-raise authentication errors, only catch processing errors - error_str = str(e).lower() - if any(keyword in error_str for keyword in ['api key', 'apikey', 'authentication', 'unauthorized']): - raise - return "No good search result found" diff --git a/src/vuln_analysis/utils/source_code_git_loader.py b/src/vuln_analysis/utils/source_code_git_loader.py deleted file mode 100644 index 61d27293d..000000000 --- a/src/vuln_analysis/utils/source_code_git_loader.py +++ /dev/null @@ -1,192 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -import typing -from pathlib import Path - -from git import Blob as GitBlob -from git import Repo -from langchain_community.document_loaders.blob_loaders.schema import BlobLoader -from langchain_core.document_loaders.blob_loaders import Blob -from tqdm import tqdm - -PathLike = typing.Union[str, os.PathLike] - -logger = logging.getLogger(__name__) - - -class SourceCodeGitLoader(BlobLoader): - """ - Load `Git` repository files. - - The Repository can be local on disk available at `repo_path`, - or remote at `clone_url` that will be cloned to `repo_path`. - Currently, supports only text files. - - Each document represents one file in the repository. The `path` points to - the local Git repository, and the `ref` specifies the git reference to load - files from. By default, it loads from the `main` branch. - """ - - def __init__( - self, - repo_path: PathLike, - clone_url: str | None = None, - ref: typing.Optional[str] = "main", - include: typing.Optional[typing.Iterable[str]] = None, - exclude: typing.Optional[typing.Iterable[str]] = None, - ): - """ - Initialize the Git loader. - - Parameters - ---------- - repo_path : PathLike - Path to the local Git repository. - clone_url : str | None, optional - URL to the remote Git repository, by default None - ref : typing.Optional[str], optional - Commit ID to load files from, by default "main" - include : typing.Optional[typing.Iterable[str]], optional - A list of file patterns to include. Uses the glob syntax, by default None - exclude : typing.Optional[typing.Iterable[str]], optional - A list of file patterns to exclude. Uses the glob syntax, by default None - """ - - self.repo_path = Path(repo_path) - self.clone_url = clone_url - self.ref = ref - - self.include = include - self.exclude = exclude - - self._repo: Repo | None = None - - def load_repo(self): - """ - Load the Git repository and return the GitPython `Repo` object. - - Returns - ------- - Repo - GitPython `Repo` object representing the Git repository. - - Raises - ------ - ValueError - If the repository path does not exist. - ValueError - If a different repository is already cloned at the path. - """ - - if (self._repo is not None): - return self._repo - - if not os.path.exists(self.repo_path) and self.clone_url is None: - raise ValueError(f"Path {self.repo_path} does not exist") - elif self.clone_url: - # If the repo_path already contains a git repository, verify that it's the - # same repository as the one we're trying to clone. - if os.path.isdir(os.path.join(self.repo_path, ".git")): - repo = Repo(self.repo_path) - # If the existing repository is not the same as the one we're trying to - # clone, raise an error. - if repo.remotes.origin.url != self.clone_url: - raise ValueError("A different repository is already cloned at this path.") - - logger.info("Updating existing Git repo for URL '%s' @ '%s'", self.clone_url, self.ref) - else: - logger.info("Cloning repository from URL: '%s' @ '%s'", self.clone_url, self.ref) - # Create a shallow clone of the repository without checking out - repo = Repo.clone_from(self.clone_url, self.repo_path, depth=1, no_checkout=True) - - # Set repo as git safe directory to avoid errors if directory ownership is changed outside the pipeline - # https://git-scm.com/docs/git-config#Documentation/git-config.txt-safedirectory - with repo.config_writer(config_level="global") as config: - config.add_value("safe", "directory", str(self.repo_path.absolute())) - - else: - repo = Repo(self.repo_path) - logger.info("Using existing Git repo at path: '%s' @ '%s'", self.repo_path, self.ref) - - # Reliable way to check out the ref using a shallow clone - repo.git.fetch("origin", self.ref, depth=1) - repo.git.checkout("FETCH_HEAD") - - logger.info("Loaded Git repository at path: '%s' @ '%s'", self.repo_path, self.ref) - - self._repo = repo - - return repo - - def yield_blobs(self) -> typing.Iterator[Blob]: - """ - Yield the blobs from the Git repository. One blob will be generated for each file in the repo which passes the - include and exclude filters. - - Returns - ------- - typing.Iterator[Blob] - An iterator of `Blob` objects representing the files in the repository. - - Yields - ------ - Iterator[typing.Iterator[Blob]] - An iterator of `Blob` objects representing the files in the repository. - """ - - repo = self.load_repo() - - logger.info("Scanning documents for Git repository at path: '%s'", self.repo_path) - - all_files_in_repo = [str(item.path) for item in repo.tree().traverse() if isinstance(item, GitBlob)] - - base_path = Path(self.repo_path) - - include_files: set[str] = set() - exclude_files: set[str] = set() - - for inc in self.include or ["**/*"]: - include_files = include_files.union(set(str(x.relative_to(base_path)) for x in base_path.glob(inc))) - - for exc in self.exclude or {}: - exclude_files = exclude_files.union(set(str(x.relative_to(base_path)) for x in base_path.glob(exc))) - - # Filter out files that are not in the repo - include_files = include_files.intersection(all_files_in_repo) - - # Take the include files and remove the exclude files. - final_files = include_files - exclude_files - - logger.info("Processing %d files in the Git repository at path: '%s'", len(final_files), self.repo_path) - - for f in tqdm(final_files): - - file_path = Path(f) - - abs_file_path = base_path / file_path - - rel_file_path = str(file_path) - - metadata = { - "source": rel_file_path, - "file_path": rel_file_path, - "file_name": file_path.name, - "file_type": file_path.suffix, - } - - yield Blob.from_path(abs_file_path, metadata=metadata) diff --git a/src/vuln_analysis/utils/vulnerable_dependency_checker.py b/src/vuln_analysis/utils/vulnerable_dependency_checker.py deleted file mode 100644 index 9e3ac3c4a..000000000 --- a/src/vuln_analysis/utils/vulnerable_dependency_checker.py +++ /dev/null @@ -1,680 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging -import os -import re -import urllib.parse - -import aiohttp -import aiohttp.client_exceptions -import aiohttp.http_exceptions -import requests -from tqdm import tqdm -from univers import versions - -from vuln_analysis.data_models.cve_intel import CveIntelNvd -from vuln_analysis.data_models.dependencies import DependencyPackage -from vuln_analysis.utils.clients.intel_client import IntelClient -from vuln_analysis.utils.string_utils import package_names_match -from vuln_analysis.utils.url_utils import url_join - -logger = logging.getLogger(__name__) - -REQUESTS_TIMEOUT = 2 - -# Mapping of ecosystem strings to standard ecosystem names in deps.dev and univers -SYS_STANDARD_MAPPING = { - ".net": "nuget", - "cargo": "cargo", - "composer": "composer", - "conan": "conan", - "conda": "pypi", - "deb": "deb", - "dpkg": "deb", - "go": "go", - "go-module": "go", - "golang": "go", - "java": "maven", - "maven": "maven", - "node.js": "npm", - "npm": "npm", - "nuget": "nuget", - "php": "composer", - "pip": "pypi", - "pypi": "pypi", - "python": "pypi", - "rpm": "rpm", - "ruby": "rubygems", - "rubygems": "rubygems", - "rust": "cargo" -} - -# Mapping of standard ecosystem names to univers structured version functions -SYS_VERSION_FUNC_MAPPING = { - "pypi": versions.PypiVersion, - "npm": versions.SemverVersion, - "rubygems": versions.RubygemsVersion, - "deb": versions.DebianVersion, - "maven": versions.MavenVersion, - "nuget": versions.NugetVersion, - "rpm": versions.RpmVersion, - "composer": versions.ComposerVersion, - "go": versions.GolangVersion, - "conan": versions.ConanVersion, -} - -# Standard ecosystem names supported in deps.dev API https://docs.deps.dev/api/v3/#getpackage -DEPDEV_SUPPORTED_SYS = set(["go", "npm", "cargo", "maven", "pypi", "nuget"]) - - -def ubuntu_check(version: str = None): - version = str(version) - if version is None: - return False - if "ubuntu" in version: - return True - return False - - -def deb_check(version: str = None): - version = str(version) - if version is None: - return False - if "deb" in version: - return True - return False - - -def rhel_check(version: str = None): - version = str(version) - if version is None: - return False - if "el" in version: - return True - return False - - -def _is_valid_comparison(v, u): - return ubuntu_check(v) == ubuntu_check(u) and deb_check(u) == deb_check(v) and rhel_check(u) == rhel_check(v) - - -class VulnerableDependencyChecker(IntelClient): - """ - This tool will scan through the input SBOM file and identify all vulnerable packages and dependencies - indicated by the provided CVEs. - """ - - def __init__(self, - *, - base_url: str | None = None, - image: str, - sbom_list: list, - session: aiohttp.ClientSession | None = None, - respect_retry_after_header: bool = True, - retry_on_client_errors: bool = True): - - super().__init__( - session=session, - base_url=base_url or os.environ.get('DEPSDEV_BASE_URL'), - retry_count=1, # Service returns 404 if the package is not found. So dont retry - respect_retry_after_header=respect_retry_after_header, - retry_on_client_errors=retry_on_client_errors) - - self._semaphore = asyncio.Semaphore(25) - - self.image = image - self.ref_packages = sbom_list - self.found_count = 0 - - @classmethod - def default_base_url(cls) -> str: - return "https://api.deps.dev/" - - @staticmethod - def _check_version_in_range(version_to_check: str, version_range: list[str], system: str) -> bool: - """Determine if a given version is within a specified range. - - Args: - version_to_check (str): The version of the target package to check, either str or None - version_range (list[str]): A set containing four elements: the start/end of the version range - (inclusive/exclusive). - system (str): The ecosystem of the package, either str or None - - Returns: - bool: True if the version is within the specified range, False otherwise. - """ - - if not version_to_check: - raise ValueError("Invalid version to check. version_to_check is empty.") - elif all([not v for v in version_range]): - raise ValueError("Invalid version range. version_range is empty.") - - all_versions = version_range + [version_to_check] - if max([ubuntu_check(v) or deb_check(v) for v in all_versions]): - system = 'deb' - if max([rhel_check(v) for v in all_versions]): - system = 'rpm' - - # Get structured version conversion function based on the package system. Use GenericVersion as default. - version_func = SYS_VERSION_FUNC_MAPPING.get(system, versions.GenericVersion) - - # Convert version strings to structured version objects - try: - version_to_check = version_func(version_to_check) - ver_start_excl, ver_end_excl, ver_start_incl, ver_end_incl = [ - version_func(v) if v is not None else v for v in version_range - ] - except versions.InvalidVersion as e: - raise e - - # Check the version range start - if ver_start_incl and _is_valid_comparison(version_to_check, ver_start_incl): - matches_start = ver_start_incl <= version_to_check - elif ver_start_excl and _is_valid_comparison(version_to_check, ver_start_excl): - matches_start = ver_start_excl < version_to_check - else: - matches_start = True - - # Check the version range end - if ver_end_incl and _is_valid_comparison(version_to_check, ver_end_incl): - matches_end = version_to_check <= ver_end_incl - elif ver_end_excl and _is_valid_comparison(version_to_check, ver_end_excl): - matches_end = version_to_check < ver_end_excl - else: - matches_end = True - - return matches_start and matches_end - - @staticmethod - def _check_package_vulnerability(package: DependencyPackage, system: str, vuln: CveIntelNvd.Configuration) -> bool: - """Determine if a package is vulnerable for a configuration based on its name and version. - - Args: - package (DependencyPackage): package to check against the vulnerable configuration. - system (str): standardized system name. - vuln (CveIntelNvd.Configuration): vulnerable configuration information. - - Returns: - bool: True if the package matches the vulnerable configuration or version check fails, False otherwise. - """ - - # First, ensure package names match - if not package_names_match(package.name, vuln.package): - return False - - if 'ubuntu' in package.version: - system = 'deb' - - # If package names match, check the version - try: - return VulnerableDependencyChecker._check_version_in_range(package.version, - [ - vuln.versionStartExcluding, - vuln.versionEndExcluding, - vuln.versionStartIncluding, - vuln.versionEndIncluding, - ], - system) - except Exception as e: - logger.warning("Version comparison failed for package %s, assuming package may be vulnerable. Error: %s. ", - package.name, - e) - return True - - async def _is_maven_package(self, package_name: str, base_url): - """Check if a package exists in Maven. - - Args: - package_name (str): Package name in the format "group_id:artifact_id" - base_url (str): Base URL for the Maven repository - - Returns: - bool: True if the package exists, False otherwise - """ - parts = package_name.split(":") - if len(parts) != 2: - return False - group_id, artifact_id = parts - - query = f"g:{group_id}+AND+a:{artifact_id}" - payload = {"q": query, "core": "gav", "rows": "20", "wt": "json"} - - payload_str = urllib.parse.urlencode(payload, safe=":+") - - try: - response = await self.request(method="GET", url=base_url, params=payload) - return response["response"]["numFound"] > 0 - except requests.exceptions.RequestException: - return False - - async def _infer_pkg_ecosystem(self, package_name: str, version: str = None) -> str: - """Attempt to infer the ecosystem given the package name and version (this might not be 100% accurate) - - Args: - package_name (str): Package name - - Returns: - str: Ecosystem name (e.g. pip, npm, maven, etc.) or "unknown" if not found - """ - # Try to first infer based on version name - if ubuntu_check(version) or deb_check(version): - return "deb" - elif rhel_check(version): - return "rpm" - - # Define the mapping of ecosystems to their registries - registry_mapping = { - "pip": "https://pypi.org/project/", - "npm": "https://registry.npmjs.org/", - "go": "https://pkg.go.dev/", - "maven": "https://search.maven.org/solrsearch/select", - "nuget": "https://www.nuget.org/packages/", - "rubygems": "https://rubygems.org/gems/", - "rust": "https://crates.io/api/v1/crates/", - "erlang": "https://hex.pm/api/packages/", - "composer": "https://packagist.org/packages/", - "conan": "https://cpp.libhunt.com/", - } - - # Check each registry to see if the package exists - for ecosystem, base_url in registry_mapping.items(): - if ecosystem == "maven": - if await self._is_maven_package(package_name, base_url): - return ecosystem - else: - url = f"{base_url}{package_name}" - if ecosystem == "php": - url = f"{base_url}{package_name}.json" - elif ecosystem == "erlang": - url = f"{base_url}{package_name.lower()}" - - try: - response = await self._session.get(url, timeout=REQUESTS_TIMEOUT) - if response.status == 200: - if ecosystem == "composer": - content = await response.text(encoding="utf-8") - if ("vendor_not_found" in content or "package_not_found" in content): - continue - return ecosystem - except ( - aiohttp.http_exceptions.HttpProcessingError, - aiohttp.client_exceptions.ClientError, - asyncio.TimeoutError, - ): - continue - - return "unknown" - - async def _get_dependency(self, system=None, package: str = "", version: str = ""): - """Get dependencies for a package from deps.dev API - - Args: - system (str, optional): Ecosystem of the package. Defaults to None. - package (str, optional): Package name. Defaults to "". - version (str, optional): Package version. Defaults to "". - - Returns: - list: List of dependencies for the package. - """ - # Attempt to infer missing package ecosystem if not provided in the SBOM - if system is None: - system = await self._infer_pkg_ecosystem(package, version) - - self_package = [DependencyPackage( - system=system, - name=package, - version=version, - relation="SELF", - )] - - system = SYS_STANDARD_MAPPING.get(system) - - if system not in DEPDEV_SUPPORTED_SYS: - return self_package - - encoded_pkg = urllib.parse.quote(package, 'utf-8') - encoded_ver = urllib.parse.quote(version, 'utf-8') - api_url = (f"v3/systems/{system}/packages/{encoded_pkg}/versions/{encoded_ver}:dependencies") - url = url_join(self.base_url, api_url) - package_info = {"package": package, "system": system, "version": version} - - try: - - # Limit the number of concurrent requests to avoid overloading the server - async with self._semaphore: - - data = await self.request(method="GET", url=url, log_on_error=False) - if not data: - raise ValueError(f"Response from {self.base_url} contains no dependencies for {package_info}") - - self.found_count += 1 - return [ - DependencyPackage( - system=node["versionKey"]["system"], - name=node["versionKey"]["name"], - version=node["versionKey"]["version"], - relation=node["relation"], - ) for node in data["nodes"] - ] - - # Handle packages with no dependencies found - except (ValueError, aiohttp.ClientResponseError) as e: - - # Only log warning for unexpected errors - if (isinstance(e, ValueError) or (isinstance(e, aiohttp.ClientResponseError) and e.status == 404)): - pass - else: - logger.warning("Error calling %s for %s: %s", self.base_url, package_info, e) - - return self_package - - async def _collect_all_dependencies(self): - """Collect all direct and indirect dependencies given the sbom package info - - Returns: - dict: A dictionary with the following structure: { - (sbom_pkg, pkg_version): [ - DependencyPackage( - system="unknown", - name=pkg_name, - version=version, - relation="SELF", - ), - ... - ] - } - """ - logger.info("Collecting SBOM package dependencies from %s for %s", self.base_url, self.image) - dependencies_info = {} - - with tqdm(total=len(self.ref_packages)) as pbar: - - async def _wrap_coro(pkg_info): - pkg_name, version, system = ( - pkg_info.name.lower(), - pkg_info.version, - pkg_info.system, - ) - - dep_out = await self._get_dependency(system, pkg_name, version) - - pbar.update(1) - - return pkg_name, version, dep_out - - dependency_coros = [_wrap_coro(pkg_info) for pkg_info in self.ref_packages] - - gathered_dep_info = await asyncio.gather(*dependency_coros) - - for pkg_name, version, dependencies in gathered_dep_info: - dependencies_info[(pkg_name, version)] = dependencies - - logger.info("Collected additional dependency info for %d of %d SBOM packages.", - self.found_count, - len(self.ref_packages)) - - return dependencies_info - - async def load_dependencies(self): - """ - Load all dependencies for the SBOM packages - """ - - self.dependencies = await self._collect_all_dependencies() - - async def _get_vuln_packages(self, vuln, dependencies_info): - """Retrieve packages that matches with CVE vulnerability, specific for ghsa, nvd inputs - - Args: - vuln (Configuration): see return format from _reconstruct_ghsa - dependencies_info (dict): see return format from _collect_all_dependencies - - Returns: - list: A list of dictionary with a structure as follows: - key (tuple): (pkg_name, version) => the affected package in SBOM - value (DependencyPackage): the matched package informaiton related to the vulnerability indicated by CVE - """ - if not vuln.package: - return [] - - system = vuln.system.lower() if vuln.system else await self._infer_pkg_ecosystem(vuln.package.lower()) - system = SYS_STANDARD_MAPPING.get(system, system) - - vulnerable_dep = [{ - root_pkg: dependency - } for root_pkg, dependencies in dependencies_info.items() for dependency in dependencies - if VulnerableDependencyChecker._check_package_vulnerability(dependency, system, vuln)] - - return vulnerable_dep - - def _get_vuln_ubuntu_packages(self, notices, dependencies_info): - """Retrieve packages that match with CVE vulnerability, specific for ubuntu inputs - - Args: - notices (dict): example format: - { - "focal": [ - { - "description": "Simple Linux Utility for Resource Management", - "is_source": true, - "name": "slurm-llnl", - "version": "19.05.5-1ubuntu0.1~esm2" - }, ... - ], ... - } - dependencies_info (dict): see return format from _collect_all_dependencies - - Returns: - list: A list of dictionary with a structure as follows: - key: (pkg_name, version) => the affected package in SBOM - value: a list of matched vulnerable package information associated with CVE - """ - vulnerable_dep = [] - version_func = SYS_VERSION_FUNC_MAPPING["deb"] - - for patched_pkg_list in notices.values(): - for patched_pkg in patched_pkg_list: - name, version = patched_pkg.get("name"), patched_pkg.get("version") - - if not (name and version): - continue - - for root_pkg, dependencies in dependencies_info.items(): - for dependency in dependencies: - if package_names_match(dependency.name, name) and version_func( - dependency.version) < version_func(version): - vulnerable_dep.append({root_pkg: dependency}) - - return vulnerable_dep - - def _get_vuln_rhsa_packages(self, vuln_pkg_info, dependencies_info): - """Retrieve packages that match with CVE vulnerability, specific for ubuntu inputs - - Args: - vuln_package_info (list): example format: ["python39", "3.9-8100020240214182535.7044f6c1"] - dependencies_info (dict): see return format from _collect_all_dependencies - - Returns: - list: A list of dictionary with a structure as follows: - key: (pkg_name, version) => the affected package in SBOM - value: a list of matched vulnerable package information associated with CVE - """ - name, version = vuln_pkg_info - vulnerable_dep = [] - version_func = SYS_VERSION_FUNC_MAPPING["rpm"] - - if name and version: - for root_pkg, dependencies in dependencies_info.items(): - for dependency in dependencies: - if package_names_match(dependency.name, name) and version_func( - dependency.version) < version_func(version): - vulnerable_dep.append({root_pkg: dependency}) - - return vulnerable_dep - - @staticmethod - def _extract_range_structure(data, vuln_range): - """Turn the vulnerable_version_range string from ghsa to structure format - - Args: - data (Configuration): information before applying vuln_range - vuln_range (str): vulnerable_version_range string from ghsa. - - sample1: < 3.1.3 - - sample2: >= 0.14.0, < 14.0.1 - - Returns: - dict: structured output for updating data in function _reconstruct_ghsa - """ - vuln_list = vuln_range.split(",") - if len(vuln_list) > 1: - low, high = vuln_list[0].strip(), vuln_list[1].strip() - sign_low, low_v = low.split(" ") - sign_high, high_v = high.split(" ") - if sign_low == ">=": - data.versionStartIncluding = low_v - elif sign_low == ">": - data.versionStartExcluding = low_v - else: - high = vuln_list[0].strip() - sign_high, high_v = high.split(" ") - - if sign_high == "<=": - data.versionEndIncluding = high_v - elif sign_high == "<": - data.versionEndExcluding = high_v - - return data - - def _reconstruct_ghsa(self, vuln): - """Transform a ghsa vulnerability input to a defined structure - - Args: - vuln (dict): ghsa vulnerability input - - Returns: - Configuration: the defined versioning structure - """ - pkg_info = vuln.get("package", None) - if pkg_info: - data = CveIntelNvd.Configuration( - package=pkg_info.get("name", None), - system=pkg_info.get("ecosystem", None), - versionStartExcluding=None, - versionEndExcluding=vuln.get("first_patched_version", None), - versionStartIncluding=None, - versionEndIncluding=None, - ) - - vulnerable_version_range = vuln.get("vulnerable_version_range", None) - if vulnerable_version_range: - data = self._extract_range_structure(data, vulnerable_version_range) - - return data - - async def run_ghsa(self, vulnerabilities): - """Get vulnerable packages information from intel.ghsa.vulnerabilities - - Args: - vulnerabilities (list): list of vulnerabilities information from - intel.ghsa.vulnerabilities - sample input: [ - { - "package": { - "ecosystem": "pip", - "name": "mlflow" - }, - "vulnerable_version_range": "< 2.9.2", - "first_patched_version": "2.9.2", - "vulnerable_functions": [] - }, ... - ] - - list: A list of dictionary with a structure as follows: - - key (tuple): (pkg_name, version) => the affected package in SBOM - - value (DependencyPackage): the matched package informaiton related to the - vulnerability indicated by CVE - """ - vuln_pkgs = [] - for vuln in vulnerabilities: - vuln = self._reconstruct_ghsa(vuln) - vuln_pkg = await self._get_vuln_packages(vuln, self.dependencies) - if vuln_pkg: - vuln_pkgs.extend(vuln_pkg) - return vuln_pkgs - - async def run_nvd(self, vulnerabilities): - """Get vulnerable packages information from intel.nvd.configurations - - Args: - vulnerabilities (list): list of vulnerabilities information from - intel.nvd.configurations - - list: A list of dictionary with a structure as follows: - - key (tuple): (pkg_name, version) => the affected package in SBOM - - value (DependencyPackage): the matched package informaiton related to the - vulnerability indicated by CVE - """ - vuln_pkgs = [] - for vuln in vulnerabilities: - vuln_pkg = await self._get_vuln_packages(vuln, self.dependencies) - if vuln_pkg: - vuln_pkgs.extend(vuln_pkg) - return vuln_pkgs - - async def run_ubuntu(self, notices): - """Get vulnerable packages information from intel.ubuntu.notices - - Args: - notices (list): list of information from intel.ubuntu.notices - - Returns: - list: a list of package information from sbom that is related to the vulnerability - """ - vuln_pkgs = [] - for notice in notices: - ubuntu_patched_dict = notice.get("release_packages", {}) - if ubuntu_patched_dict: - vuln_pkg = self._get_vuln_ubuntu_packages(ubuntu_patched_dict, self.dependencies) - vuln_pkgs.extend(vuln_pkg) - - return vuln_pkgs - - async def run_rhsa(self, vulnerabilities): - """Get vulnerable packages information from intel.rhsa.affected_release - - Args: - vulnerabilities (list): list of vulnerabilities information from - intel.rhsa.affected_release. - example format: [{ - "product_name": "Red Hat Enterprise Linux 8", - "release_date": "2024-05-22T00:00:00Z", - "advisory": "RHSA-2024:2985", - "cpe": "cpe:/a:redhat:enterprise_linux:8", - "package": "python39:3.9-8100020240214182535.7044f6c1" - }, ...] - - Returns: - list: a list of package information from sbom that is related to the vulnerability - """ - vuln_pkgs = [] - for vuln in vulnerabilities: - cve_vuln_pkg = vuln.get("package", "").split(":") - if len(cve_vuln_pkg) == 2: - vuln_pkg = self._get_vuln_rhsa_packages(cve_vuln_pkg, self.dependencies) - vuln_pkgs.extend(vuln_pkg) - - return vuln_pkgs diff --git a/src/vuln_analysis/utils/web_researcher.py b/src/vuln_analysis/utils/web_researcher.py new file mode 100644 index 000000000..9c60052ee --- /dev/null +++ b/src/vuln_analysis/utils/web_researcher.py @@ -0,0 +1,62 @@ +"""Phase 1 — Web Research Sub-agent for the Container Analyzer. + +A lightweight, isolated LLM agent that gathers classification evidence +for public container images. It has no filesystem access — it only +receives the image name string and uses web_search + web_fetch. +""" + +from __future__ import annotations + +import logging + +from aiolimiter import AsyncLimiter +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.tools import BaseTool + +from vuln_analysis.prompts.container_analyzer import ( + PHASE1_SYSTEM_PROMPT, + build_web_research_message, +) +from vuln_analysis.utils.agentic_loop import run_agentic_loop + +logger = logging.getLogger(__name__) + + +async def run_web_research( + llm: BaseChatModel, + tools: list[BaseTool], + image_name: str, + *, + max_iterations: int = 8, + rate_limiter: AsyncLimiter | None = None, +) -> str | None: + """Research *image_name* via web search and return a text summary. + + Returns None if the research fails entirely. + """ + logger.info("Phase 1: starting web research for %s", image_name) + + messages = [ + SystemMessage(content=PHASE1_SYSTEM_PROMPT), + HumanMessage(content=build_web_research_message(image_name)), + ] + + try: + content, tool_calls, llm_calls = await run_agentic_loop( + llm=llm, + messages=messages, + tools=tools, + max_iterations=max_iterations, + context_window_tokens=200_000, + phase_label="Phase 1", + rate_limiter=rate_limiter, + ) + if content: + logger.info("Phase 1 complete (%d tool calls). Summary length: %d chars", tool_calls, len(content)) + return content + logger.warning("Phase 1 returned no content") + return None + except Exception as exc: + logger.error("Phase 1 web research failed: %s — proceeding without web context", exc, exc_info=True) + return None diff --git a/tests/data_models/__init__.py b/tests/data_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data_models/test_input.py b/tests/data_models/test_input.py new file mode 100644 index 000000000..96f25f541 --- /dev/null +++ b/tests/data_models/test_input.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from pydantic import ValidationError + +from vuln_analysis.data_models.input import CVEEntry, PipelineInput + + +class TestPipelineInputValidator: + + def test_accepts_filesystem_path_only(self): + inp = PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2025-0001")], + image_name="example/img", + image_tag="1", + filesystem_path="/tmp/rootfs", + ) + assert inp.filesystem_path == "/tmp/rootfs" + assert inp.image_tag == "1" + assert inp.vulns[0].vuln_id == "CVE-2025-0001" + + def test_accepts_image_tag_with_image_name(self): + inp = PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2025-0001")], + image_name="example/img", + image_tag="1", + ) + assert inp.image_name == "example/img" + assert inp.image_tag == "1" + assert inp.filesystem_path == "" + + def test_filesystem_path_requires_image_name(self): + with pytest.raises(ValidationError, match="image_name"): + PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2025-0001")], + image_tag="1", + filesystem_path="/tmp/rootfs", + ) + + def test_image_tag_requires_image_name(self): + with pytest.raises(ValidationError, match="image_name"): + PipelineInput(vulns=[CVEEntry(vuln_id="CVE-2025-0001")], image_tag="1") + + def test_filesystem_path_requires_image_tag(self): + with pytest.raises(ValidationError, match="image_tag"): + PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2025-0001")], + image_name="example/img", + filesystem_path="/tmp/rootfs", + ) + + def test_image_name_must_not_include_tag_with_image_tag(self): + with pytest.raises(ValidationError, match="must not include a tag"): + PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2025-0001")], + image_name="example/img:latest", + image_tag="1", + ) + + def test_requires_vulns(self): + with pytest.raises(ValidationError, match="vulns"): + PipelineInput(image_name="example/img", image_tag="1") diff --git a/tests/data_models/test_multi_cve.py b/tests/data_models/test_multi_cve.py new file mode 100644 index 000000000..5ae0d7f4c --- /dev/null +++ b/tests/data_models/test_multi_cve.py @@ -0,0 +1,272 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for multi-CVE pipeline support — data models and input validation.""" + +import pytest +from pydantic import ValidationError + +from vuln_analysis.data_models.input import CVEEntry, PipelineInput +from vuln_analysis.data_models.output import CVEResult, PipelineResult, StageResult + + +# --------------------------------------------------------------------------- +# CVEEntry +# --------------------------------------------------------------------------- + +class TestCVEEntry: + @pytest.mark.parametrize("vuln_id, is_ghsa", [ + ("CVE-2024-1234", False), + ("CVE-2024-9999", False), + ("GHSA-xxxx-yyyy-zzzz", True), + ("GHSA-abcd-efgh-ijkl", True), + ]) + def test_auto_classify(self, vuln_id, is_ghsa): + e = CVEEntry(vuln_id=vuln_id) + assert e.is_ghsa == is_ghsa + + def test_with_hints(self): + e = CVEEntry(vuln_id="CVE-2024-1234", package_hint="openssl", version_hint="3.0.1") + assert e.package_hint == "openssl" + assert e.version_hint == "3.0.1" + + def test_missing_vuln_id_raises(self): + with pytest.raises(ValidationError): + CVEEntry() + + +# --------------------------------------------------------------------------- +# PipelineInput +# --------------------------------------------------------------------------- + +_BASE = {"image_name": "test", "image_tag": "latest", "filesystem_path": "/tmp"} + + +class TestPipelineInput: + def test_single_cve(self): + inp = PipelineInput(vulns=[CVEEntry(vuln_id="CVE-2024-1234")], **_BASE) + assert len(inp.vulns) == 1 + assert inp.vulns[0].vuln_id == "CVE-2024-1234" + + def test_single_ghsa(self): + inp = PipelineInput(vulns=[CVEEntry(vuln_id="GHSA-xxxx-yyyy-zzzz")], **_BASE) + assert inp.vulns[0].is_ghsa + + def test_multiple_vulns(self): + inp = PipelineInput( + vulns=[ + CVEEntry(vuln_id="CVE-2024-1111"), + CVEEntry(vuln_id="GHSA-aaaa-bbbb-cccc"), + CVEEntry(vuln_id="CVE-2025-2222", package_hint="numpy"), + ], + **_BASE, + ) + assert len(inp.vulns) == 3 + assert inp.vulns[1].is_ghsa + assert inp.vulns[2].package_hint == "numpy" + + def test_duplicate_vuln_id_raises(self): + with pytest.raises(ValidationError, match="Duplicate vuln_id"): + PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2024-1234"), CVEEntry(vuln_id="CVE-2024-1234")], + **_BASE, + ) + + def test_empty_vulns_raises(self): + with pytest.raises(ValidationError, match="vulns"): + PipelineInput(vulns=[], **_BASE) + + def test_missing_vulns_raises(self): + with pytest.raises(ValidationError, match="vulns"): + PipelineInput(**_BASE) + + def test_json_round_trip(self): + inp = PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2024-1111"), CVEEntry(vuln_id="GHSA-aaaa-bbbb-cccc")], + **_BASE, + ) + raw = inp.model_dump_json() + restored = PipelineInput.model_validate_json(raw) + assert len(restored.vulns) == 2 + assert restored.vulns[0].vuln_id == "CVE-2024-1111" + assert restored.vulns[1].vuln_id == "GHSA-aaaa-bbbb-cccc" + + def test_json_from_dict(self): + """Verify parsing from a raw JSON dict (as sent by API callers).""" + raw = { + "vulns": [{"vuln_id": "CVE-2024-1234"}, {"vuln_id": "GHSA-xxxx-yyyy-zzzz"}], + "image_name": "test", + "image_tag": "latest", + "filesystem_path": "/tmp", + } + inp = PipelineInput(**raw) + assert len(inp.vulns) == 2 + assert inp.vulns[0].vuln_id == "CVE-2024-1234" + assert inp.vulns[1].vuln_id == "GHSA-xxxx-yyyy-zzzz" + assert inp.vulns[1].is_ghsa + + def test_multi_cve_with_image_tag(self): + inp = PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2024-1111"), CVEEntry(vuln_id="CVE-2024-2222")], + image_name="example/img", + image_tag="1", + ) + assert len(inp.vulns) == 2 + assert inp.image_name == "example/img" + assert inp.image_tag == "1" + assert inp.filesystem_path == "" + + def test_accepts_image_tag_and_filesystem_path(self): + inp = PipelineInput( + vulns=[CVEEntry(vuln_id="CVE-2024-1234")], + image_name="test", + image_tag="latest", + filesystem_path="/tmp", + ) + assert inp.filesystem_path == "/tmp" + + +# --------------------------------------------------------------------------- +# CVEResult +# --------------------------------------------------------------------------- + +class TestCVEResult: + def test_basic(self): + r = CVEResult(vuln_id="CVE-2024-1234", success=True, vex_status="not_affected", + vex_justification="code_not_reachable") + assert r.vuln_id == "CVE-2024-1234" + assert r.cve_id == "CVE-2024-1234" + assert r.ghsa_id is None + assert r.success + assert r.total_tool_calls == 0 + + def test_ghsa_auto_classify(self): + r = CVEResult(vuln_id="GHSA-xxxx-yyyy-zzzz", success=True) + assert r.vuln_id == "GHSA-xxxx-yyyy-zzzz" + assert r.cve_id is None + assert r.ghsa_id == "GHSA-xxxx-yyyy-zzzz" + + def test_preserves_lookup_aliases(self): + cve = CVEResult( + vuln_id="CVE-2024-1234", + ghsa_id="GHSA-5vqr-wprc-cpp7", + success=True, + ) + assert cve.cve_id == "CVE-2024-1234" + assert cve.ghsa_id == "GHSA-5vqr-wprc-cpp7" + + ghsa = CVEResult( + vuln_id="GHSA-5vqr-wprc-cpp7", + cve_id="CVE-2024-1234", + success=True, + ) + assert ghsa.cve_id == "CVE-2024-1234" + assert ghsa.ghsa_id == "GHSA-5vqr-wprc-cpp7" + + def test_tool_call_aggregation(self): + r = CVEResult( + vuln_id="CVE-2024-1234", + success=True, + cve_researcher=StageResult(agent_name="CVE Researcher", success=True, tool_call_count=5, llm_call_count=2), + exploitability_analyzer=StageResult(agent_name="Exploitability", success=True, tool_call_count=10, + llm_call_count=3), + vex_categorizer=StageResult(agent_name="VEX", success=True, tool_call_count=0, llm_call_count=1), + ) + assert r.total_tool_calls == 15 + assert r.total_llm_calls == 6 + + +# --------------------------------------------------------------------------- +# PipelineResult — single CVE (results populated) +# --------------------------------------------------------------------------- + +class TestPipelineResultSingleCVE: + def test_single_cve_uses_results_field(self): + r = PipelineResult( + image_name="test", + image_tag="latest", + success=True, + container_analyzer=StageResult(agent_name="Container Analyzer", success=True, tool_call_count=7), + results=[ + CVEResult( + vuln_id="CVE-2024-1234", + success=True, + cve_researcher=StageResult(agent_name="CVE Researcher", success=True, tool_call_count=3), + exploitability_analyzer=StageResult( + agent_name="Exploitability", + success=True, + tool_call_count=5, + ), + vex_categorizer=StageResult(agent_name="VEX", success=True, tool_call_count=1), + vex_status="not_affected", + vex_justification="code_not_reachable", + ) + ], + ) + assert len(r.results) == 1 + assert r.results[0].vex_status == "not_affected" + assert r.total_tool_calls == 16 + assert r.total_llm_calls == 0 + + +# --------------------------------------------------------------------------- +# PipelineResult — multi-CVE (results populated) +# --------------------------------------------------------------------------- + +class TestPipelineResultMultiCVE: + def test_multi_cve_results(self): + cve1 = CVEResult( + vuln_id="CVE-2024-1111", success=True, + cve_researcher=StageResult(agent_name="CVE Researcher", success=True, tool_call_count=3, llm_call_count=1), + vex_status="not_affected", + ) + cve2 = CVEResult( + vuln_id="CVE-2024-2222", success=True, + cve_researcher=StageResult(agent_name="CVE Researcher", success=True, tool_call_count=4, llm_call_count=2), + vex_status="affected", + ) + r = PipelineResult( + image_name="test", + image_tag="latest", + success=True, + container_analyzer=StageResult(agent_name="Container Analyzer", success=True, + tool_call_count=10, llm_call_count=5), + results=[cve1, cve2], + ) + assert r.total_tool_calls == 10 + 3 + 4 + assert r.total_llm_calls == 5 + 1 + 2 + + def test_json_round_trip(self): + r = PipelineResult( + image_name="test", + image_tag="latest", + success=True, + results=[ + CVEResult(vuln_id="CVE-2024-1111", success=True, vex_status="not_affected"), + CVEResult(vuln_id="GHSA-aaaa-bbbb-cccc", success=False, error="timeout"), + ], + ) + raw = r.model_dump_json() + restored = PipelineResult.model_validate_json(raw) + assert restored.image_name == "test" + assert restored.image_tag == "latest" + assert len(restored.results) == 2 + assert restored.results[0].vuln_id == "CVE-2024-1111" + assert restored.results[0].cve_id == "CVE-2024-1111" + assert restored.results[0].vex_status == "not_affected" + assert restored.results[1].vuln_id == "GHSA-aaaa-bbbb-cccc" + assert restored.results[1].cve_id is None + assert restored.results[1].ghsa_id == "GHSA-aaaa-bbbb-cccc" + assert restored.results[1].error == "timeout" diff --git a/tests/eval/test_parse_eval_input.py b/tests/eval/test_parse_eval_input.py new file mode 100644 index 000000000..75fa8b746 --- /dev/null +++ b/tests/eval/test_parse_eval_input.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from vuln_analysis.eval.parse_eval_input import parse_input + + +def test_parse_input_uses_explicit_image_name_and_tag(tmp_path): + csv_path = tmp_path / "benchmark.csv" + csv_path.write_text( + "\n".join( + [ + "vuln_id,image_name,image_tag,filesystem_path,image_config_path,ground_truth_label", + "CVE-2025-0001,nvcr.io/nvidia/morpheus/morpheus,25.06-runtime,/tmp/rootfs,,true", + ] + ), + encoding="utf-8", + ) + + item = parse_input(csv_path).eval_input_items[0] + pipeline_input = json.loads(item.input_obj) + + assert pipeline_input["image_name"] == "nvcr.io/nvidia/morpheus/morpheus" + assert pipeline_input["image_tag"] == "25.06-runtime" + assert item.full_dataset_entry["pull_ref"] == ( + "nvcr.io/nvidia/morpheus/morpheus:25.06-runtime" + ) + + +def test_parse_input_allows_missing_optional_path_columns(tmp_path): + csv_path = tmp_path / "benchmark.csv" + csv_path.write_text( + "\n".join( + [ + "vuln_id,image_name,image_tag,ground_truth_label", + "GHSA-xxxx-yyyy-zzzz,example/image,latest,code_not_present", + ] + ), + encoding="utf-8", + ) + + item = parse_input(csv_path).eval_input_items[0] + pipeline_input = json.loads(item.input_obj) + + assert pipeline_input["filesystem_path"] == "" + assert pipeline_input["image_config_path"] is None + assert item.full_dataset_entry["filesystem_path"] == "" + assert item.full_dataset_entry["image_config_path"] == "" diff --git a/tests/plugins/test_cve_researcher_plugin.py b/tests/plugins/test_cve_researcher_plugin.py new file mode 100644 index 000000000..3c6de832b --- /dev/null +++ b/tests/plugins/test_cve_researcher_plugin.py @@ -0,0 +1,208 @@ +"""Tests for the local CVE researcher NAT plugin.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from langchain_core.messages import AIMessage +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool + +from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepResearchSandboxConfig +from aiq_agent.agents.deep_researcher.deepagents_runtime import DeepResearchSkillsConfig +from aiq_agent.common import LLMProvider +from aiq_agent.common import LLMRole +from vuln_analysis.functions.cve_researcher import CVEResearcherConfig +from vuln_analysis.plugins.cve_researcher.agent import CVEResearcherAgent +from vuln_analysis.plugins.cve_researcher.agent import _build_research_subagents +from vuln_analysis.plugins.cve_researcher.models import CVEResearcherAgentState +from vuln_analysis.plugins.cve_researcher.models import parse_research_prompt +from vuln_analysis.plugins.cve_researcher.register import CVEResearcherAgentConfig +from vuln_analysis.plugins.cve_researcher.register import cve_researcher +from vuln_analysis.plugins.cve_researcher.register import cve_researcher_agent + + +@tool +def web_search(query: str) -> str: + """Search the web for advisory information.""" + + return f"results for {query}" + + +@tool +def web_fetch(url: str) -> str: + """Fetch a web page.""" + + return f"content from {url}" + + +@tool +def osv_lookup(cve_id: str, version: str, package_name: str | None = None) -> str: + """Look up OSV affected status.""" + + del package_name + return f"{cve_id} {version} NOT AFFECTED" + + +def _llm_provider(llm): + provider = LLMProvider() + provider.set_default(llm) + provider.configure(LLMRole.ORCHESTRATOR, llm) + provider.configure(LLMRole.RESEARCHER, llm) + return provider + + +class FakeBuilder: + async def get_tools(self, tool_names, wrapper_type): + del tool_names, wrapper_type + return [web_search, web_fetch, osv_lookup] + + async def get_llm(self, llm_name, wrapper_type): + del llm_name, wrapper_type + return MagicMock() + + +async def _single_yield(async_generator): + async with async_generator as value: + return value + + +def test_cve_state_requires_identifier() -> None: + with pytest.raises(ValueError, match="At least one CVE or GHSA"): + CVEResearcherAgentState(messages=[HumanMessage(content="Research this vulnerability")]) + + assert CVEResearcherAgentState.model_validate("CVE-2025-12345").identifier == "CVE-2025-12345" + assert CVEResearcherAgentState.model_validate("GHSA-abcd-efgh-ijkl").identifier == "GHSA-abcd-efgh-ijkl" + + +def test_prompt_parser_handles_ghsa_and_filename_style_hints() -> None: + assert parse_research_prompt("GHSA-ABCD-EFGH-IJKL setuptools==75.8.0") == { + "ghsa_id": "GHSA-abcd-efgh-ijkl", + "package_hint": "setuptools", + "version_hint": "75.8.0", + } + assert parse_research_prompt("CVE-2025-9900_holoscan-ltsb2_23.10.08-lws2.3.1-dgpu.md") == { + "cve_id": "CVE-2025-9900", + "package_hint": "holoscan-ltsb2", + "version_hint": "23.10.08-lws2.3.1-dgpu", + } + + +def test_build_research_subagents_matches_expected_shape() -> None: + llm = MagicMock() + tools = [web_search, web_fetch, osv_lookup] + + subagents = _build_research_subagents(tools, llm) + + assert [subagent["name"] for subagent in subagents] == [ + "advisory-researcher", + "exploit-intel-researcher", + ] + assert subagents[0]["tools"] == tools + assert [tool.name for tool in subagents[1]["tools"]] == ["web_search", "web_fetch"] + assert subagents[0]["model"] is llm + assert subagents[1]["model"] is llm + + +@pytest.mark.asyncio +async def test_agent_run_invokes_local_deepagents_with_cve_subagents() -> None: + llm = MagicMock() + provider = _llm_provider(llm) + mock_agent = MagicMock() + mock_agent.with_config = MagicMock(return_value=mock_agent) + mock_agent.ainvoke = AsyncMock( + return_value={"messages": [AIMessage(content="# CVE Analysis: CVE-2025-12345\n\n## Overview")]} + ) + + with patch( + "vuln_analysis.plugins.cve_researcher.agent.create_deep_agent", + return_value=mock_agent, + ) as create: + agent = CVEResearcherAgent( + llm_provider=provider, + tools=[web_search, web_fetch, osv_lookup], + callbacks=[MagicMock()], + ) + result = await agent.run( + CVEResearcherAgentState( + messages=[HumanMessage(content="Research CVE-2025-12345 for example-package 1.2.3")], + cve_id="CVE-2025-12345", + package_hint="example-package", + version_hint="1.2.3", + ) + ) + + assert result.report_content.startswith("# CVE Analysis") + assert result.tool_call_count == 0 + assert result.llm_call_count == 1 + + kwargs = create.call_args.kwargs + assert kwargs["model"] is llm + assert kwargs["tools"] == [web_search, web_fetch, osv_lookup] + assert kwargs["name"] == "cve-researcher" + assert kwargs["context_schema"] is CVEResearcherAgentState + assert [subagent["name"] for subagent in kwargs["subagents"]] == [ + "advisory-researcher", + "exploit-intel-researcher", + ] + + mock_agent.with_config.assert_called_once_with({"recursion_limit": 60}) + payload = mock_agent.ainvoke.call_args.args[0] + assert isinstance(payload, CVEResearcherAgentState) + assert "CVE-2025-12345" in payload.messages[0].content + assert "example-package" in payload.messages[0].content + assert "1.2.3" in payload.messages[0].content + + +def test_local_plugin_config_types() -> None: + pipeline_config = CVEResearcherConfig( + llm_name="llm", + tool_names=["web_search", "web_fetch", "osv_lookup"], + ) + agent_config = CVEResearcherAgentConfig( + llm_name="llm", + tool_names=["web_search", "web_fetch", "osv_lookup"], + include_middleware=False, + skills=DeepResearchSkillsConfig(), + sandbox=DeepResearchSandboxConfig(app_name="custom-cve-researcher"), + ) + + assert pipeline_config.type == "cve_researcher" + assert agent_config.type == "cve_researcher_agent" + assert agent_config.include_middleware is False + assert agent_config.skills.agents == {} + assert agent_config.sandbox is not None + assert agent_config.sandbox.app_name == "custom-cve-researcher" + + +@pytest.mark.asyncio +async def test_pipeline_registration_builds_function_info_with_concrete_annotations() -> None: + function_info = await _single_yield( + cve_researcher( + CVEResearcherConfig( + llm_name="llm", + tool_names=["web_search", "web_fetch", "osv_lookup"], + ), + FakeBuilder(), + ) + ) + + assert function_info.input_schema.__name__ == "CVEResearcherInput" + assert function_info.single_output_schema.__name__ == "CVEResearcherResult" + + +@pytest.mark.asyncio +async def test_agent_registration_builds_function_info_with_concrete_annotations() -> None: + function_info = await _single_yield( + cve_researcher_agent( + CVEResearcherAgentConfig( + llm_name="llm", + tool_names=["web_search", "web_fetch", "osv_lookup"], + ), + FakeBuilder(), + ) + ) + + assert function_info.input_schema is CVEResearcherAgentState + assert function_info.single_output_schema is CVEResearcherAgentState diff --git a/tests/test_java_script_extended.py b/tests/test_java_script_extended.py deleted file mode 100644 index 7c7fd430f..000000000 --- a/tests/test_java_script_extended.py +++ /dev/null @@ -1,140 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from vuln_analysis.utils.js_extended_parser import ExtendedJavaScriptSegmenter - -TEST_CASES = [ - { - "name": - "regular_script", - "code": - """ -function hello() { - console.log('Hello'); -} - -class MyClass { - constructor() { - this.value = 42; - } -} -""", - "expected_functions": - 2, # One function and one class - "should_parse": - True, - }, - { - "name": - "es_module", - "code": - """ -import { something } from './module'; -export function exportedFunc() { - return 'exported'; -} -export class ExportedClass { - method() {} -} -""", - "expected_functions": - 2, # One exported function and one exported class - "should_parse": - True, - }, - { - "name": - "optional_chaining", - "code": - """ -function processUser(user) { - return user?.profile?.name; -} -class UserManager { - getAddress() { - return this.user?.address?.street; - } -} -""", - "expected_functions": - 2, - "should_parse": - True, - }, - { - "name": "shebang_file", - "code": """#!/usr/bin/env node -function main() { - console.log('Main'); -} -""", - "expected_functions": 0, # Should skip due to shebang - "should_parse": False, - } -] - - -@pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda x: x["name"]) -def test_function_extraction(test_case): - """Test that functions and classes are correctly extracted.""" - segmenter = ExtendedJavaScriptSegmenter(test_case["code"]) - functions = segmenter.extract_functions_classes() - assert len(functions) == test_case["expected_functions"] - if not test_case["should_parse"]: - assert segmenter.skip_file - - -@pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda x: x["name"]) -def test_code_simplification(test_case): - """Test that code is correctly simplified.""" - segmenter = ExtendedJavaScriptSegmenter(test_case["code"]) - simplified = segmenter.simplify_code() - - # Basic checks - assert isinstance(simplified, str) - if not test_case["should_parse"]: - assert segmenter.skip_file - assert simplified == test_case["code"] # Return original code for unparseable files - else: - # Should have fewer or equal lines than original - assert len(simplified.splitlines()) <= len(test_case["code"].splitlines()) - # Should contain "// Code for:" for each function/class - if test_case["expected_functions"] > 0: - assert "// Code for:" in simplified - - -def test_optional_chaining_replacement(): - """Test that optional chaining is correctly replaced.""" - code = "const name = user?.profile?.name;" - segmenter = ExtendedJavaScriptSegmenter(code) - assert not segmenter.skip_file - assert "?." not in segmenter.code - assert "." in segmenter.code - - -def test_invalid_js(): - """Test handling of invalid JavaScript code.""" - code = "this is not valid javascript" - segmenter = ExtendedJavaScriptSegmenter(code) - assert not segmenter.skip_file # Should attempt to parse - - # Should handle errors gracefully - functions = segmenter.extract_functions_classes() - assert functions == [] - - simplified = segmenter.simplify_code() - assert simplified == code # Return original code for invalid JS diff --git a/tests/test_pipeline_output_ids.py b/tests/test_pipeline_output_ids.py new file mode 100644 index 000000000..b214d3b9e --- /dev/null +++ b/tests/test_pipeline_output_ids.py @@ -0,0 +1,30 @@ +from vuln_analysis.data_models.output import StageResult +from vuln_analysis.register import _resolve_pipeline_result_ids + + +def test_pipeline_result_ids_from_research_report(tmp_path): + report = tmp_path / "GHSA-5vqr-wprc-cpp7.md" + report.write_text( + "\n".join([ + "| Field | Value |", + "|-------|-------|", + "| **CVE ID** | CVE-2024-1234 |", + "| **GHSA ID** | GHSA-5vqr-wprc-cpp7 |", + ]), + encoding="utf-8", + ) + + cve_id, ghsa_id = _resolve_pipeline_result_ids( + "GHSA-5vqr-wprc-cpp7", + StageResult(agent_name="CVE Researcher", success=True, report_path=str(report)), + ) + + assert cve_id == "CVE-2024-1234" + assert ghsa_id == "GHSA-5vqr-wprc-cpp7" + + +def test_pipeline_result_ids_without_report_use_only_input_identifier(): + cve_id, ghsa_id = _resolve_pipeline_result_ids("CVE-2024-1234") + + assert cve_id == "CVE-2024-1234" + assert ghsa_id is None diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/test_concurrency.py b/tests/utils/test_concurrency.py new file mode 100644 index 000000000..3d2c217f8 --- /dev/null +++ b/tests/utils/test_concurrency.py @@ -0,0 +1,283 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from pathlib import Path + +import pytest + +from vuln_analysis.utils.concurrency import ( + AsyncCoalescer, + atomic_write, + get_effective_rate_limiter, + make_lazy_rate_limiter, +) + + +# --------------------------------------------------------------------------- +# atomic_write tests +# --------------------------------------------------------------------------- + +class TestAtomicWrite: + + def test_writes_content(self, tmp_path): + p = tmp_path / "report.md" + atomic_write(p, "hello world") + assert p.read_text(encoding="utf-8") == "hello world" + + def test_creates_parent_dirs(self, tmp_path): + p = tmp_path / "a" / "b" / "report.md" + atomic_write(p, "nested") + assert p.read_text(encoding="utf-8") == "nested" + + def test_overwrites_existing_file(self, tmp_path): + p = tmp_path / "report.md" + p.write_text("old", encoding="utf-8") + atomic_write(p, "new") + assert p.read_text(encoding="utf-8") == "new" + + def test_no_temp_files_left_on_success(self, tmp_path): + p = tmp_path / "report.md" + atomic_write(p, "content") + tmp_files = list(tmp_path.glob("*.tmp")) + assert tmp_files == [] + + def test_no_partial_write_on_error(self, tmp_path): + """If the write raises, the target file should not exist (or remain unchanged).""" + p = tmp_path / "report.md" + p.write_text("original", encoding="utf-8") + + class WriteError(Exception): + pass + + # Monkey-patch to simulate a write failure after file creation + import os + real_replace = os.replace + + def failing_replace(src, dst): + raise WriteError("simulated failure") + + os.replace = failing_replace + try: + with pytest.raises(WriteError): + atomic_write(p, "should not appear") + finally: + os.replace = real_replace + + # Original content should be preserved + assert p.read_text(encoding="utf-8") == "original" + # No temp files left behind + assert list(tmp_path.glob("*.tmp")) == [] + + +# --------------------------------------------------------------------------- +# AsyncCoalescer tests +# --------------------------------------------------------------------------- + +class TestAsyncCoalescer: + + @pytest.mark.asyncio + async def test_single_call_executes(self): + coalescer = AsyncCoalescer() + call_count = 0 + + async def work(): + nonlocal call_count + call_count += 1 + return "result" + + result = await coalescer.run_once("key1", work) + assert result == "result" + assert call_count == 1 + + @pytest.mark.asyncio + async def test_concurrent_calls_coalesce(self): + """Two concurrent calls with the same key should only execute once.""" + coalescer = AsyncCoalescer() + call_count = 0 + gate = asyncio.Event() + + async def slow_work(): + nonlocal call_count + call_count += 1 + gate.set() # signal that work started + await asyncio.sleep(0.1) + return "result" + + # Launch two concurrent calls with the same key + task1 = asyncio.create_task(coalescer.run_once("same_key", slow_work)) + await gate.wait() # ensure the first call has started + task2 = asyncio.create_task(coalescer.run_once("same_key", slow_work)) + + results = await asyncio.gather(task1, task2) + assert results == ["result", "result"] + assert call_count == 1 # only one execution + + @pytest.mark.asyncio + async def test_different_keys_run_independently(self): + coalescer = AsyncCoalescer() + calls = [] + + async def work(name): + calls.append(name) + return name + + r1, r2 = await asyncio.gather( + coalescer.run_once("key_a", lambda: work("a")), + coalescer.run_once("key_b", lambda: work("b")), + ) + assert r1 == "a" + assert r2 == "b" + assert sorted(calls) == ["a", "b"] + + @pytest.mark.asyncio + async def test_exception_propagates_to_all_waiters(self): + coalescer = AsyncCoalescer() + gate = asyncio.Event() + + async def failing_work(): + gate.set() + await asyncio.sleep(0.05) + raise ValueError("boom") + + task1 = asyncio.create_task(coalescer.run_once("err_key", failing_work)) + await gate.wait() + task2 = asyncio.create_task(coalescer.run_once("err_key", failing_work)) + + with pytest.raises(ValueError, match="boom"): + await task1 + with pytest.raises(ValueError, match="boom"): + await task2 + + @pytest.mark.asyncio + async def test_cancellation_propagates_to_waiters(self): + coalescer = AsyncCoalescer() + gate = asyncio.Event() + + async def cancellable_work(): + gate.set() + await asyncio.sleep(10) # will be cancelled + + task1 = asyncio.create_task(coalescer.run_once("cancel_key", cancellable_work)) + await gate.wait() + task2 = asyncio.create_task(coalescer.run_once("cancel_key", cancellable_work)) + await asyncio.sleep(0) # let task2 register as waiter + + task1.cancel() + with pytest.raises(asyncio.CancelledError): + await task1 + with pytest.raises((asyncio.CancelledError, BaseException)): + await task2 + + @pytest.mark.asyncio + async def test_key_available_after_completion(self): + """After the first call completes, a new call with the same key should execute.""" + coalescer = AsyncCoalescer() + call_count = 0 + + async def work(): + nonlocal call_count + call_count += 1 + return call_count + + r1 = await coalescer.run_once("reuse_key", work) + r2 = await coalescer.run_once("reuse_key", work) + assert r1 == 1 + assert r2 == 2 + assert call_count == 2 + + +# --------------------------------------------------------------------------- +# Rate limiter tests +# --------------------------------------------------------------------------- + +class _FakeWorkflowConfig: + def __init__(self, llm_max_rate): + self.llm_max_rate = llm_max_rate + + +class _FakeBuilder: + """Mimics WorkflowBuilder: the workflow config is only available after + set_workflow() runs, which (in populate_builder) happens *after* all + functions have been built.""" + + def __init__(self): + self._workflow = None + + def set_workflow(self, llm_max_rate): + self._workflow = _FakeWorkflowConfig(llm_max_rate) + + def get_workflow_config(self): + if self._workflow is None: + raise ValueError("No workflow set") + return self._workflow + + +class TestGetEffectiveRateLimiter: + + def test_local_override_wins(self): + builder = _FakeBuilder() + builder.set_workflow(5) + limiter = get_effective_rate_limiter(10, builder) + # No-burst conversion: rate 10 -> one request every 0.1s + assert limiter.max_rate == 1 + assert limiter.time_period == pytest.approx(0.1) + + def test_inherits_workflow_when_local_none(self): + builder = _FakeBuilder() + builder.set_workflow(5) + limiter = get_effective_rate_limiter(None, builder) + assert limiter.max_rate == 1 + assert limiter.time_period == pytest.approx(0.2) + + def test_no_workflow_set_returns_none(self): + # This is the build-time scenario: workflow not yet set. + builder = _FakeBuilder() + assert get_effective_rate_limiter(None, builder) is None + + def test_workflow_none_returns_none(self): + builder = _FakeBuilder() + builder.set_workflow(None) + assert get_effective_rate_limiter(None, builder) is None + + +class TestMakeLazyRateLimiter: + + def test_resolves_at_call_time_after_workflow_set(self): + # Reproduces the real build order: factory created while workflow is + # unset, then resolved on first invocation after set_workflow. + builder = _FakeBuilder() + get_rl = make_lazy_rate_limiter(None, builder) # built before workflow exists + builder.set_workflow(5) + limiter = get_rl() + assert limiter is not None + assert limiter.time_period == pytest.approx(0.2) + + def test_caches_single_shared_instance(self): + builder = _FakeBuilder() + builder.set_workflow(5) + get_rl = make_lazy_rate_limiter(None, builder) + assert get_rl() is get_rl() + + def test_caches_none_result(self): + # When no rate limit applies, None is cached (not re-resolved each call). + builder = _FakeBuilder() + builder.set_workflow(None) + get_rl = make_lazy_rate_limiter(None, builder) + first = get_rl() + assert first is None + # Even if the workflow later changed, the cached None is returned. + builder.set_workflow(5) + assert get_rl() is None diff --git a/tests/utils/test_image_extraction.py b/tests/utils/test_image_extraction.py new file mode 100644 index 000000000..e30b0e0f6 --- /dev/null +++ b/tests/utils/test_image_extraction.py @@ -0,0 +1,469 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import logging +import threading +import time +from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest + +from vuln_analysis.utils import image_extraction +from vuln_analysis.utils.image_extraction import CraneNotInstalledError +from vuln_analysis.utils.image_extraction import ImageAuthError +from vuln_analysis.utils.image_extraction import ImageExtractionError +from vuln_analysis.utils.image_extraction import ImageMaterializationError +from vuln_analysis.utils.image_extraction import ImageNotFoundError +from vuln_analysis.utils.image_extraction import MaterializedImage +from vuln_analysis.utils.image_extraction import _ProgressReporter +from vuln_analysis.utils.image_extraction import _dir_size +from vuln_analysis.utils.image_extraction import _human_bytes +from vuln_analysis.utils.image_extraction import materialize_image +from vuln_analysis.utils.image_extraction import materialize_image_bounded + +DIGEST = "sha256:abc123def456" +PULL_REF = "nvcr.io/nvidia/example:1.0" + + +def _mk_run_result(returncode: int = 0, stdout: bytes | str = b"", stderr: bytes | str = b""): + res = MagicMock() + res.returncode = returncode + res.stdout = stdout + res.stderr = stderr + return res + + +def _digest_run_ok(): + return _mk_run_result(returncode=0, stdout=f"{DIGEST}\n", stderr="") + + +def _config_run_ok(): + return _mk_run_result(returncode=0, stdout=b'{"config":{}}', stderr=b"") + + +def _patch_extraction_popen(rc_crane: int = 0, rc_tar: int = 0, + stderr_crane: bytes = b"", stderr_tar: bytes = b""): + """Return a MagicMock that simulates the crane | tar pipeline.""" + + def factory(*args, **kwargs): # noqa: ARG001 + proc = MagicMock() + cmd = args[0] if args else kwargs.get("args", []) + if cmd and (cmd[0].endswith("tar") or (len(cmd) > 1 and "tar" in cmd[0])): + proc.returncode = rc_tar + proc.communicate.return_value = (b"", stderr_tar) + else: + proc.returncode = rc_crane + proc.communicate.return_value = (b"", stderr_crane) + proc.stdout = MagicMock() + return proc + + return factory + + +# --------------------------------------------------------------------------- +# Digest resolution +# --------------------------------------------------------------------------- + +class TestResolveDigest: + + def test_returns_digest_on_success(self, tmp_path): + with patch("subprocess.run", side_effect=[_digest_run_ok(), _config_run_ok()]), \ + patch("subprocess.Popen", side_effect=_patch_extraction_popen()): + mat = materialize_image(PULL_REF, tmp_path) + assert mat.digest == DIGEST + + def test_raises_when_crane_missing(self, tmp_path): + with patch("subprocess.run", side_effect=FileNotFoundError("crane")): + with pytest.raises(CraneNotInstalledError): + materialize_image(PULL_REF, tmp_path) + + def test_unexpected_digest_format_raises(self, tmp_path): + bad = _mk_run_result(returncode=0, stdout="not-a-digest\n") + with patch("subprocess.run", return_value=bad): + with pytest.raises(ImageExtractionError): + materialize_image(PULL_REF, tmp_path) + + +class TestArgumentInjectionGuard: + """`--` must sit between the crane subcommand and the pull_ref so a value + starting with `-` cannot smuggle a crane flag.""" + + def test_separator_present_in_all_crane_invocations(self, tmp_path): + run_calls = [] + popen_calls = [] + + def run_capture(cmd, *args, **kwargs): # noqa: ARG001 + run_calls.append(cmd) + if cmd[1] == "digest": + return _digest_run_ok() + return _config_run_ok() + + def popen_capture(cmd, *args, **kwargs): + popen_calls.append(cmd) + return _patch_extraction_popen()(cmd, *args, **kwargs) + + with patch("subprocess.run", side_effect=run_capture), \ + patch("subprocess.Popen", side_effect=popen_capture): + materialize_image(PULL_REF, tmp_path) + + crane_calls = [c for c in run_calls + popen_calls if c and "tar" not in c[0]] + assert crane_calls, "no crane invocations were captured" + for cmd in crane_calls: + # Layout: [crane_bin, , "--", pull_ref, ...] + assert "--" in cmd, f"crane invocation missing `--` separator: {cmd}" + assert cmd.index("--") < cmd.index(PULL_REF), \ + f"`--` must precede pull_ref in {cmd}" + + def test_dash_leading_pull_ref_is_not_parsed_as_flag(self, tmp_path): + """Simulate a hostile pull_ref; verify it lands after `--`, not as a flag.""" + hostile = "--insecure-flag" + seen = [] + + def run_capture(cmd, *args, **kwargs): + seen.append(cmd) + # Return a not-found error so we don't try to extract. + return _mk_run_result(returncode=1, stderr="manifest unknown") + + with patch("subprocess.run", side_effect=run_capture): + with pytest.raises(ImageNotFoundError): + materialize_image(hostile, tmp_path) + + digest_cmd = seen[0] + assert digest_cmd[-2] == "--" + assert digest_cmd[-1] == hostile + + +# --------------------------------------------------------------------------- +# Error mapping +# --------------------------------------------------------------------------- + +class TestErrorMapping: + + @pytest.mark.parametrize("stderr", ["UNAUTHORIZED: token required", "authentication required"]) + def test_auth_errors(self, tmp_path, stderr): + bad = _mk_run_result(returncode=1, stderr=stderr) + with patch("subprocess.run", return_value=bad): + with pytest.raises(ImageAuthError): + materialize_image(PULL_REF, tmp_path) + + @pytest.mark.parametrize("stderr", [ + "MANIFEST_UNKNOWN: manifest unknown", + "NAME_UNKNOWN: name unknown", + "manifest unknown for tag", + "image not found", + ]) + def test_not_found_errors(self, tmp_path, stderr): + bad = _mk_run_result(returncode=1, stderr=stderr) + with patch("subprocess.run", return_value=bad): + with pytest.raises(ImageNotFoundError): + materialize_image(PULL_REF, tmp_path) + + def test_generic_error(self, tmp_path): + bad = _mk_run_result(returncode=1, stderr="something else broke") + with patch("subprocess.run", return_value=bad): + with pytest.raises(ImageExtractionError): + materialize_image(PULL_REF, tmp_path) + + +# --------------------------------------------------------------------------- +# Caching +# --------------------------------------------------------------------------- + +class TestCaching: + + def test_cache_hit_skips_extraction(self, tmp_path): + target = tmp_path / DIGEST + (target / "rootfs").mkdir(parents=True) + (target / "config.json").write_bytes(b"{}") + (target / image_extraction._COMPLETE_MARKER).touch() + + run_mock = MagicMock(return_value=_digest_run_ok()) + popen_mock = MagicMock() + with patch("subprocess.run", run_mock), patch("subprocess.Popen", popen_mock): + mat = materialize_image(PULL_REF, tmp_path) + + assert mat.digest == DIGEST + assert mat.rootfs_path == target / "rootfs" + # Only the digest call should have run; no Popen-based extraction. + assert run_mock.call_count == 1 + popen_mock.assert_not_called() + + def test_use_cache_false_re_extracts(self, tmp_path): + target = tmp_path / DIGEST + (target / "rootfs").mkdir(parents=True) + (target / "config.json").write_bytes(b"{}") + (target / image_extraction._COMPLETE_MARKER).touch() + + with patch("subprocess.run", side_effect=[_digest_run_ok(), _config_run_ok()]), \ + patch("subprocess.Popen", side_effect=_patch_extraction_popen()): + mat = materialize_image(PULL_REF, tmp_path, use_cache=False) + assert mat.digest == DIGEST + + def test_partial_cache_without_marker_is_replaced(self, tmp_path): + target = tmp_path / DIGEST + (target / "rootfs").mkdir(parents=True) + (target / "leftover").write_text("stale") + # No .complete marker → must re-extract. + + with patch("subprocess.run", side_effect=[_digest_run_ok(), _config_run_ok()]), \ + patch("subprocess.Popen", side_effect=_patch_extraction_popen()): + mat = materialize_image(PULL_REF, tmp_path) + + assert (target / image_extraction._COMPLETE_MARKER).exists() + assert not (target / "leftover").exists() + assert mat.digest == DIGEST + + +# --------------------------------------------------------------------------- +# Extraction +# --------------------------------------------------------------------------- + +class TestExtraction: + + def test_full_flow_publishes_atomically(self, tmp_path): + with patch("subprocess.run", side_effect=[_digest_run_ok(), _config_run_ok()]), \ + patch("subprocess.Popen", side_effect=_patch_extraction_popen()): + mat = materialize_image(PULL_REF, tmp_path) + + target = tmp_path / DIGEST + assert (target / image_extraction._COMPLETE_MARKER).exists() + assert (target / "rootfs").is_dir() + assert (target / "config.json").read_bytes() == b'{"config":{}}' + assert (target / "pull_ref.txt").read_text(encoding="utf-8") == PULL_REF + assert mat.rootfs_path == target / "rootfs" + assert mat.config_path == target / "config.json" + # No staging dir left behind. + assert not list(tmp_path.glob(f"{image_extraction._STAGING_PREFIX}*")) + + def test_tar_failure_removes_staging_and_raises(self, tmp_path): + with patch("subprocess.run", side_effect=[_digest_run_ok()]), \ + patch("subprocess.Popen", + side_effect=_patch_extraction_popen(rc_tar=2, stderr_tar=b"tar: error")): + with pytest.raises(ImageExtractionError): + materialize_image(PULL_REF, tmp_path) + assert not list(tmp_path.glob(f"{image_extraction._STAGING_PREFIX}*")) + assert not (tmp_path / DIGEST).exists() + + def test_disk_full_message_surfaces(self, tmp_path): + with patch("subprocess.run", side_effect=[_digest_run_ok()]), \ + patch("subprocess.Popen", + side_effect=_patch_extraction_popen( + rc_tar=1, stderr_tar=b"tar: No space left on device")): + with pytest.raises(ImageExtractionError, match="Disk full"): + materialize_image(PULL_REF, tmp_path) + + def test_crane_export_failure_raises(self, tmp_path): + with patch("subprocess.run", side_effect=[_digest_run_ok()]), \ + patch("subprocess.Popen", + side_effect=_patch_extraction_popen( + rc_crane=1, stderr_crane=b"crane: pull failed")): + with pytest.raises(ImageExtractionError): + materialize_image(PULL_REF, tmp_path) + + +# --------------------------------------------------------------------------- +# Stale staging cleanup +# --------------------------------------------------------------------------- + +class TestStaleStagingCleanup: + + def test_old_staging_dir_is_removed(self, tmp_path): + stale = tmp_path / f"{image_extraction._STAGING_PREFIX}{DIGEST}-old" + stale.mkdir() + old = time.time() - (image_extraction._STALE_STAGING_AGE_SECONDS + 60) + import os + os.utime(stale, (old, old)) + + with patch("subprocess.run", side_effect=[_digest_run_ok(), _config_run_ok()]), \ + patch("subprocess.Popen", side_effect=_patch_extraction_popen()): + materialize_image(PULL_REF, tmp_path) + + assert not stale.exists() + + def test_recent_staging_dir_is_kept(self, tmp_path): + recent = tmp_path / f"{image_extraction._STAGING_PREFIX}{DIGEST}-recent" + recent.mkdir() + + with patch("subprocess.run", side_effect=[_digest_run_ok(), _config_run_ok()]), \ + patch("subprocess.Popen", side_effect=_patch_extraction_popen()): + materialize_image(PULL_REF, tmp_path) + + assert recent.exists() + + +# --------------------------------------------------------------------------- +# Progress reporting +# --------------------------------------------------------------------------- + +class TestHumanBytes: + + @pytest.mark.parametrize("n,expected", [ + (0, "0.0 B"), + (512, "512.0 B"), + (2048, "2.0 KB"), + (5 * 1024 * 1024, "5.0 MB"), + (3 * 1024 * 1024 * 1024, "3.0 GB"), + ]) + def test_formats(self, n, expected): + assert _human_bytes(n) == expected + + +class TestDirSize: + + def test_counts_files_and_bytes(self, tmp_path): + (tmp_path / "a.txt").write_bytes(b"hello") + sub = tmp_path / "sub" + sub.mkdir() + (sub / "b.bin").write_bytes(b"x" * 100) + size, count = _dir_size(tmp_path) + assert count == 2 + assert size == 105 + + def test_handles_missing_dir(self, tmp_path): + size, count = _dir_size(tmp_path / "does-not-exist") + assert (size, count) == (0, 0) + + +class TestProgressReporter: + + def test_logs_periodically_during_work(self, tmp_path, caplog): + (tmp_path / "f").write_bytes(b"x" * 1024) + with caplog.at_level(logging.INFO, logger=image_extraction.__name__): + with _ProgressReporter(tmp_path, PULL_REF, interval=0.01): + time.sleep(0.05) + msgs = [r.message for r in caplog.records if "Extracting" in r.message] + assert msgs, "expected at least one progress log line" + assert PULL_REF in msgs[0] + + def test_zero_interval_disables_thread(self, tmp_path, caplog): + with caplog.at_level(logging.INFO, logger=image_extraction.__name__): + with _ProgressReporter(tmp_path, PULL_REF, interval=0): + time.sleep(0.05) + assert not [r for r in caplog.records if "Extracting" in r.message] + + def test_clean_exit_on_context_close(self, tmp_path): + rep = _ProgressReporter(tmp_path, PULL_REF, interval=0.01) + with rep: + time.sleep(0.02) + assert rep._thread is not None + assert not rep._thread.is_alive() + + +# --------------------------------------------------------------------------- +# Concurrency-bounded wrapper +# --------------------------------------------------------------------------- + +class TestMaterializeImageBounded: + """Verify the asyncio.Semaphore gate around :func:`materialize_image`. + + These tests mock the underlying (sync) ``materialize_image`` with a slow + stub so we can observe whether the semaphore actually caps concurrency. + """ + + @staticmethod + def _fake_materialize_factory(active_counter: dict, sleep_s: float): + """Return a sync stub that tracks max in-flight calls.""" + lock = threading.Lock() + + def fake(pull_ref: str, cache_root, **kwargs): + with lock: + active_counter["current"] += 1 + active_counter["max"] = max( + active_counter["max"], active_counter["current"] + ) + time.sleep(sleep_s) + with lock: + active_counter["current"] -= 1 + return MaterializedImage( + rootfs_path=Path(cache_root) / "rootfs", + config_path=Path(cache_root) / "config.json", + digest="sha256:test", + pull_ref=pull_ref, + ) + + return fake + + @pytest.mark.asyncio + async def test_caps_concurrent_extractions(self, tmp_path): + limit = 2 + n_requests = 6 + sleep_s = 0.05 + counter = {"current": 0, "max": 0} + fake = self._fake_materialize_factory(counter, sleep_s) + sem = asyncio.Semaphore(limit) + + with patch("vuln_analysis.utils.image_extraction.materialize_image", + side_effect=fake): + results = await asyncio.gather(*[ + materialize_image_bounded(sem, f"reg/img:{i}", tmp_path, use_cache=False) + for i in range(n_requests) + ]) + + assert len(results) == n_requests + # Gate held: peak in-flight never exceeded the limit. + assert counter["max"] <= limit + # Sanity: we actually achieved parallelism (otherwise we tested nothing). + assert counter["max"] >= 2 + + @pytest.mark.asyncio + async def test_releases_slot_on_failure(self, tmp_path): + """If extraction raises, the slot must still be released for the next waiter.""" + sem = asyncio.Semaphore(1) + call_count = {"n": 0} + + def fake(pull_ref, cache_root, **kwargs): + call_count["n"] += 1 + if call_count["n"] == 1: + raise ImageMaterializationError("boom") + return MaterializedImage( + rootfs_path=Path(cache_root) / "rootfs", + config_path=Path(cache_root) / "config.json", + digest="sha256:test", + pull_ref=pull_ref, + ) + + with patch("vuln_analysis.utils.image_extraction.materialize_image", + side_effect=fake): + with pytest.raises(ImageMaterializationError): + await materialize_image_bounded(sem, "img:1", tmp_path, use_cache=False) + # Second call must succeed — proves the failed first call released the slot. + result = await materialize_image_bounded(sem, "img:2", tmp_path, use_cache=False) + + assert result.pull_ref == "img:2" + assert sem._value == 1 # type: ignore[attr-defined] + + @pytest.mark.asyncio + async def test_wait_log_emitted_when_threshold_exceeded(self, tmp_path, caplog): + sem = asyncio.Semaphore(1) + sleep_s = 0.1 + counter = {"current": 0, "max": 0} + fake = self._fake_materialize_factory(counter, sleep_s) + + with caplog.at_level(logging.INFO, logger=image_extraction.__name__), \ + patch("vuln_analysis.utils.image_extraction.materialize_image", + side_effect=fake): + await asyncio.gather( + materialize_image_bounded(sem, "img:1", tmp_path, use_cache=False, + wait_log_threshold_s=0.01), + materialize_image_bounded(sem, "img:2", tmp_path, use_cache=False, + wait_log_threshold_s=0.01), + ) + + wait_msgs = [r.message for r in caplog.records + if "Waited" in r.message and "extraction slot" in r.message] + assert wait_msgs, "expected a wait-log line for the second caller" diff --git a/uv.lock b/uv.lock index d6f8e9779..0ca354047 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,9 @@ version = 1 -revision = 3 -requires-python = ">=3.11, <3.13" +revision = 1 +requires-python = "==3.13.*" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform != 'win32'", + "sys_platform == 'win32'", + "sys_platform != 'win32'", ] [[package]] @@ -16,9 +14,9 @@ dependencies = [ { name = "aiobotocore", extra = ["boto3"] }, { name = "aiofiles" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913 }, ] [[package]] @@ -34,9 +32,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039 }, ] [package.optional-dependencies] @@ -48,23 +46,23 @@ boto3 = [ name = "aiofiles" version = "25.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668 }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, ] [[package]] name = "aiohttp" -version = "3.13.2" +version = "3.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -75,87 +73,58 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, - { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, - { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, - { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, - { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, - { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, - { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, - { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, -] - -[[package]] -name = "aiohttp-client-cache" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "attrs" }, - { name = "itsdangerous" }, - { name = "url-normalize" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/3e/6c302a45c23c746afc86d1f35d85125815df9273b17cfb3035f3dfe69971/aiohttp_client_cache-0.11.0.tar.gz", hash = "sha256:0766fff4eda05498c7525374a587810dcc2ccb7b256809dde52ae8790a8453eb", size = 61244, upload-time = "2024-02-09T12:50:40.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/2c/2203bdced6b17aef522775521f0fbebc18a62b766b10f419f6ae5c815a2d/aiohttp_client_cache-0.11.0-py3-none-any.whl", hash = "sha256:5b6217bc26a7b3f5f939809b63a66b67658b660809cd38869d7d45066a26d079", size = 31804, upload-time = "2024-02-09T12:50:38.471Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399 }, + { url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068 }, + { url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670 }, + { url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591 }, + { url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503 }, + { url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870 }, + { url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588 }, + { url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492 }, + { url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111 }, + { url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674 }, + { url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808 }, + { url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921 }, + { url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241 }, + { url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554 }, + { url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864 }, + { url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803 }, + { url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671 }, + { url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904 }, + { url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516 }, + { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656 }, + { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803 }, + { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889 }, ] [[package]] name = "aioitertools" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182 }, ] [[package]] name = "aiolimiter" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185, upload-time = "2024-12-08T15:31:51.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711, upload-time = "2024-12-08T15:31:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711 }, ] [[package]] -name = "aioresponses" -version = "0.7.6" +name = "aiorwlock" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/63/bb78ed078e2d514050aadc42a932465a83c43c628746f0e788500ec0bf5d/aioresponses-0.7.6.tar.gz", hash = "sha256:f795d9dbda2d61774840e7e32f5366f45752d1adc1b74c9362afd017296c7ee1", size = 35846, upload-time = "2023-11-20T17:55:20.934Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/65/316cdc82c1b92953235ced1c71a3763f0cd9273c3bec5db60bdb5ad59bfe/aiorwlock-1.5.1.tar.gz", hash = "sha256:2729c77ec736c8d85ec305aa3827a50394fd8c6d823f4404d301cc8c59a4b7f5", size = 7288 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/1e/c259a960a4dff46840133ce19682ef14db2922da80a6088283ec9c3f647a/aioresponses-0.7.6-py2.py3-none-any.whl", hash = "sha256:d2c26defbb9b440ea2685ec132e90700907fd10bcca3e85ec2f157219f0d26f7", size = 11875, upload-time = "2023-11-20T17:55:18.795Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/dc60c00fb9178e356d7f5fb009aabae4625ce79c3e63ed4ef3bcc73d949c/aiorwlock-1.5.1-py3-none-any.whl", hash = "sha256:a28e534a5fce4dabe437055db141369a0803c69fe61c406b6fc5cdfa8f3dda13", size = 8016 }, ] [[package]] @@ -164,11 +133,10 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, ] [[package]] @@ -178,9 +146,95 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792 }, +] + +[[package]] +name = "aiq-agent" +version = "2.0.0" +source = { editable = "aiq" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiosqlite" }, + { name = "asyncpg" }, + { name = "cryptography" }, + { name = "deepagents" }, + { name = "knowledge-layer", extra = ["all"] }, + { name = "langchain-modal" }, + { name = "langgraph-checkpoint-postgres" }, + { name = "langgraph-checkpoint-sqlite" }, + { name = "nvidia-nat", extra = ["async-endpoints", "langchain", "mcp", "phoenix"] }, + { name = "nvidia-nat-core" }, + { name = "nvidia-nat-eval" }, + { name = "nvidia-nat-profiler" }, + { name = "pillow" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pygments" }, + { name = "python-multipart" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=23.0.0" }, + { name = "aiosqlite", specifier = ">=0.19.0" }, + { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "cryptography", specifier = ">=46.0.6,<47" }, + { name = "deepagents", specifier = ">=0.6.5" }, + { name = "knowledge-layer", extras = ["all"], editable = "aiq/sources/knowledge_layer" }, + { name = "langchain-modal", specifier = "==0.0.5" }, + { name = "langgraph-checkpoint-postgres", specifier = ">=3.0.0" }, + { name = "langgraph-checkpoint-sqlite", specifier = ">=2.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=3.0" }, + { name = "nvidia-nat", extras = ["langchain", "async-endpoints", "phoenix", "mcp"], specifier = "==1.7.0" }, + { name = "nvidia-nat-core", specifier = "==1.7.0" }, + { name = "nvidia-nat-eval", specifier = "==1.7.0" }, + { name = "nvidia-nat-profiler", specifier = "==1.7.0" }, + { name = "nvidia-sphinx-theme", marker = "extra == 'docs'", specifier = ">=0.0.9.post1" }, + { name = "pillow", specifier = ">=12.2.0,<13" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.0.0" }, + { name = "pygments", specifier = ">=2.20.0,<3" }, + { name = "pygraphviz", marker = "extra == 'viz'", specifier = ">=1.11" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "~=8.3" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==0.24.*" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = "~=6.1" }, + { name = "pytest-env", marker = "extra == 'dev'", specifier = ">=1.1.0" }, + { name = "python-multipart", specifier = ">=0.0.27,<0.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "~=0.15.1" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.0" }, + { name = "sphinx-autobuild", marker = "extra == 'docs'", specifier = ">=2025.8.25" }, + { name = "sphinx-copybutton", marker = "extra == 'docs'", specifier = ">=0.5" }, + { name = "sphinx-design", marker = "extra == 'docs'", specifier = ">=0.7.0" }, + { name = "sphinx-mermaid", marker = "extra == 'docs'", specifier = ">=0.0.8" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=2.0" }, + { name = "sphinxcontrib-mermaid", marker = "extra == 'docs'", specifier = ">=2.0.1" }, + { name = "yapf", marker = "extra == 'dev'", specifier = "==0.43.*" }, +] +provides-extras = ["dev", "docs", "viz"] + +[package.metadata.requires-dev] +dev = [ + { name = "aiq-api", editable = "aiq/frontends/aiq_api" }, + { name = "aiq-debug", editable = "aiq/frontends/debug" }, + { name = "aiq-research-cli", editable = "aiq/frontends/cli" }, + { name = "dask", extras = ["distributed"], specifier = ">=2024.1.0" }, + { name = "duckduckgo-news-search", editable = "aiq/sources/duckduckgo_news_search" }, + { name = "exa-web-search", editable = "aiq/sources/exa_web_search" }, + { name = "google-scholar-paper-search", editable = "aiq/sources/google_scholar_paper_search" }, + { name = "knowledge-layer", extras = ["all"], editable = "aiq/sources/knowledge_layer" }, + { name = "mypy", specifier = ">=1.5.0" }, + { name = "polymarket-prediction-market", editable = "aiq/sources/polymarket_prediction_market" }, + { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "pytest", specifier = "~=8.3" }, + { name = "pytest-asyncio", specifier = "==0.24.*" }, + { name = "pytest-cov", specifier = "~=6.1" }, + { name = "pytest-env", specifier = ">=1.1.0" }, + { name = "ruff", specifier = "~=0.15.1" }, + { name = "tavily-web-search", editable = "aiq/sources/tavily_web_search" }, + { name = "yapf", specifier = "==0.43.*" }, ] [[package]] @@ -192,41 +246,59 @@ dependencies = [ { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554 }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] -name = "anyio" -version = "4.11.0" +name = "anthropic" +version = "0.109.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/b7/9a8e2f79011e89dd6eeb599c27332aed765dac9d6fbee3a55e68e4e3ec25/anthropic-0.109.2.tar.gz", hash = "sha256:d37db299597c7bc124b49b767ff135f1e6456b64af2b2fad4b63b2a1df333cf0", size = 927559 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/bee5de8a2699fc8a3cce34d61c7a2626a2c310ddde7ea5611327eb0ddbe9/anthropic-0.109.2-py3-none-any.whl", hash = "sha256:e0fb4ca5df0ed983248c9c6c3242adc81d9cfddb8725902da53698554117abac", size = 923800 }, ] [[package]] -name = "appdirs" -version = "1.4.4" +name = "anyio" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 }, ] [[package]] @@ -249,8 +321,7 @@ dependencies = [ { name = "httpx" }, { name = "jinja2" }, { name = "jmespath" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "openinference-instrumentation" }, { name = "openinference-semantic-conventions" }, { name = "opentelemetry-exporter-otlp" }, @@ -277,9 +348,9 @@ dependencies = [ { name = "uvicorn" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/ef/858ffaff6e02353948603490d17a3163a065befdbab57d4321735f4878df/arize_phoenix-12.16.0.tar.gz", hash = "sha256:58ee465ab646a2174a6ab0777012ba3404bb83a06ba71aa926fc292b82a82b4e", size = 2334835, upload-time = "2025-11-19T16:55:30.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/ef/858ffaff6e02353948603490d17a3163a065befdbab57d4321735f4878df/arize_phoenix-12.16.0.tar.gz", hash = "sha256:58ee465ab646a2174a6ab0777012ba3404bb83a06ba71aa926fc292b82a82b4e", size = 2334835 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/c5/22e17f01d55beb1825616716e9667f72fcc2857978d903492444977b51d7/arize_phoenix-12.16.0-py3-none-any.whl", hash = "sha256:c98688a80c8bc0defbcdb20128fc887692e0a81ff2527754767ea59575975462", size = 2557497, upload-time = "2025-11-19T16:55:28.095Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c5/22e17f01d55beb1825616716e9667f72fcc2857978d903492444977b51d7/arize_phoenix-12.16.0-py3-none-any.whl", hash = "sha256:c98688a80c8bc0defbcdb20128fc887692e0a81ff2527754767ea59575975462", size = 2557497 }, ] [[package]] @@ -295,9 +366,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/ea/cdf01262013d0f1b8129d385187bcd14a5ba45a6d30ed37d64b58f13d367/arize_phoenix_client-1.24.0.tar.gz", hash = "sha256:45e669dff2243f74c9d2d954c2de22c46bd609889e08ba7f58974c59b823e4fe", size = 137022, upload-time = "2025-11-19T19:50:13.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/ea/cdf01262013d0f1b8129d385187bcd14a5ba45a6d30ed37d64b58f13d367/arize_phoenix_client-1.24.0.tar.gz", hash = "sha256:45e669dff2243f74c9d2d954c2de22c46bd609889e08ba7f58974c59b823e4fe", size = 137022 } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/41/25913f57d7e83fdceb289a5600c6e7f3fdb7016b40bec9b6bba085fe40b2/arize_phoenix_client-1.24.0-py3-none-any.whl", hash = "sha256:76378223573f7d3cedb977a4926368087fe90c65d60b8e9bc630c485a0d52ff9", size = 143956, upload-time = "2025-11-19T19:50:11.489Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/25913f57d7e83fdceb289a5600c6e7f3fdb7016b40bec9b6bba085fe40b2/arize_phoenix_client-1.24.0-py3-none-any.whl", hash = "sha256:76378223573f7d3cedb977a4926368087fe90c65d60b8e9bc630c485a0d52ff9", size = 143956 }, ] [[package]] @@ -315,9 +386,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/e4/a7402d8e8fb95153e587df3e59ec1ee247688f2151e76a9fb63a9aaafe4b/arize_phoenix_evals-2.6.0.tar.gz", hash = "sha256:f64dd81a8a632b38659f92443193c9c8e66bf25f4f0d6b468af2ef055f32b7af", size = 102498, upload-time = "2025-11-12T22:46:48.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/e4/a7402d8e8fb95153e587df3e59ec1ee247688f2151e76a9fb63a9aaafe4b/arize_phoenix_evals-2.6.0.tar.gz", hash = "sha256:f64dd81a8a632b38659f92443193c9c8e66bf25f4f0d6b468af2ef055f32b7af", size = 102498 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/85/316deacbfd2a88958a66021d8a471d462d5c7d3dccc5d1f864b6ffbb958a/arize_phoenix_evals-2.6.0-py3-none-any.whl", hash = "sha256:cb461d9e05a781ae9145ec3a261c2126fbc5553a61bbb86c7cf8a898a23daef2", size = 143361, upload-time = "2025-11-12T22:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/a5/85/316deacbfd2a88958a66021d8a471d462d5c7d3dccc5d1f864b6ffbb958a/arize_phoenix_evals-2.6.0-py3-none-any.whl", hash = "sha256:cb461d9e05a781ae9145ec3a261c2126fbc5553a61bbb86c7cf8a898a23daef2", size = 143361 }, ] [[package]] @@ -334,45 +405,132 @@ dependencies = [ { name = "typing-extensions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/3b/8745dc29b9d7db658f64d4a14d100285d1b74f38827171c22120aa5b7c9c/arize_phoenix_otel-0.13.1.tar.gz", hash = "sha256:6fb7bcfed4260b9ddd5bcaa20a678b60e388525e4bbaec1247092f163a0e0204", size = 20166, upload-time = "2025-09-10T06:09:30.954Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/3b/8745dc29b9d7db658f64d4a14d100285d1b74f38827171c22120aa5b7c9c/arize_phoenix_otel-0.13.1.tar.gz", hash = "sha256:6fb7bcfed4260b9ddd5bcaa20a678b60e388525e4bbaec1247092f163a0e0204", size = 20166 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/40/1d421e172453f07fc8a91b5a2407350bbba598e37b9025ae84b68b0ce8a2/arize_phoenix_otel-0.13.1-py3-none-any.whl", hash = "sha256:98d34da78aebac7f60ec4bc30f0eab1e4490c7329b2c74988b9684f9dc182949", size = 17717, upload-time = "2025-09-10T06:09:30.037Z" }, + { url = "https://files.pythonhosted.org/packages/8d/40/1d421e172453f07fc8a91b5a2407350bbba598e37b9025ae84b68b0ce8a2/arize_phoenix_otel-0.13.1-py3-none-any.whl", hash = "sha256:98d34da78aebac7f60ec4bc30f0eab1e4490c7329b2c74988b9684f9dc182949", size = 17717 }, ] [[package]] name = "arpy" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/d5/a1985a4a645a1d66825e4cbb9fa7e8c5d10bf5b41fd32badb38b6b5e55ea/arpy-2.3.0.tar.gz", hash = "sha256:8302829a991cfcef2630b61e00f315db73164021cecbd7fb1fc18525f83f339c", size = 9572, upload-time = "2022-06-11T05:07:00.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/d5/a1985a4a645a1d66825e4cbb9fa7e8c5d10bf5b41fd32badb38b6b5e55ea/arpy-2.3.0.tar.gz", hash = "sha256:8302829a991cfcef2630b61e00f315db73164021cecbd7fb1fc18525f83f339c", size = 9572 } [[package]] name = "astroid" version = "3.3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612 }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667 } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111 }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928 }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067 }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156 }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636 }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079 }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606 }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569 }, ] [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, ] [[package]] name = "authlib" -version = "1.6.5" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, + { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548 }, +] + +[[package]] +name = "banks" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "filetype" }, + { name = "griffe" }, + { name = "jinja2" }, + { name = "platformdirs" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/51/08fb68d23f4b0f6256fe85dc86e9576941550f890b079352fba719e07b39/banks-2.4.2.tar.gz", hash = "sha256:cda6013bd377ea7b701933578bfb9370fc21ad70bc13cedfc3f5cb2c034ca3dc", size = 188633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b6/8dc5477681b782e2f99de703e7a99828883364b9e03a60d3e2c47053d56a/banks-2.4.2-py3-none-any.whl", hash = "sha256:5fe407cc48c101f3e13d1cf732b83b8246003337612f13c0705d2e81f6faffb7", size = 35050 }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806 }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626 }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853 }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793 }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930 }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194 }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381 }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750 }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757 }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740 }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197 }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974 }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498 }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853 }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626 }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553 }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009 }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029 }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907 }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500 }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412 }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486 }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940 }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776 }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922 }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367 }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187 }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752 }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881 }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931 }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313 }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290 }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253 }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185 }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656 }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662 }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240 }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152 }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284 }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643 }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698 }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725 }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912 }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953 }, ] [[package]] @@ -383,9 +541,30 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392 }, +] + +[[package]] +name = "bokeh" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "jinja2" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "tornado", marker = "sys_platform != 'emscripten'" }, + { name = "xyzservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/7ee0c4dfd0255631b0624ce01be178704f91f763f02a1879368eb109befd/bokeh-3.8.2.tar.gz", hash = "sha256:8e7dcacc21d53905581b54328ad2705954f72f2997f99fc332c1de8da53aa3cc", size = 6529251 } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131 }, ] [[package]] @@ -397,9 +576,9 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321 }, ] [[package]] @@ -411,55 +590,66 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973 }, ] [[package]] -name = "brotli" -version = "1.2.0" +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508 }, +] + +[[package]] +name = "build" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, - { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, - { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, - { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, - { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, - { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, - { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, - { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, - { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, - { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018 }, ] [[package]] name = "cachetools" version = "6.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503 }, +] + +[[package]] +name = "cbor2" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/af/473c241e41c142ea06ebef8d1f660fa6ff928fb97210e7bec8ee5974f8cd/cbor2-6.1.2.tar.gz", hash = "sha256:6b43037a66947dee5af0abb1a4c3a13b3abac5a4a3f32f9771efbbcd030fd909", size = 86760 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/bc045c8f36317e4e5f7a60d94b36833139909fc32e3a65f44bc61a36def0/cbor2-6.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f1aa38c422d87ea61849b2a823b10b64053fb4da8763f19ac78ea9a69d682b2a", size = 408846 }, + { url = "https://files.pythonhosted.org/packages/2b/36/d66f5f0dd98ecbdcfc7da1fbd423f7b3782a27719f0062a560476f00b334/cbor2-6.1.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ff7d0bd8ff432832338a8d2430aee34f8a082342480ff537c0ba90e2b8ff7894", size = 454624 }, + { url = "https://files.pythonhosted.org/packages/38/6b/4884b9cf03db14dc5007825d5d1bf8678a75c49d4268d8e0c1c6e9580104/cbor2-6.1.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c1eedf3290d88a5f663bd8b4b8f0f0e2103d0594c293fa5f4e62e53100972309", size = 466585 }, + { url = "https://files.pythonhosted.org/packages/50/f6/36a15beb3915f56a79d6e9213c6d40c0f5cb90cd3462923f555d78068847/cbor2-6.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3049b04bddf9a5a2d0e5bb25dccdaf4552fcaf607b404e249d4f78f010fcc7d0", size = 521678 }, + { url = "https://files.pythonhosted.org/packages/c6/3f/e899313371ebeb7a191d751de97ccd8242abc24bbc9d8e2c58e04475cfb0/cbor2-6.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96eb687a62040401668f06a85de8f47361ef44574de1493899e0ec678109fc04", size = 534044 }, + { url = "https://files.pythonhosted.org/packages/1e/5e/1a872acdeb1ab9a884ec3460f73a43e02154dc20d8ccb627bbd60f4c0ea1/cbor2-6.1.2-cp313-cp313-win32.whl", hash = "sha256:03440b505882280023db1fedcee6844804e9968bb50f9eb4ff12aaf27777fcfe", size = 282328 }, + { url = "https://files.pythonhosted.org/packages/70/79/29721bc15d38889e7bec214ede2346ee15970bedcc5e6ce1fa30f21e9a4e/cbor2-6.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:d2c8da2c0f821827dcc9eb59a5c9351791a8aa3b389a2ea7ca64c4f97bcb94cf", size = 300313 }, + { url = "https://files.pythonhosted.org/packages/07/98/a13b424fb2f14fe332b57f71f479953b2f291a051f797d42ddab9fcd2027/cbor2-6.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:8e1478d3b980ddfcaf56e27cecbfe13057e0f67d5e8240fe8a398815acb9c4bf", size = 288725 }, ] [[package]] name = "certifi" version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, ] [[package]] @@ -469,74 +659,96 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "chromadb" +version = "1.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d1/5e33b26985f0c7046a0be1cee2158ada1748ee700d2545057fde1468d74d/chromadb-1.5.9.tar.gz", hash = "sha256:5c20e62a455c28bacac927f26116a73fd8e1799e0d908be8e8a4f02197a54731", size = 2595635 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/5b/3cced915244f43ed14b53fe9f63a37f05f865064f4e4fe7d9448d3f2a352/chromadb-1.5.9-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60701011b5e6409647fa40d12c7c5a66b2b0bfcf33a52db2ad53a30a2abc4957", size = 22564540 }, + { url = "https://files.pythonhosted.org/packages/34/4c/adcef1f4e82a2ef69ccd3711d55fc289193d54c4c0ff7a0292a3631db46f/chromadb-1.5.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:814b9c95617377f6501e5757d63dfddb554a283a7739c87b9fa573850174e6f3", size = 21699698 }, + { url = "https://files.pythonhosted.org/packages/38/4e/937bc4d2e6f8ab9664ec79931fbbd69efff47e513ec2924b071e4b0ff774/chromadb-1.5.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9192d111bd662241625867962333d99369a00769a50f8b2f58cb388731274d7e", size = 22680924 }, + { url = "https://files.pythonhosted.org/packages/e6/ec/0c42039e80b9acc534f67b73b7a42471948042859b3a64867b50a4a77fa3/chromadb-1.5.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc09b3df76e5a5cb386aed2715a2eea152e3949f9e1ba93c7119505377749929", size = 23316203 }, + { url = "https://files.pythonhosted.org/packages/eb/ce/0f7be6e5d0feafa2cda54b12e6542afeea7dea89d2d411e14da90f8abb96/chromadb-1.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:4fd0b560e56761b7f3cb4d5c6205fd5f20814484b4a3e4e9af9038c2b428fc6c", size = 23542454 }, +] + +[[package]] +name = "circuitbreaker" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ac/de7a92c4ed39cba31fe5ad9203b76a25ca67c530797f6bb420fff5f65ccb/circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084", size = 10787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/34/15f08edd4628f65217de1fc3c1a27c82e46fe357d60c217fc9881e12ebcc/circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1", size = 7737 }, ] [[package]] @@ -546,39 +758,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, ] [[package]] name = "cloudpickle" version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "colorlog" -version = "6.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] @@ -586,153 +786,159 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234 }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169 }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859 }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062 }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932 }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024 }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578 }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524 }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730 }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897 }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751 }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486 }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106 }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548 }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297 }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023 }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157 }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570 }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713 }, ] [[package]] name = "coverage" version = "7.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, - { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, - { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, - { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, - { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, - { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, - { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, - { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725 }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098 }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093 }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686 }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930 }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296 }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068 }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034 }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853 }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619 }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261 }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072 }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702 }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420 }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773 }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078 }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144 }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574 }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298 }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150 }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763 }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653 }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856 }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936 }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001 }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273 }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503 }, +] + +[[package]] +name = "crc32c" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/66/7e97aa77af7cf6afbff26e3651b564fe41932599bc2d3dce0b2f73d4829a/crc32c-2.8.tar.gz", hash = "sha256:578728964e59c47c356aeeedee6220e021e124b9d3e8631d95d9a5e5f06e261c", size = 48179 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/d8/3ae227890b3be40955a7144106ef4dd97d6123a82c2a5310cdab58ca49d8/crc32c-2.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36f1e03ee9e9c6938e67d3bcb60e36f260170aa5f37da1185e04ef37b56af395", size = 66380 }, + { url = "https://files.pythonhosted.org/packages/bd/8b/178d3f987cd0e049b484615512d3f91f3d2caeeb8ff336bb5896ae317438/crc32c-2.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b2f3226b94b85a8dd9b3533601d7a63e9e3e8edf03a8a169830ee8303a199aeb", size = 63048 }, + { url = "https://files.pythonhosted.org/packages/f2/a1/48145ae2545ebc0169d3283ebe882da580ea4606bfb67cf4ca922ac3cfc3/crc32c-2.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e08628bc72d5b6bc8e0730e8f142194b610e780a98c58cb6698e665cb885a5b", size = 61530 }, + { url = "https://files.pythonhosted.org/packages/06/4b/cf05ed9d934cc30e5ae22f97c8272face420a476090e736615d9a6b53de0/crc32c-2.8-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:086f64793c5ec856d1ab31a026d52ad2b895ac83d7a38fce557d74eb857f0a82", size = 80001 }, + { url = "https://files.pythonhosted.org/packages/15/ab/4b04801739faf36345f6ba1920be5b1c70282fec52f8280afd3613fb13e2/crc32c-2.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcf72ee7e0135b3d941c34bb2c26c3fc6bc207106b49fd89aaafaeae223ae209", size = 81543 }, + { url = "https://files.pythonhosted.org/packages/a9/1b/6e38dde5bfd2ea69b7f2ab6ec229fcd972a53d39e2db4efe75c0ac0382ce/crc32c-2.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a717dd9c3fd777d9bc6603717eae172887d402c4ab589d124ebd0184a83f89e", size = 80644 }, + { url = "https://files.pythonhosted.org/packages/ce/45/012176ffee90059ae8ec7131019c71724ea472aa63e72c0c8edbd1fad1d7/crc32c-2.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0450bb845b3c3c7b9bdc0b4e95620ec9a40824abdc8c86d6285c919a90743c1a", size = 79919 }, + { url = "https://files.pythonhosted.org/packages/f0/2b/f557629842f9dec2b3461cb3a0d854bb586ec45b814cea58b082c32f0dde/crc32c-2.8-cp313-cp313-win32.whl", hash = "sha256:765d220bfcbcffa6598ac11eb1e10af0ee4802b49fe126aa6bf79f8ddb9931d1", size = 64896 }, + { url = "https://files.pythonhosted.org/packages/d0/db/fd0f698c15d1e21d47c64181a98290665a08fcbb3940cd559e9c15bda57e/crc32c-2.8-cp313-cp313-win_amd64.whl", hash = "sha256:171ff0260d112c62abcce29332986950a57bddee514e0a2418bfde493ea06bb3", size = 66646 }, + { url = "https://files.pythonhosted.org/packages/db/b9/8e5d7054fe8e7eecab10fd0c8e7ffb01439417bdb6de1d66a81c38fc4a20/crc32c-2.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b977a32a3708d6f51703c8557008f190aaa434d7347431efb0e86fcbe78c2a50", size = 66203 }, + { url = "https://files.pythonhosted.org/packages/55/5f/cc926c70057a63cc0c98a3c8a896eb15fc7e74d3034eadd53c94917c6cc3/crc32c-2.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7399b01db4adaf41da2fb36fe2408e75a8d82a179a9564ed7619412e427b26d6", size = 62956 }, + { url = "https://files.pythonhosted.org/packages/a1/8a/0660c44a2dd2cb6ccbb529eb363b9280f5c766f1017bc8355ed8d695bd94/crc32c-2.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4379f73f9cdad31958a673d11a332ec725ca71572401ca865867229f5f15e853", size = 61442 }, + { url = "https://files.pythonhosted.org/packages/f5/5a/6108d2dfc0fe33522ce83ba07aed4b22014911b387afa228808a278e27cd/crc32c-2.8-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e68264555fab19bab08331550dab58573e351a63ed79c869d455edd3b0aa417", size = 79109 }, + { url = "https://files.pythonhosted.org/packages/84/1e/c054f9e390090c197abf3d2936f4f9effaf0c6ee14569ae03d6ddf86958a/crc32c-2.8-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b48f2486727b8d0e7ccbae4a34cb0300498433d2a9d6b49cb13cb57c2e3f19cb", size = 80987 }, + { url = "https://files.pythonhosted.org/packages/c8/ad/1650e5c3341e4a485f800ea83116d72965030c5d48ccc168fcc685756e4d/crc32c-2.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ecf123348934a086df8c8fde7f9f2d716d523ca0707c5a1367b8bb00d8134823", size = 79994 }, + { url = "https://files.pythonhosted.org/packages/d7/3b/f2ed924b177729cbb2ab30ca2902abff653c31d48c95e7b66717a9ca9fcc/crc32c-2.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e636ac60f76de538f7a2c0d0f3abf43104ee83a8f5e516f6345dc283ed1a4df7", size = 79046 }, + { url = "https://files.pythonhosted.org/packages/4b/80/413b05ee6ace613208b31b3670c3135ee1cf451f0e72a9c839b4946acc04/crc32c-2.8-cp313-cp313t-win32.whl", hash = "sha256:8dd4a19505e0253892e1b2f1425cc3bd47f79ae5a04cb8800315d00aad7197f2", size = 64837 }, + { url = "https://files.pythonhosted.org/packages/3b/1b/85eddb6ac5b38496c4e35c20298aae627970c88c3c624a22ab33e84f16c7/crc32c-2.8-cp313-cp313t-win_amd64.whl", hash = "sha256:4bb18e4bd98fb266596523ffc6be9c5b2387b2fa4e505ec56ca36336f49cb639", size = 66574 }, ] [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869 }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492 }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670 }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275 }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402 }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985 }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652 }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805 }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883 }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756 }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244 }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868 }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504 }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363 }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618 }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628 }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405 }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715 }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400 }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634 }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233 }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955 }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888 }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961 }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696 }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256 }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001 }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985 }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, ] [[package]] name = "dask" -version = "2023.6.0" +version = "2026.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "cloudpickle" }, { name = "fsspec" }, - { name = "importlib-metadata" }, { name = "packaging" }, { name = "partd" }, { name = "pyyaml" }, { name = "toolz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/70/9da1c8bd773fe494448fd3a78532d21df10c499ffada926c3a9014323c72/dask-2023.6.0.tar.gz", hash = "sha256:980741966ef4d14c62dfb146f315f57d5eaaa6bedc52866f99687bc6054c2d4b", size = 8514740, upload-time = "2023-06-09T16:44:21.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/52/b0f9172b22778def907db1ff173249e4eb41f054b46a9c83b1528aaf811f/dask-2026.1.2.tar.gz", hash = "sha256:1136683de2750d98ea792670f7434e6c1cfce90cab2cc2f2495a9e60fd25a4fc", size = 10997838 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/ae/233e8bc6b02b4d066e607f4335de37bf965f78334bc7b1cb33b5d42fd35d/dask-2023.6.0-py3-none-any.whl", hash = "sha256:294f449108397c4ca7a3bca27e32cb0fe9c65599c57d6301b3b70f825c2457d4", size = 1182567, upload-time = "2023-06-09T16:44:11.975Z" }, + { url = "https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl", hash = "sha256:46a0cf3b8d87f78a3d2e6b145aea4418a6d6d606fe6a16c79bd8ca2bb862bc91", size = 1482084 }, ] [[package]] @@ -743,9 +949,9 @@ dependencies = [ { name = "marshmallow" }, { name = "typing-inspect" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 }, ] [[package]] @@ -759,8 +965,7 @@ dependencies = [ { name = "httpx" }, { name = "huggingface-hub" }, { name = "multiprocess" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "packaging" }, { name = "pandas" }, { name = "pyarrow" }, @@ -769,32 +974,61 @@ dependencies = [ { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/bf/0dae295d6d1ba0b1a200a9dd216838464b5bbd05da01407cb1330b377445/datasets-4.4.1.tar.gz", hash = "sha256:80322699aa8c0bbbdb7caa87906da689c3c2e29523cff698775c67f28fdab1fc", size = 585341, upload-time = "2025-11-05T16:00:38.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/bf/0dae295d6d1ba0b1a200a9dd216838464b5bbd05da01407cb1330b377445/datasets-4.4.1.tar.gz", hash = "sha256:80322699aa8c0bbbdb7caa87906da689c3c2e29523cff698775c67f28fdab1fc", size = 585341 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591 }, +] + +[[package]] +name = "deepagents" +version = "0.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "langchain-google-genai" }, + { name = "langsmith" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/ab/3225f47404d401559ab67819b3b20833cdefe7e283e39f796d6019c7dfa7/deepagents-0.6.10.tar.gz", hash = "sha256:bce9f8e6b7870fe1bba5e5a128588e6f38df810f60695e01dd568e6e62e74a89", size = 203631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/81/49f1a98434b462aa60a07ef5a98437bd6a4445219b91c459e1e7e5d5564e/deepagents-0.6.10-py3-none-any.whl", hash = "sha256:21486ba213f027f7f2d5b4822bf6099f806a1d325dd33e93d3f5b9e857b2ea89", size = 228251 }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591, upload-time = "2025-11-05T16:00:36.365Z" }, + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298 }, ] [[package]] name = "dill" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, ] [[package]] -name = "diskcache" -version = "5.6.3" +name = "dirtyjson" +version = "1.0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197 }, ] [[package]] name = "distributed" -version = "2023.6.0" +version = "2026.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -813,34 +1047,61 @@ dependencies = [ { name = "urllib3" }, { name = "zict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/4f/c0a4f1e330f83312f598a310682d5b0cc96ecceabd8a80b74ed1ac94864d/distributed-2023.6.0.tar.gz", hash = "sha256:a5bdd4d15f3e7c8df98513b409753d46a39827a650620cc0c0c5ef2751561280", size = 1062868, upload-time = "2023-06-09T16:42:33.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/75/b6e5b77229097ff03dd5ba6a07c77e2da87e7e991ccfef412549bba78746/distributed-2026.1.2.tar.gz", hash = "sha256:8333fa7a34151ed3b4cf1a03136fe1f1799eca706a5e47bdb63022c8795d853b", size = 2103721 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f1/3f90dc2fb600c72a957623ac06c1000c5ec3cd5c709689c93d54647d62c7/distributed-2023.6.0-py3-none-any.whl", hash = "sha256:dea5c199623aab6babaa5c6a9c98542723c1436d065d28c68ac324266978701b", size = 976090, upload-time = "2023-06-09T16:42:28.702Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/0fe5889a83991ac29c93e6b2e121ad2afc3bff5f9327f34447d3068d8142/distributed-2026.1.2-py3-none-any.whl", hash = "sha256:30ccb5587351f50304f6f6e219ea91bc09d88401125779caa8be5253e9d3ecf2", size = 1009083 }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] [[package]] name = "dnspython" version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094 }, ] [[package]] name = "docopt" version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901 } + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484 }, +] + +[[package]] +name = "docx2txt" +version = "0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/07/4486a038624e885e227fe79111914c01f55aa70a51920ff1a7f2bd216d10/docx2txt-0.9.tar.gz", hash = "sha256:18013f6229b14909028b19aa7bf4f8f3d6e4632d7b089ab29f7f0a4d1f660e28", size = 3613 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/51/756e71bec48ece0ecc2a10e921ef2756e197dcb7e478f2b43673b6683902/docx2txt-0.9-py3-none-any.whl", hash = "sha256:e3718c0653fd6f2fcf4b51b02a61452ad1c38a4c163bcf0a6fd9486cd38f529a", size = 4025 }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 }, +] [[package]] name = "email-validator" @@ -850,33 +1111,45 @@ dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238 } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604 }, ] [[package]] -name = "esprima" -version = "4.0.1" +name = "et-xmlfile" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a1/50fccd68a12bcfc27adfc9969c090286670a9109a0259f3f70943390b721/esprima-4.0.1.tar.gz", hash = "sha256:08db1a876d3c2910db9cfaeb83108193af5411fc3a3a66ebefacd390d21323ee", size = 47021, upload-time = "2018-08-24T13:59:11.374Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, +] [[package]] -name = "et-xmlfile" -version = "2.0.0" +name = "exa-py" +version = "1.16.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +dependencies = [ + { name = "httpcore" }, + { name = "httpx" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b0/d035d8e56ca7d9192daf4a3bf4d4fc4a64d427d29df700868dd91ffc0a69/exa_py-1.16.2.tar.gz", hash = "sha256:1808a82ef9f25271ffd238d7b9e2ed279a96de89f140de401887866fc8b54ced", size = 41380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, + { url = "https://files.pythonhosted.org/packages/98/27/934d9750e4f3c888a0cc8e702c2424069abb7d86b67602229c44083c3d25/exa_py-1.16.2-py3-none-any.whl", hash = "sha256:7a574e978a50dd458b07e8bbb8b1da9543b8f8e57191b7bd1086546414686689", size = 56642 }, ] [[package]] name = "expandvars" version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/64/a9d8ea289d663a44b346203a24bf798507463db1e76679eaa72ee6de1c7a/expandvars-1.1.2.tar.gz", hash = "sha256:6c5822b7b756a99a356b915dd1267f52ab8a4efaa135963bd7f4bd5d368f71d7", size = 70842, upload-time = "2025-09-12T10:55:20.929Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/64/a9d8ea289d663a44b346203a24bf798507463db1e76679eaa72ee6de1c7a/expandvars-1.1.2.tar.gz", hash = "sha256:6c5822b7b756a99a356b915dd1267f52ab8a4efaa135963bd7f4bd5d368f71d7", size = 70842 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/e6/79c43f7a55264e479a9fbf21ddba6a73530b3ea8439a8bb7fa5a281721af/expandvars-1.1.2-py3-none-any.whl", hash = "sha256:d1652fe4e61914f5b88ada93aaedb396446f55ae4621de45c8cb9f66e5712526", size = 7526, upload-time = "2025-09-12T10:55:18.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e6/79c43f7a55264e479a9fbf21ddba6a73530b3ea8439a8bb7fa5a281721af/expandvars-1.1.2-py3-none-any.whl", hash = "sha256:d1652fe4e61914f5b88ada93aaedb396446f55ae4621de45c8cb9f66e5712526", size = 7526 }, ] [[package]] @@ -887,99 +1160,59 @@ dependencies = [ { name = "sortedcontainers" }, { name = "toolz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/17/43350d1b147510b3ebbb09fdc05109aab6de2b6483b7f2110bb043f44ffb/extratools-0.8.2.1.tar.gz", hash = "sha256:d1410f4ffb59a508a3ec4b9f801eb378c6ce051f70360001536824542d6deb7a", size = 25589, upload-time = "2018-12-02T22:22:22.967Z" } - -[[package]] -name = "faiss-cpu" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "packaging" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a2/b346d976c0adcdeab7d23bf6f58eef6d1c5c9c0bf919353923cf91553049/faiss_cpu-1.9.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:b0e9208a36da519dc2eb90e4c44c66a6812a5b68457582d8ed21d04e910e3d1f", size = 7661789, upload-time = "2024-10-08T07:06:26.259Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9f/f0a39439a938818f1add48bd7b79d4b8e12e60f2f0c1e4a0b37b295625e0/faiss_cpu-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6a4b2871057560020b83ad7bb5aaf3b97b64f980f9af2ca99ba34eeb4fe38bdf", size = 3215612, upload-time = "2024-10-08T07:06:28.359Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c4/5d83db571038082869265826f04b5d4f0a3249fcd2f0df736b35bae0e2c0/faiss_cpu-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f1dc3a42ea386f49a86a9d09a3e30a40fa2e678395df5c2f5706c3f26f06751", size = 3641782, upload-time = "2024-10-08T07:06:30.035Z" }, - { url = "https://files.pythonhosted.org/packages/51/b2/4f9abd2b859cef0e2332d3ff032e1973281fac1204fa8da14effc326f528/faiss_cpu-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2baeed5f1d8b006533c71184cc29065892647774a3df9c6f6dc31c1b694f57fa", size = 27474817, upload-time = "2024-10-08T07:06:32.581Z" }, - { url = "https://files.pythonhosted.org/packages/87/2b/850200d901fd0409232d9bcae26228ab9aadf4803bbcc38f93d6f81cab37/faiss_cpu-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:81d8fcb0ef92c9e7af2f7104e321895462681a598aff6d526a8da8272a61c1dd", size = 14862693, upload-time = "2024-10-08T07:06:35.073Z" }, - { url = "https://files.pythonhosted.org/packages/14/76/67475a3009c4aeccab462325144738fb62074ed6edbc9ab55cc9ddddc127/faiss_cpu-1.9.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:2ed784120f6be7a7cde90f507831e670b4edc94f20cc7955eef3ae5fba70d449", size = 7691461, upload-time = "2024-10-08T07:06:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/31/28/6ece7a2bf3a4f53b25533353baf47e42055f16004e79554f3fcd975569f2/faiss_cpu-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:358be27446389c9df374fba17221ae5e45a7a8c943c4c675f81814d6fb7c31b1", size = 3217821, upload-time = "2024-10-08T07:06:39.377Z" }, - { url = "https://files.pythonhosted.org/packages/e5/20/822dd24d168a56f1af0f2aa276b183b2176659c44644e67757015e8272b1/faiss_cpu-1.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a0b5ec546c7455cf526326194ace125199769ccbc90bb69b464cd4a26b7f4d", size = 3651805, upload-time = "2024-10-08T07:06:41.715Z" }, - { url = "https://files.pythonhosted.org/packages/3b/27/45be1b5e71feef7d22f65c409658d698655a8c86547d20448388521a6697/faiss_cpu-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f03a4882e27c71ead60d84d06263d3f8592c842f0f469eeaf7883cfd4f2bfa", size = 27471710, upload-time = "2024-10-08T07:06:44.653Z" }, - { url = "https://files.pythonhosted.org/packages/51/d6/4228ddef6324abb7b6a62cf0dd72938147234770b1f04adcb23fa1a424bb/faiss_cpu-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:39a163c2c3c33df10b82fd3b61cb6c8bd7884e2526f1393de32ed71814c5cbfb", size = 14864760, upload-time = "2024-10-08T07:06:47.875Z" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/30/17/43350d1b147510b3ebbb09fdc05109aab6de2b6483b7f2110bb043f44ffb/extratools-0.8.2.1.tar.gz", hash = "sha256:d1410f4ffb59a508a3ec4b9f801eb378c6ce051f70360001536824542d6deb7a", size = 25589 } [[package]] name = "fastapi" -version = "0.115.14" +version = "0.135.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, -] - -[[package]] -name = "fastjsonschema" -version = "2.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999 }, ] [[package]] name = "fastuuid" version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, - { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, - { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, - { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, - { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, - { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, - { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, - { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, - { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720 }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024 }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679 }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862 }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278 }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788 }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819 }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546 }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921 }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559 }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539 }, ] [[package]] name = "filelock" version = "3.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, ] [[package]] name = "filetype" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020 } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 }, ] [[package]] @@ -991,9 +1224,9 @@ dependencies = [ { name = "pycodestyle" }, { name = "pyflakes" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922 }, ] [[package]] @@ -1004,82 +1237,82 @@ dependencies = [ { name = "flake8" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/1d/635e86f9f3a96b7ea9e9f19b5efe17a987e765c39ca496e4a893bb999112/flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a", size = 4756, upload-time = "2023-03-21T20:51:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1d/635e86f9f3a96b7ea9e9f19b5efe17a987e765c39ca496e4a893bb999112/flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a", size = 4756 }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, ] [[package]] name = "fonttools" -version = "4.60.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, - { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, - { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, - { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, - { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, - { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, - { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062 }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594 }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840 }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801 }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009 }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892 }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313 }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299 }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562 }, ] [[package]] name = "frozenlist" version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, ] [[package]] name = "fsspec" version = "2025.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966 }, ] [package.optional-dependencies] @@ -1094,9 +1327,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, ] [[package]] @@ -1106,19 +1339,49 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 }, ] [[package]] -name = "google-search-results" -version = "2.4.0" +name = "google-auth" +version = "2.55.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/1c/70b23fc52b2bb3c70b379f3bd05c4a60ab3a873e30c6bd21c57e0154848a/google_auth-2.55.0.tar.gz", hash = "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb", size = 349379 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/71/c0321dc6d63d99946da45f7c06299b934e4f7f7da5c4f14d101bcb39adf1/google_auth-2.55.0-py3-none-any.whl", hash = "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", size = 252400 }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/52/0244e310812f3063d09d60b30ae29ab7df9343bd005744cd5eeaa6ba39b4/google_genai-2.8.0.tar.gz", hash = "sha256:37a9b3cb127d763e7f4ca47452ae3562c87728773bd1b149f7b559c239da2bc1", size = 564955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/de/747ad1aa49e902da9a4699081c282a1ed8ceed3b4d295fd99a6d286e09e4/google_genai-2.8.0-py3-none-any.whl", hash = "sha256:4da0a223a100f4b37f609a68b835e3326ab0fa313314dc0fd9d34e76ee293844", size = 832497 }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/35/d60b766cf8ffbbb86c701dc2042e360d224180ba32056b7a5f56bfab2697/google_search_results-2.4.0.tar.gz", hash = "sha256:fa3305468c7ecaff3d39b38810455a4256404f4c01fa90e31dc352b798df0ac5", size = 10104, upload-time = "2021-07-26T16:58:32.324Z" } [[package]] name = "googleapis-common-protos" @@ -1127,150 +1390,199 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, ] [[package]] name = "graphql-core" version = "3.2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484, upload-time = "2025-11-01T22:30:40.436Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" }, + { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262 }, ] [[package]] name = "greenlet" version = "3.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, - { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, - { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, - { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, - { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, - { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, - { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, - { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, - { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, - { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, - { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, - { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, - { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, - { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814 }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073 }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191 }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516 }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169 }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497 }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662 }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210 }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759 }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288 }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685 }, ] [[package]] -name = "grpc-interceptor" -version = "0.15.4" +name = "griffe" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "grpcio" }, + { name = "griffecli" }, + { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/28/57449d5567adf4c1d3e216aaca545913fbc21a915f2da6790d6734aac76e/grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926", size = 19322, upload-time = "2023-11-16T02:05:42.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/49/eb6d2935e27883af92c930ed40cc4c69bcd32c402be43b8ca4ab20510f67/griffe-2.0.2.tar.gz", hash = "sha256:c5d56326d159f274492e9bf93a9895cec101155d944caa66d0fc4e0c13751b92", size = 293757 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/ac/8d53f230a7443401ce81791ec50a3b0e54924bf615ad287654fa4a2f5cdc/grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d", size = 20848, upload-time = "2023-11-16T02:05:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/94/c0/2bb018eecf9a83c68db9cd9fffd9dab25f102ad30ed869451046e46d1187/griffe-2.0.2-py3-none-any.whl", hash = "sha256:2b31816460aee1996af26050a1fc6927a2e5936486856707f55508e4c9b5960b", size = 5141 }, ] [[package]] -name = "grpcio" -version = "1.76.0" +name = "griffecli" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "colorama" }, + { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, - { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, - { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, - { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, - { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, - { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, - { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, - { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, - { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, - { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, - { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, - { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, - { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/e0/6a7d661d71bb043656a109b91d84a42b5342752542074ec83b16a6eb97f0/griffecli-2.0.2.tar.gz", hash = "sha256:40a1ad4181fc39685d025e119ae2c5b669acdc1f19b705fb9bf971f4e6f6dffb", size = 56281 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e8/90d93356c88ac34c20cb5edffca68138df55ca9bbd1a06eccfbcec8fdbe5/griffecli-2.0.2-py3-none-any.whl", hash = "sha256:0d44d39e59afa81e288a3e1c3bf352cc4fa537483326ac06b8bb6a51fd8303a0", size = 9500 }, ] [[package]] -name = "h11" -version = "0.16.0" +name = "griffelib" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357 }, ] [[package]] -name = "hf-xet" -version = "1.2.0" +name = "grpc-interceptor" +version = "0.15.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +dependencies = [ + { name = "grpcio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/28/57449d5567adf4c1d3e216aaca545913fbc21a915f2da6790d6734aac76e/grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926", size = 19322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, + { url = "https://files.pythonhosted.org/packages/15/ac/8d53f230a7443401ce81791ec50a3b0e54924bf615ad287654fa4a2f5cdc/grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d", size = 20848 }, ] [[package]] -name = "httpcore" +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 }, +] + +[[package]] +name = "grpclib" +version = "0.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h2" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/28/5a2c299ec82a876a252c5919aa895a6f1d1d35c96417c5ce4a4660dc3a80/grpclib-0.4.9.tar.gz", hash = "sha256:cc589c330fa81004c6400a52a566407574498cb5b055fa927013361e21466c46", size = 84798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/90/b0cbbd9efcc82816c58f31a34963071aa19fb792a212a5d9caf8e0fc3097/grpclib-0.4.9-py3-none-any.whl", hash = "sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e", size = 77063 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870 }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004 }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636 }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448 }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401 }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866 }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099 }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178 }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214 }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054 }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812 }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920 }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + +[[package]] +name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] name = "httptools" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, ] [[package]] @@ -1283,48 +1595,55 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] name = "httpx-sse" version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, ] [[package]] name = "huggingface-hub" -version = "1.1.5" +version = "0.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "shellingham" }, + { name = "requests" }, { name = "tqdm" }, - { name = "typer-slim" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/02/c3d534d7498ba2792da1d2ce56b5d38bbcbcbbba62071c90ee289b408e8d/huggingface_hub-1.1.5.tar.gz", hash = "sha256:40ba5c9a08792d888fde6088920a0a71ab3cd9d5e6617c81a797c657f1fd9968", size = 607199, upload-time = "2025-11-20T15:49:32.809Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/54/096903f02ca14eb2670a4d11729da44a026c0bababec8c15f160441124c5/huggingface_hub-0.36.1.tar.gz", hash = "sha256:5a3b8bf87e182ad6f1692c196bb9ec9ade7755311d5d5e792dc45045f77283ad", size = 649681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/cb/8f5141b3c21d1ecdf87852506eb583fec497c7e9803a168fe4aec64252bb/huggingface_hub-0.36.1-py3-none-any.whl", hash = "sha256:c6fa8a8f7b8559bc624ebb7e218fb72171b30f6049ebe08f8bfc2a44b38ece50", size = 566283 }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/f4/124858007ddf3c61e9b144107304c9152fa80b5b6c168da07d86fe583cc1/huggingface_hub-1.1.5-py3-none-any.whl", hash = "sha256:e88ecc129011f37b868586bbcfae6c56868cae80cd56a79d61575426a3aa0d7d", size = 516000, upload-time = "2025-11-20T15:49:30.926Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, ] [[package]] @@ -1334,36 +1653,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, ] [[package]] -name = "iniconfig" -version = "2.3.0" +name = "importlib-resources" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232 }, ] [[package]] -name = "isort" -version = "5.12.0" +name = "iniconfig" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/c4/dc00e42c158fc4dda2afebe57d2e948805c06d5169007f1724f0683010a9/isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", size = 174643, upload-time = "2023-01-28T17:10:22.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/63/4036ae70eea279c63e2304b91ee0ac182f467f24f86394ecfe726092340b/isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6", size = 91198, upload-time = "2023-01-28T17:10:21.149Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] [[package]] -name = "itsdangerous" -version = "2.2.0" +name = "isort" +version = "5.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/c4/dc00e42c158fc4dda2afebe57d2e948805c06d5169007f1724f0683010a9/isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", size = 174643 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, + { url = "https://files.pythonhosted.org/packages/0a/63/4036ae70eea279c63e2304b91ee0ac182f467f24f86394ecfe726092340b/isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6", size = 91198 }, ] [[package]] @@ -1373,78 +1692,65 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "jiter" version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, ] [[package]] name = "jmespath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, ] [[package]] name = "joblib" version = "1.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396 }, ] [[package]] -name = "json5" -version = "0.12.1" +name = "joserfc" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/c3/2f590052b55cbdd0ace470ee7ee1f685f6882051be93a9374891005623e2/joserfc-1.7.0.tar.gz", hash = "sha256:4aced6ab0c47846f0a531402aec2419a874b91e918df9c4c9da8a82fb559d6c4", size = 232967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" }, + { url = "https://files.pythonhosted.org/packages/5b/83/b6b62a66a06ce872d9429a5eb5ee20b2002fd9c331b953c94381c1f7c9f9/joserfc-1.7.0-py3-none-any.whl", hash = "sha256:17e5d7a5a35e65442b05efc435a3d5d46696ffa2c8a2ed0eea6f63fc268e3224", size = 70387 }, ] [[package]] @@ -1454,30 +1760,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpointer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699 } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898 }, ] [[package]] name = "jsonpath-ng" -version = "1.7.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ply" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838, upload-time = "2024-10-11T15:41:42.404Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105, upload-time = "2024-11-20T17:58:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844 }, ] [[package]] name = "jsonpointer" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, ] [[package]] @@ -1490,9 +1793,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, ] [[package]] @@ -1502,95 +1805,176 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, ] [[package]] -name = "jupyter-core" -version = "5.9.1" +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166 }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395 }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065 }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903 }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751 }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793 }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041 }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292 }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865 }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369 }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989 }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645 }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237 }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573 }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998 }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700 }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537 }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514 }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848 }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542 }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447 }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918 }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856 }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580 }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018 }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804 }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482 }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328 }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410 }, +] + +[[package]] +name = "knowledge-layer" +version = "1.0.0" +source = { editable = "aiq/sources/knowledge_layer" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dotenv" }, +] + +[package.optional-dependencies] +all = [ + { name = "chromadb" }, + { name = "docx2txt" }, + { name = "llama-index" }, + { name = "llama-index-embeddings-nvidia" }, + { name = "llama-index-vector-stores-chroma" }, + { name = "openai" }, + { name = "pdfplumber" }, + { name = "pillow" }, + { name = "pypdfium2" }, + { name = "python-pptx" }, + { name = "requests" }, + { name = "urllib3" }, +] + +[package.metadata] +requires-dist = [ + { name = "chromadb", marker = "extra == 'llamaindex'", specifier = ">=0.4.0" }, + { name = "docx2txt", marker = "extra == 'foundational-rag'", specifier = ">=0.8" }, + { name = "docx2txt", marker = "extra == 'llamaindex'", specifier = ">=0.8" }, + { name = "httpx", specifier = ">=0.24.0" }, + { name = "knowledge-layer", extras = ["llamaindex", "foundational-rag"], marker = "extra == 'all'", editable = "aiq/sources/knowledge_layer" }, + { name = "llama-index", marker = "extra == 'llamaindex'", specifier = ">=0.10.0" }, + { name = "llama-index-embeddings-nvidia", marker = "extra == 'llamaindex'", specifier = ">=0.1.0" }, + { name = "llama-index-vector-stores-chroma", marker = "extra == 'llamaindex'", specifier = ">=0.1.0" }, + { name = "openai", marker = "extra == 'llamaindex'", specifier = ">=1.0.0" }, + { name = "pdfplumber", marker = "extra == 'llamaindex'", specifier = ">=0.10.0" }, + { name = "pillow", marker = "extra == 'llamaindex'", specifier = ">=9.0.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pypdfium2", marker = "extra == 'llamaindex'", specifier = ">=5.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "python-pptx", marker = "extra == 'foundational-rag'", specifier = ">=0.6.21" }, + { name = "requests", marker = "extra == 'foundational-rag'", specifier = ">=2.28.0" }, + { name = "urllib3", marker = "extra == 'foundational-rag'", specifier = ">=2.7.0,<3" }, +] +provides-extras = ["llamaindex", "foundational-rag", "all"] + +[[package]] +name = "kubernetes" +version = "36.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, + { name = "aiohttp" }, + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/57/8b538af5076bc3372949d76f70ba3449bdfe52f9e6488170fa5d4f7cbe70/kubernetes-36.0.2.tar.gz", hash = "sha256:03551fcb49cae1f708f63624041e37403545b7aaed10cbf54e2b01a37a5438e3", size = 2336738 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/5c160dbdef7123f8cc97fd8ece7e0198627a426a2a49614845e9086feb8d/kubernetes-36.0.2-py2.py3-none-any.whl", hash = "sha256:faf9b5241b58de0c4a5069f2a0ffc8ac06fece7215156cd3d3ba081a78a858b6", size = 4617568 }, ] [[package]] -name = "kiwisolver" -version = "1.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, - { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, - { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +name = "langchain" +version = "1.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/7c/651d0dc4913a7a892156c03dd343b99cfe19ee729e6911ab1f4fe7567b8b/langchain-1.3.9.tar.gz", hash = "sha256:9b14ef0db9ef314299ded858b22ca2a40b8f1b05c8c9cb6b82d53a53075fef00", size = 631514 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/55/3481619d21b9bdfbfda8680fba5cfc6cfe926789b8eaaad95353078cfa20/langchain-1.3.9-py3-none-any.whl", hash = "sha256:4af49ad1095799e4408b489fb79d4b8b49292453618b202d8a697fca59bb6871", size = 132873 }, ] [[package]] -name = "langchain" -version = "0.3.27" +name = "langchain-anthropic" +version = "1.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anthropic" }, { name = "langchain-core" }, - { name = "langchain-text-splitters" }, - { name = "langsmith" }, { name = "pydantic" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/f6/f4f7f3a56626fe07e2bb330feb61254dbdf06c506e6b59a536a337da51cf/langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62", size = 10233809, upload-time = "2025-07-24T14:42:32.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/f5/cd397b94aeed5fa0e8ab9595b9fb578ac99f424d42220defe6626e6a1a7b/langchain_anthropic-1.4.6.tar.gz", hash = "sha256:78942d4458d883b7d362438a095ed501ed84f44d402622404482481fc973b9da", size = 706540 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/d5/4861816a95b2f6993f1360cfb605aacb015506ee2090433a71de9cca8477/langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798", size = 1018194, upload-time = "2025-07-24T14:42:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/927dbbc5a1f5fea1a69adc2883f034cbd1430004e36f4eacd302d500393a/langchain_anthropic-1.4.6-py3-none-any.whl", hash = "sha256:dbd412a956b6b8b0716d9d8460ef71f834a6731cdbfc59e6160482a4a9fb5200", size = 51797 }, ] [[package]] name = "langchain-aws" -version = "0.2.35" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, { name = "langchain-core" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/7a/19a903725acbb1c4481dc0391b2551250bf4e04cbe5a891a55e09319772b/langchain_aws-0.2.35.tar.gz", hash = "sha256:45793a34fe45d365f4292cc768db74669ca24601d2c5da1ac6f44403750d70af", size = 120567, upload-time = "2025-10-02T23:59:57.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/1d/bb306951b1c394b7a27effb8eb6c9ee65dd77fcc4be7c20f76e3299a9e1e/langchain_aws-1.1.0.tar.gz", hash = "sha256:1e2f8570328eae4907c3cf7e900dc68d8034ddc865d9dc96823c9f9d8cccb901", size = 393899 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/92/1827652b4ed6d8ffaffe8b40be49a6889a9b3cb4b523fb56871691c48601/langchain_aws-0.2.35-py3-none-any.whl", hash = "sha256:8ddb10f3c29f6d52bcbaa4d7f4f56462acf01f608adc7c70f41e5a476899a6bc", size = 145620, upload-time = "2025-10-02T23:59:55.288Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/91b8d2a7570657b371382b45054142c54165a51706990a5c1b4cc40c0e9a/langchain_aws-1.1.0-py3-none-any.whl", hash = "sha256:8ec074615b42839e035354063717374c32c63f5028ef5221ba073fd5f3ef5e37", size = 152432 }, +] + +[[package]] +name = "langchain-classic" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langchain-text-splitters" }, + { name = "langsmith" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/4b/bd03518418ece4c13192a504449b58c28afee915dc4a6f4b02622458cb1b/langchain_classic-1.0.1.tar.gz", hash = "sha256:40a499684df36b005a1213735dc7f8dca8f5eb67978d6ec763e7a49780864fdc", size = 10516020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0f/eab87f017d7fe28e8c11fff614f4cdbfae32baadb77d0f79e9f922af1df2/langchain_classic-1.0.1-py3-none-any.whl", hash = "sha256:131d83a02bb80044c68fedc1ab4ae885d5b8f8c2c742d8ab9e7534ad9cda8e80", size = 1040666 }, ] [[package]] @@ -1604,94 +1988,185 @@ dependencies = [ { name = "langchain" }, { name = "langchain-core" }, { name = "langsmith" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "pydantic-settings" }, { name = "pyyaml" }, { name = "requests" }, { name = "sqlalchemy" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/49/2ff5354273809e9811392bc24bcffda545a196070666aef27bc6aacf1c21/langchain_community-0.3.31.tar.gz", hash = "sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81", size = 33241237, upload-time = "2025-10-07T20:17:57.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/49/2ff5354273809e9811392bc24bcffda545a196070666aef27bc6aacf1c21/langchain_community-0.3.31.tar.gz", hash = "sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81", size = 33241237 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/0a/b8848db67ad7c8d4652cb6f4cb78d49b5b5e6e8e51d695d62025aa3f7dbc/langchain_community-0.3.31-py3-none-any.whl", hash = "sha256:1c727e3ebbacd4d891b07bd440647668001cea3e39cbe732499ad655ec5cb569", size = 2532920, upload-time = "2025-10-07T20:17:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0a/b8848db67ad7c8d4652cb6f4cb78d49b5b5e6e8e51d695d62025aa3f7dbc/langchain_community-0.3.31-py3-none-any.whl", hash = "sha256:1c727e3ebbacd4d891b07bd440647668001cea3e39cbe732499ad655ec5cb569", size = 2532920 }, ] [[package]] name = "langchain-core" -version = "0.3.80" +version = "1.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, + { name = "langchain-protocol" }, { name = "langsmith" }, { name = "packaging" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "tenacity" }, { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/2b/fffaff399d20a56d40b9562fa19701e91abd72d8c9d9bc8c2673077b56b6/langchain_core-1.4.7.tar.gz", hash = "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", size = 952522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3e/dcdffa60078ae7b3a00ebb4cbbf1a204a14c3609983c604886523a7d4418/langchain_core-1.4.7-py3-none-any.whl", hash = "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d", size = 554941 }, +] + +[[package]] +name = "langchain-exa" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exa-py" }, + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/69/f956c20fb8a73185063f8c54b559fc65562939a25bb58a16ad97ec8640d3/langchain_exa-1.1.0.tar.gz", hash = "sha256:25a782c310b55475d4b40d50c2ed15a9d7b1e6c413fb958ebb570bba372bee7e", size = 107388 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/d0/cfce61b53b79974a4883ff7675fe516192ae715e0f925e063e5869b6a929/langchain_exa-1.1.0-py3-none-any.whl", hash = "sha256:7eb4e1b6004f74fe278467470a4e76ba3b74a17df13f64e64745f0780f63d845", size = 7972 }, +] + +[[package]] +name = "langchain-google-genai" +version = "4.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/49/f76647b7ba1a6f9c11b0343056ab4d3e5fc445981d205237fed882b2ad60/langchain_core-0.3.80.tar.gz", hash = "sha256:29636b82513ab49e834764d023c4d18554d3d719a185d37b019d0a8ae948c6bb", size = 583629, upload-time = "2025-11-19T22:23:18.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/4b/a1acdba3a86f861d379cb654f234d334c04a4c93178c8c7b0182ddeb9966/langchain_google_genai-4.2.5.tar.gz", hash = "sha256:2abab4be22699a9cc29948b2bf012946f51a0bbf10ab3a4a9a129047234829f8", size = 271850 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/e8/e7a090ebe37f2b071c64e81b99fb1273b3151ae932f560bb94c22f191cde/langchain_core-0.3.80-py3-none-any.whl", hash = "sha256:2141e3838d100d17dce2359f561ec0df52c526bae0de6d4f469f8026c5747456", size = 450786, upload-time = "2025-11-19T22:23:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/6a/82/3d4d3dc181ea1756f323dad4d5936239c2f404ea0acb5102316224280634/langchain_google_genai-4.2.5-py3-none-any.whl", hash = "sha256:289699ddb8e1076a76144f83e25e0086e4ce629b196fc103251f2a629e0756e5", size = 69404 }, +] + +[[package]] +name = "langchain-huggingface" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "langchain-core" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/e8/4068ad02179253f55958e59e442e5b6e8cb95ffc5e805cc4db0b1ef61d4e/langchain_huggingface-1.2.2.tar.gz", hash = "sha256:1dd91ec415190d2704e93ec149618e3145075863ba37e74afc9080d685dc2743", size = 255513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/ed/648b87f9b67153ade616f360bf4145b76ed428b4adb89525938f611e9828/langchain_huggingface-1.2.2-py3-none-any.whl", hash = "sha256:f94944b0c0d5afc687568d426c87ed5236907464c41e72108ed76eee1a690f6d", size = 31926 }, ] [[package]] name = "langchain-litellm" -version = "0.2.3" +version = "0.6.6" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cryptography" }, + { name = "httpx" }, { name = "langchain-core" }, { name = "litellm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/8f/08032033cd4bdff1d177d6a9e3a1021e47c4c63fd1d8c564af6f3c7e9f8d/langchain_litellm-0.2.3.tar.gz", hash = "sha256:0e11687373ae6a99efee5a04d3a76de4fab0e1459edc0e84adb6f60ca76ebf79", size = 10829, upload-time = "2025-09-25T11:01:41.295Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/70/e1f42ceccd5fdad78ed7fd1a150c12661b63b92647d768168cd9efcdcf15/langchain_litellm-0.6.6.tar.gz", hash = "sha256:fb4399ae4c239b5bb85c19574a5bb4c17988433d48ec716e62144f0dad4a63af", size = 346225 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/26/271b1dad80b39a0e9df7ab13f63fa3fad52ce8288ddf73dec32a2212219f/langchain_litellm-0.2.3-py3-none-any.whl", hash = "sha256:422254b8742893aed6380f5ee73e6ae77869b218758edd0888d14ebd2c439352", size = 11571, upload-time = "2025-09-25T11:01:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d0/adda5508115c28d78b408cc8f6b1e3fa3b2cc83a551b62cd613bb705535c/langchain_litellm-0.6.6-py3-none-any.whl", hash = "sha256:d49d0353254e10e38c351d7eae3d7b34128a0d31a3a22928ac289a8987c21a30", size = 26393 }, ] [[package]] name = "langchain-milvus" -version = "0.2.1" +version = "0.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "pymilvus" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/dd/5e8b7f6f17da0e54205956feab3f7856cb7dc821dbe817f2990aa028e4cc/langchain_milvus-0.2.1.tar.gz", hash = "sha256:6e60e43959464ae2be9dadceb4fab6b3ddcec5bb1f2d29e898924f1c2651baf1", size = 32639, upload-time = "2025-06-28T09:59:53.826Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/21/ecce785a24e61ba2c0f6249a5a68b969ccc053342f933aeab31a3f885f5e/langchain_milvus-0.3.3.tar.gz", hash = "sha256:406c2d88da133741f5cc3e2fea4b36386182b35500205c70d003382ded210e41", size = 35577 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/b1/54e176cc8ac80df9a2c4ee9f726d6383fcf9818317c68532cfc90fa91b6c/langchain_milvus-0.2.1-py3-none-any.whl", hash = "sha256:faabf4685c15ef9651605172427073d6ffc52c0f36f3b88842977db883062c99", size = 36110, upload-time = "2025-06-28T09:59:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/be/5d/6a0dac51ca2343332d5de9c79686d54f905d225b173a8e1b03ae6d35982a/langchain_milvus-0.3.3-py3-none-any.whl", hash = "sha256:6e12f15453372dd48836978faa4a149de79c721df3322229ad732a5e628e8e97", size = 38962 }, +] + +[[package]] +name = "langchain-modal" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deepagents" }, + { name = "modal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/a7/4c65b5204837dd1d26b93aeb01768d053bf6f92988f476842b7b9a5bf61c/langchain_modal-0.0.5.tar.gz", hash = "sha256:acd3ee16264ad97d0040f20b7c711948cf90bbf1fd791e9effdf2be3c82f38bf", size = 190600 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/94/ce0d8423c85d61422f00022852185296bcd43c98e7bc2ba9075a1dcf6adc/langchain_modal-0.0.5-py3-none-any.whl", hash = "sha256:123d9b3ce781a3cf59d87e5297d1ea8608a1f2042327593321cb8ab6257d8599", size = 4633 }, ] [[package]] name = "langchain-nvidia-ai-endpoints" -version = "0.3.19" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "filetype" }, { name = "langchain-core" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/26/cae8babfecb9b7b503dbf5d6c0d98df74149c3151247d20e7bbfd17ca461/langchain_nvidia_ai_endpoints-1.4.1.tar.gz", hash = "sha256:8835f7e56d559b370b87164f937c1eb048ab837f25de91598f00555a705c2d16", size = 58092, upload-time = "2026-06-03T19:34:15.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/50/9dcb23c73270e775c38e8d264ccbf8b1b352dbac089bce3746756d895e46/langchain_nvidia_ai_endpoints-1.4.1-py3-none-any.whl", hash = "sha256:3edd1678a3e2c55789128e53ba32aab3dffe94cb201c70e6cea521fab7c261ff", size = 63203, upload-time = "2026-06-03T19:34:11.08Z" }, +] + +[[package]] +name = "langchain-oci" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "langchain" }, + { name = "langchain-core" }, + { name = "langchain-openai" }, + { name = "langgraph" }, + { name = "oci" }, + { name = "oci-openai" }, + { name = "openai" }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/0fcad6732f124c0d96fd7e8487a0b51db9f167964cfe041e4669a2148dbe/langchain_nvidia_ai_endpoints-0.3.19.tar.gz", hash = "sha256:1c420c88f7c78b2b2c2fdcef8e46104c2dd19c81e036bdd03a4838a6340950fe", size = 42884, upload-time = "2025-10-31T00:17:19.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/c7/a43f7b3b5a5b542bc17972bc9a95ee40fd7029aab98c9504ff2bf456b6ef/langchain_oci-0.2.6.tar.gz", hash = "sha256:92538d3ee45e3323290fcc672e3f6618b13878b464abd8692ade9b7441b5863b", size = 85514 } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/d7529004de384b2abc9e5b76cf4a84a23f3028ec6381bd5f7c00ac39bfab/langchain_nvidia_ai_endpoints-0.3.19-py3-none-any.whl", hash = "sha256:40161a71646fcbe457ac5f2222c5eadcbe31a7d79d618f5a0857c37fffa3a6d5", size = 46229, upload-time = "2025-10-31T00:17:18.306Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/92cc0ac1194ea285668b02ef2bcc39d04a846dc6e2e943b2a1f1e968d777/langchain_oci-0.2.6-py3-none-any.whl", hash = "sha256:3451385da788926d5cffd19de8afb912e15bdb28fb76f3844d3d88a5683142b0", size = 107591 }, ] [[package]] name = "langchain-openai" -version = "0.3.35" +version = "1.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/96/06d0d25a37e05a0ff2d918f0a4b0bf0732aed6a43b472b0b68426ce04ef8/langchain_openai-0.3.35.tar.gz", hash = "sha256:fa985fd041c3809da256a040c98e8a43e91c6d165b96dcfeb770d8bd457bf76f", size = 786635, upload-time = "2025-10-06T15:09:28.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b7/30bfc4d1b658a9ee524bcce3b0b2ec9c45a11c853a13c4f0c9da9882784b/langchain_openai-1.1.7.tar.gz", hash = "sha256:f5ec31961ed24777548b63a5fe313548bc6e0eb9730d6552b8c6418765254c81", size = 1039134 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/d5/c90c5478215c20ee71d8feaf676f7ffd78d0568f8c98bd83f81ce7562ed7/langchain_openai-0.3.35-py3-none-any.whl", hash = "sha256:76d5707e6e81fd461d33964ad618bd326cb661a1975cef7c1cb0703576bdada5", size = 75952, upload-time = "2025-10-06T15:09:27.137Z" }, + { url = "https://files.pythonhosted.org/packages/64/a1/50e7596aca775d8c3883eceeaf47489fac26c57c1abe243c00174f715a8a/langchain_openai-1.1.7-py3-none-any.whl", hash = "sha256:34e9cd686aac1a120d6472804422792bf8080a2103b5d21ee450c9e42d053815", size = 84753 }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/e7/8300ba22d968653051fd06e3117d783872dddf3dcebdd6b1d386836eb43c/langchain_protocol-0.0.16.tar.gz", hash = "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff", size = 5969 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/9c/06dfcc88d02a6364e8d864c421ddd3736305cb0a6c853f75c302c80fe17c/langchain_protocol-0.0.16-py3-none-any.whl", hash = "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3", size = 7037 }, ] [[package]] name = "langchain-tavily" -version = "0.2.13" +version = "0.2.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1699,26 +2174,26 @@ dependencies = [ { name = "langchain-core" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/75/a08ff73441fdaaef6e1ed83ee3b6747fd7c562ff772a09ebf5fe8fb252d3/langchain_tavily-0.2.13.tar.gz", hash = "sha256:97752167225b1459ffd2055bc8ffcec30658ff808dd5f7f9b2efa85ebbc7895a", size = 21842, upload-time = "2025-11-04T15:30:32.756Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/b309ef3062b189a82463dc93553804566e71aa393f9ba8954750793c1a6f/langchain_tavily-0.2.18.tar.gz", hash = "sha256:cd7859ae1a6ce79236580ef67072ff5fc43c7ded94e7eac38ff04209ca85a320", size = 25378 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/a2/7de8e9071b6a60cf9fe974744a2961e2dc77492d0ac29285a70b465cdb57/langchain_tavily-0.2.13-py3-none-any.whl", hash = "sha256:aa5b445c336f5f9314ab00f348554a42316af75ac839a0016442e382dd2cb1cb", size = 26025, upload-time = "2025-11-04T15:30:31.679Z" }, + { url = "https://files.pythonhosted.org/packages/71/9c/0c043e4434b1823f0ac194f66036cbb0569275a99dcb890e0891ecd34fb2/langchain_tavily-0.2.18-py3-none-any.whl", hash = "sha256:dccf3ad1c50e2cb2a89bec11727555805c9df8abd42c1f3ad42ccad86e28aa44", size = 30814 }, ] [[package]] name = "langchain-text-splitters" -version = "0.3.11" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/43/dcda8fd25f0b19cb2835f2f6bb67f26ad58634f04ac2d8eae00526b0fa55/langchain_text_splitters-0.3.11.tar.gz", hash = "sha256:7a50a04ada9a133bbabb80731df7f6ddac51bc9f1b9cab7fa09304d71d38a6cc", size = 46458, upload-time = "2025-08-31T23:02:58.316Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/42/c178dcdc157b473330eb7cc30883ea69b8ec60078c7b85e2d521054c4831/langchain_text_splitters-1.1.0.tar.gz", hash = "sha256:75e58acb7585dc9508f3cd9d9809cb14751283226c2d6e21fb3a9ae57582ca22", size = 272230 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/0d/41a51b40d24ff0384ec4f7ab8dd3dcea8353c05c973836b5e289f1465d4f/langchain_text_splitters-0.3.11-py3-none-any.whl", hash = "sha256:cf079131166a487f1372c8ab5d0bfaa6c0a4291733d9c43a34a16ac9bcd6a393", size = 33845, upload-time = "2025-08-31T23:02:57.195Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/a84ed1c046deecf271356b0179c1b9fba95bfdaa6f934e1849dee26fad7b/langchain_text_splitters-1.1.0-py3-none-any.whl", hash = "sha256:f00341fe883358786104a5f881375ac830a4dd40253ecd42b4c10536c6e4693f", size = 34182 }, ] [[package]] name = "langgraph" -version = "0.6.11" +version = "1.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -1728,53 +2203,85 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/4d/8dfe5e0f9c69655dfb1f450922699ab683b3abbc038cfe38f769eaf871c2/langgraph-0.6.11.tar.gz", hash = "sha256:cd5373d0a59701ab39c9f8af33a33c5704553de815318387fa7f240511e0efd7", size = 492075, upload-time = "2025-10-21T00:04:14.608Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9d/7c9ebd17b95569122e2d2e641f535cf086c870d66bb8e59be33cdba856b3/langgraph-1.2.5.tar.gz", hash = "sha256:09a3bdec6fdb3228623fc78b6f69a1400d383f66348d0b04d0efb692022cc6ef", size = 712532 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/94/430f0341c5c2fe3e3b9f5ab2622f35e2bda12c4a7d655c519468e853d1b0/langgraph-0.6.11-py3-none-any.whl", hash = "sha256:49268de69d85b7db3da9e2ca582a474516421c1c44be5cff390416cfa6967faa", size = 155424, upload-time = "2025-10-21T00:04:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/a2/03/187281cf61845c5a9c397ae6cd9cd73bb54b39435e5575a7b83c853e5b76/langgraph-1.2.5-py3-none-any.whl", hash = "sha256:9286bb5def82fc865959c14378fe473518dc097d586225f622f029637a2a4bb9", size = 246150 }, ] [[package]] name = "langgraph-checkpoint" -version = "3.0.1" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/07/2b1c042fa87d40cf2db5ca27dc4e8dd86f9a0436a10aa4361a8982718ae7/langgraph_checkpoint-3.0.1.tar.gz", hash = "sha256:59222f875f85186a22c494aedc65c4e985a3df27e696e5016ba0b98a5ed2cee0", size = 137785, upload-time = "2025-11-04T21:55:47.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212 }, +] + +[[package]] +name = "langgraph-checkpoint-postgres" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langgraph-checkpoint" }, + { name = "orjson" }, + { name = "psycopg" }, + { name = "psycopg-pool" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/51/5a2dc42e8b5d5942b933b5b7237eae5a4dbc92508a04727c263dd383ad8a/langgraph_checkpoint_postgres-3.1.0.tar.gz", hash = "sha256:02bff4ab63d9dae8eab3a9640fce1d479da8965c9fba7b0dc04cb1f7c56f0a55", size = 148473 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/cd/eff9b82bc3b5f62d481b437099f44f3ef7b1d907f166fb4ee25e8f84a1e7/langgraph_checkpoint_postgres-3.1.0-py3-none-any.whl", hash = "sha256:814cce2ef35d792bf07b090a95eed004f1acac0724fe6605536b13f6d1e7032c", size = 48988 }, +] + +[[package]] +name = "langgraph-checkpoint-sqlite" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "langgraph-checkpoint" }, + { name = "sqlite-vec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ea/83917c2369acf8a10a894d4247655fd063c07924ba5bc4e83c85d2eaeded/langgraph_checkpoint_sqlite-3.1.0.tar.gz", hash = "sha256:f926916ebc1b985d802cc9c820026036e84db9d910d62c97b57e4ba64f67d5ae", size = 147902 } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/e3/616e3a7ff737d98c1bbb5700dd62278914e2a9ded09a79a1fa93cf24ce12/langgraph_checkpoint-3.0.1-py3-none-any.whl", hash = "sha256:9b04a8d0edc0474ce4eaf30c5d731cee38f11ddff50a6177eead95b5c4e4220b", size = 46249, upload-time = "2025-11-04T21:55:46.472Z" }, + { url = "https://files.pythonhosted.org/packages/97/07/b342811a16327900af2747c752ea19676172fcddf9b592cc384031076623/langgraph_checkpoint_sqlite-3.1.0-py3-none-any.whl", hash = "sha256:cc9b40df0076feae8a9ad42ae713621b148b00ac23adc09dc1dc66090a46e5ad", size = 38587 }, ] [[package]] name = "langgraph-prebuilt" -version = "0.6.5" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/6a/76ed0f0d740b187ac2014beae929658881b8d18291bd107571aae5515b12/langgraph_prebuilt-0.6.5.tar.gz", hash = "sha256:9c63e9e867e62b345805fd1e8ea5c2df5cc112e939d714f277af84f2afe5950d", size = 125791, upload-time = "2025-10-21T00:14:50.431Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/d1/e4727f4822943befc3b7046f79049b1086c9493a34b4d44a1adf78577693/langgraph_prebuilt-0.6.5-py3-none-any.whl", hash = "sha256:b6ceb5db31c16a30a3ee3c0b923667f02e7c9e27852621abf9d5bd5603534141", size = 28158, upload-time = "2025-10-21T00:14:49.192Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043 }, ] [[package]] name = "langgraph-sdk" -version = "0.2.9" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/d8/40e01190a73c564a4744e29a6c902f78d34d43dad9b652a363a92a67059c/langgraph_sdk-0.2.9.tar.gz", hash = "sha256:b3bd04c6be4fa382996cd2be8fbc1e7cc94857d2bc6b6f4599a7f2a245975303", size = 99802, upload-time = "2025-09-20T18:49:14.734Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327 } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/05/b2d34e16638241e6f27a6946d28160d4b8b641383787646d41a3727e0896/langgraph_sdk-0.2.9-py3-none-any.whl", hash = "sha256:fbf302edadbf0fb343596f91c597794e936ef68eebc0d3e1d358b6f9f72a1429", size = 56752, upload-time = "2025-09-20T18:49:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521 }, ] [[package]] name = "langsmith" -version = "0.4.45" +version = "0.8.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1783,16 +2290,19 @@ dependencies = [ { name = "pydantic" }, { name = "requests" }, { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/2b/a0d817e8212042786b483880428d4040983eb7ef6ea20bb812049b0d0675/langsmith-0.4.45.tar.gz", hash = "sha256:f73f86353e31e8f89da2da2b1ebf9f695227f394d22df3ef5d7618e59738bd71", size = 986697, upload-time = "2025-11-20T20:56:54.33Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/19/1ed2af9c6d5d7a148e6b3e809b0af8ce8848e1f66a0726c8223d30e5292b/langsmith-0.8.16.tar.gz", hash = "sha256:8c943f0c9185fe2a9637b5b442828b7efd823b1de28d50d14c136c79660f909b", size = 4513275 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/26/bc3e723564c4fdcb623c43a7fd54db958f9deea0ce0f8da8a63fbc10e903/langsmith-0.4.45-py3-none-any.whl", hash = "sha256:6326ad2e66c24d47361b4338fbc23e7fee8da624b289921a9c164e15217ed8e2", size = 411979, upload-time = "2025-11-20T20:56:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/c3/13/8186a9867c67f3fef9958a1d60b45f46c1a9b5d28f67d8fd136f28ceab3f/langsmith-0.8.16-py3-none-any.whl", hash = "sha256:081e57c0175d142192683288740a796eb0eb32d9e703b4bf9133678ceefe3286", size = 500303 }, ] [[package]] name = "litellm" -version = "1.80.0" +version = "1.87.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1808,18 +2318,176 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/8c/48d533affdbc6d485b7ad4221cd3b40b8c12f9f5568edfe0be0b11e7b945/litellm-1.80.0.tar.gz", hash = "sha256:eeac733eb6b226f9e5fb020f72fe13a32b3354b001dc62bcf1bc4d9b526d6231", size = 11591976, upload-time = "2025-11-16T00:03:51.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/0d/ccdf682ccfd7f18bf0e179c39d85616b8f8ef05a798588285310412db13d/litellm-1.87.0.tar.gz", hash = "sha256:cafc1882cb0cbab8374c41180af86e4a067796e4524e15f59e99f6e689cd1bd8", size = 15453755 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/53/aa31e4d057b3746b3c323ca993003d6cf15ef987e7fe7ceb53681695ae87/litellm-1.80.0-py3-none-any.whl", hash = "sha256:fd0009758f4772257048d74bf79bb64318859adb4ea49a8b66fdbc718cd80b6e", size = 10492975, upload-time = "2025-11-16T00:03:49.182Z" }, + { url = "https://files.pythonhosted.org/packages/98/20/88a372fa7e50fc2c33458c6eef94a79afcf7bdfa43610079531b82b484a3/litellm-1.87.0-py3-none-any.whl", hash = "sha256:fbbba7e47ae29b55f878fe1acc80effb92761bc168f6236bd81a0cb6e147d855", size = 17103948 }, +] + +[[package]] +name = "llama-index" +version = "0.14.22" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-llms-openai" }, + { name = "nltk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/89/3b6f3318ea2249158daab3ff22777ef5ffa87a63c011659a6cfc55e54c35/llama_index-0.14.22.tar.gz", hash = "sha256:c2c9b31f50d2815abdc191085db4acaf96b7c01851ac66b2e4cc82be8cde589e", size = 8565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/fd/f0837c4ce049d8ece7525bbf64564e93e3f16333856c2a0b47fecb58f317/llama_index-0.14.22-py3-none-any.whl", hash = "sha256:14b4bdd799112062e38288eab6aa16643f29d7532505ab174b0b6d5b0817fe94", size = 7115 }, +] + +[[package]] +name = "llama-index-core" +version = "0.14.22" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiosqlite" }, + { name = "banks" }, + { name = "dataclasses-json" }, + { name = "deprecated" }, + { name = "dirtyjson" }, + { name = "filetype" }, + { name = "fsspec" }, + { name = "httpx" }, + { name = "llama-index-workflows" }, + { name = "nest-asyncio" }, + { name = "networkx" }, + { name = "nltk" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "tinytag" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/7f/94a4b940ef0d069840df0fd6d361a2aa832a2dd73b4cecdf86e8f8c353c8/llama_index_core-0.14.22.tar.gz", hash = "sha256:1384410f89bdbd32349aab444ef4f5c828c338787bc65bd1ffd8e86dfb44ac41", size = 11584786 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/15/e1a26d8d56aa55fa07587a3e9c7e85294d2df5af6c2229193019bc549ef6/llama_index_core-0.14.22-py3-none-any.whl", hash = "sha256:9cfffde46fd5b7937101e1c0c9bb5c21bd7ff8c8a56937810b87ba3542f31225", size = 11920774 }, +] + +[[package]] +name = "llama-index-embeddings-nvidia" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/8c/a15b90fd688bf20296731130910a03479a4bb6b8ed22f023dd631181ddc4/llama_index_embeddings_nvidia-0.5.1.tar.gz", hash = "sha256:3a45af84a49b75721f33c05394a81b15def9850d062e5c46320a81632972c2d2", size = 8370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3f/7a73cbf25d6d64d073fa93bd55110f798603d4967e9c7e91b918074afd16/llama_index_embeddings_nvidia-0.5.1-py3-none-any.whl", hash = "sha256:ab6b1e8369f46ef73709354235fc5e5b976136be2e2c08b20e1b457544271742", size = 8425 }, +] + +[[package]] +name = "llama-index-embeddings-openai" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/52/eb56a4887501651fb17400f7f571c1878109ff698efbe0bbac9165a5603d/llama_index_embeddings_openai-0.6.0.tar.gz", hash = "sha256:eb3e6606be81cb89125073e23c97c0a6119dabb4827adbd14697c2029ad73f29", size = 7629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d1/4bb0b80f4057903110060f617ef519197194b3ff5dd6153d850c8f5676fa/llama_index_embeddings_openai-0.6.0-py3-none-any.whl", hash = "sha256:039bb1007ad4267e25ddb89a206dfdab862bfb87d58da4271a3919e4f9df4d61", size = 7666 }, +] + +[[package]] +name = "llama-index-instrumentation" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d0/671b23ccff255c9bce132a84ffd5a6f4541ceefdeab9c1786b08c9722f2e/llama_index_instrumentation-0.5.0.tar.gz", hash = "sha256:eeb724648b25d149de882a5ac9e21c5acb1ce780da214bda2b075341af29ad8e", size = 43831 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/45/6dcaccef44e541ffa138e4b45e33e0d40ab2a7d845338483954fcf77bc75/llama_index_instrumentation-0.5.0-py3-none-any.whl", hash = "sha256:aaab83cddd9dd434278891012d8995f47a3bc7ed1736a371db90965348c56a21", size = 16444 }, +] + +[[package]] +name = "llama-index-llms-openai" +version = "0.7.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/85/f07466d0f5c3f1e1f295a60f2d2e9d32163635ba39ae2fda22ce2fbac1c9/llama_index_llms_openai-0.7.9.tar.gz", hash = "sha256:f54a24b717134c86e724007057a06a84394f019d1f01e918b624894e208a86df", size = 27564 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/c2/cfa341cff00154b58abed3fc559fb43c2531d5dd70eefb75f230704d4c3c/llama_index_llms_openai-0.7.9-py3-none-any.whl", hash = "sha256:0bc8f59faddf8dbc9f90c5576127ab2fa6d368be5078885dc7e611af129b5a79", size = 28648 }, +] + +[[package]] +name = "llama-index-vector-stores-chroma" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chromadb" }, + { name = "llama-index-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/e7/19d0a81ccba54c5d0c5be46ac3a300ed86eb9e6638375d8b4ceb506bd629/llama_index_vector_stores_chroma-0.5.5.tar.gz", hash = "sha256:823865069bcea4d9de91510271ae03cd6ea8eb0106a5b5ea43ffcec03ec2a708", size = 8262 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/3e/eced8b70b63067bf1600faa003f5bcc11c67ce39fcb6efb9aa38ebcc3b05/llama_index_vector_stores_chroma-0.5.5-py3-none-any.whl", hash = "sha256:a1ca18abe2b6b90cd0bdff89fef9fb389bbd3dfb6fc74844bf9100d48d258874", size = 8078 }, +] + +[[package]] +name = "llama-index-workflows" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-instrumentation" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/89/0b97ed4b0cc70bb2f2ad87b9f52b165978e9a9a61da13a069e98863f6aca/llama_index_workflows-2.21.0.tar.gz", hash = "sha256:663db71c063c098038773a794dbc8172f99443e358f423d7c81836b674e09ab1", size = 108014 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/54/86d65239d589714f757539f4716fcf46963ebb1fcaea3c80ad4191388ce4/llama_index_workflows-2.21.0-py3-none-any.whl", hash = "sha256:72235adf71b41e46743af88367c7234e019e573eb02df7d7b3c022439e736970", size = 132673 }, ] [[package]] name = "locket" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350 } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398 }, +] + +[[package]] +name = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780 }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006 }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139 }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329 }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564 }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467 }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304 }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607 }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168 }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487 }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231 }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450 }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874 }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987 }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276 }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903 }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869 }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490 }, ] [[package]] @@ -1829,9 +2497,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509 }, ] [[package]] @@ -1841,39 +2509,39 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, ] [[package]] @@ -1883,60 +2551,56 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 }, ] [[package]] name = "matplotlib" -version = "3.10.7" +version = "3.10.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, { name = "cycler" }, { name = "fonttools" }, { name = "kiwisolver" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, - { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, - { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, - { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, - { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, - { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, - { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331 }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461 }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091 }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027 }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269 }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588 }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913 }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019 }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645 }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194 }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684 }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790 }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571 }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292 }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, ] [[package]] name = "mcp" -version = "1.19.0" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1945,95 +2609,142 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/2b/916852a5668f45d8787378461eaa1244876d77575ffef024483c94c0649c/mcp-1.19.0.tar.gz", hash = "sha256:213de0d3cd63f71bc08ffe9cc8d4409cc87acffd383f6195d2ce0457c021b5c1", size = 444163, upload-time = "2025-10-24T01:11:15.839Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/a3/3e71a875a08b6a830b88c40bc413bff01f1650f1efe8a054b5e90a9d4f56/mcp-1.19.0-py3-none-any.whl", hash = "sha256:f5907fe1c0167255f916718f376d05f09a830a215327a3ccdd5ec8a519f2e572", size = 170105, upload-time = "2025-10-24T01:11:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832 }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964 }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114 }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819 }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330 }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078 }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498 }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320 }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299 }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222 }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048 }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742 }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132 }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686 }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479 }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030 }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536 }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769 }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563 }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310 }, +] + +[[package]] +name = "modal" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "cbor2" }, + { name = "certifi" }, + { name = "click" }, + { name = "grpclib" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "synchronicity" }, + { name = "toml" }, + { name = "types-certifi" }, + { name = "types-toml" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/f9/87425e60db2a8597b248417772b409c49ca3a05ff6b1282a21cd7d856f09/modal-1.5.0.tar.gz", hash = "sha256:15033cf84f5f4f9f8a3dcf47a768cfcca36d1ad38ab7b3459fd3cbc29aa84a77", size = 771722 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/85e476e7d32c0a648d5aa97c4335ac02357d059c2bb734cf175b08446597/modal-1.5.0-py3-none-any.whl", hash = "sha256:9c5687eff775d1372bd70b87e43499e40777a1de160f23786c00807bf342fcb6", size = 882122 }, ] [[package]] name = "msgpack" version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212 }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315 }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721 }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657 }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668 }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040 }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037 }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631 }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118 }, ] [[package]] name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135 }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117 }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472 }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342 }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082 }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704 }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355 }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259 }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903 }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365 }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062 }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683 }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254 }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967 }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085 }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713 }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915 }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077 }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114 }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442 }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885 }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588 }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966 }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618 }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539 }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345 }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934 }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243 }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878 }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452 }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312 }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935 }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385 }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777 }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104 }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503 }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 }, ] [[package]] @@ -2043,184 +2754,187 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503, upload-time = "2025-04-17T03:11:27.742Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503 } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/4d/9af0d1279c84618bcd35bf5fd7e371657358c7b0a523e54a9cffb87461f8/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6", size = 144695, upload-time = "2025-04-17T03:11:09.161Z" }, - { url = "https://files.pythonhosted.org/packages/17/bf/87323e79dd0562474fad3373c21c66bc6c3c9963b68eb2a209deb4c8575e/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3", size = 144742, upload-time = "2025-04-17T03:11:10.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/74/cb8c831e58dc6d5cf450b17c7db87f14294a1df52eb391da948b5e0a0b94/multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797", size = 144745, upload-time = "2025-04-17T03:11:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948, upload-time = "2025-04-17T03:11:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462, upload-time = "2025-04-17T03:11:21.657Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287, upload-time = "2025-04-17T03:11:22.69Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636, upload-time = "2025-04-17T03:11:24.936Z" }, - { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478, upload-time = "2025-04-17T03:11:26.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948 }, + { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462 }, + { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287 }, + { url = "https://files.pythonhosted.org/packages/ee/25/7d7e78e750bc1aecfaf0efbf826c69a791d2eeaf29cf20cba93ff4cced78/multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334", size = 151917 }, + { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636 }, + { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478 }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] [[package]] -name = "nbformat" -version = "5.10.4" +name = "narwhals" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastjsonschema" }, - { name = "jsonschema" }, - { name = "jupyter-core" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951 }, ] [[package]] -name = "nemollm" -version = "0.3.5" +name = "nest-asyncio" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "requests-futures" }, - { name = "requests-toolbelt" }, - { name = "tqdm" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/c1/0975d3e7b3293fc4480fe6521ddfcba2a02da131f7751c295430c797e5f8/nemollm-0.3.5-py3-none-any.whl", hash = "sha256:de3962d82d557ce5f3d773144361b897a181028b8e1bbdf94804eff865eead88", size = 11573, upload-time = "2024-01-05T19:46:57.009Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, ] [[package]] -name = "nest-asyncio" -version = "1.6.0" +name = "nest-asyncio2" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/eb/ecf8bbf9d22a4e8f7be1628336fe0202da7660790053aa28abeb6c15eb14/nest_asyncio2-1.7.1.tar.gz", hash = "sha256:a1fe5bbbd20894dcceb1842322d74992c5834d5ab692af2c4f59a9a4fcf75fe8", size = 13797 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, + { url = "https://files.pythonhosted.org/packages/8c/48/c1f1ddcfd04bba60470235c2f83733ecff43ebe068dc7715aab60bc92ad8/nest_asyncio2-1.7.1-py3-none-any.whl", hash = "sha256:f83bc1744c3cfa7d47fd29431e5e168db6cb76eda1bb20108955c32f60d7eddf", size = 7504 }, ] [[package]] name = "networkx" version = "3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, ] [[package]] -name = "numpy" -version = "1.26.4" +name = "nltk" +version = "3.9.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087 }, ] [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251 }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652 }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172 }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990 }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902 }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430 }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551 }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275 }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637 }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090 }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710 }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292 }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897 }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391 }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275 }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855 }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359 }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374 }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587 }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940 }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341 }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507 }, ] [[package]] name = "nvidia-nat" -version = "1.3.1" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aioboto3" }, + { name = "nvidia-nat-core" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/da/d80afea5cc635fe1befd4c952f6fef10b74754c7ece237b96fb4c2926f13/nvidia_nat-1.7.0-py3-none-any.whl", hash = "sha256:d7f36722d529a2e4660d7f153c150289dd45f6e4cf4feb65f26fc26090b3f29a", size = 49724 }, +] + +[package.optional-dependencies] +async-endpoints = [ + { name = "nvidia-nat-core", extra = ["async-endpoints"] }, +] +eval = [ + { name = "nvidia-nat-eval", extra = ["full"] }, +] +langchain = [ + { name = "nvidia-nat-langchain" }, +] +mcp = [ + { name = "nvidia-nat-mcp" }, +] +phoenix = [ + { name = "nvidia-nat-phoenix" }, +] +profiler = [ + { name = "nvidia-nat-profiler" }, +] + +[[package]] +name = "nvidia-nat-atif" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/fb/d50faa2e9cc0dc4a6a652bcca4bf3282ad98541b97c4469c9251293af2d7/nvidia_nat_atif-1.7.0-py3-none-any.whl", hash = "sha256:65d471a366dfafe75cf94428bdba3007bd3e1368487e2e16fd62484298635334", size = 105803 }, +] + +[[package]] +name = "nvidia-nat-core" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, { name = "authlib" }, { name = "click" }, { name = "colorama" }, - { name = "datasets" }, + { name = "cryptography" }, { name = "expandvars" }, { name = "fastapi" }, { name = "httpx" }, { name = "jinja2" }, { name = "jsonpath-ng" }, - { name = "mcp" }, - { name = "nest-asyncio" }, + { name = "nest-asyncio2" }, { name = "networkx" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "openinference-semantic-conventions" }, - { name = "openpyxl" }, - { name = "optuna" }, + { name = "numpy" }, + { name = "nvidia-nat-atif" }, + { name = "pandas" }, { name = "pip" }, { name = "pkce" }, { name = "pkginfo" }, { name = "platformdirs" }, { name = "pydantic" }, + { name = "pyjwt" }, { name = "pymilvus" }, { name = "python-dotenv" }, + { name = "python-multipart" }, { name = "pyyaml" }, - { name = "ragas" }, { name = "rich" }, + { name = "starlette" }, { name = "tabulate" }, + { name = "tzlocal" }, + { name = "urllib3" }, { name = "uvicorn", extra = ["standard"] }, - { name = "wikipedia" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/35/cdc2fcb7f675f4acc6c28cd35481d34189a0c9aa5061a28ab07a59fe4590/nvidia_nat-1.3.1-py3-none-any.whl", hash = "sha256:b14094368fc44f8f4d3df41a7846e86c69ead7a339dfbb72cda11e1ff91f7b25", size = 828895, upload-time = "2025-11-07T21:04:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/28/cf/634983c35ab78e410d0b625181d1b1dfad5a28cbddabd31ca8c33fa8ab68/nvidia_nat_core-1.7.0-py3-none-any.whl", hash = "sha256:fb4691ad3437e0e8b8d84256d36da18085ba1fc6dd47861e7a7e64c6a808390c", size = 808487 }, ] [package.optional-dependencies] @@ -2230,83 +2944,181 @@ async-endpoints = [ { name = "distributed" }, { name = "sqlalchemy", extra = ["asyncio"] }, ] -langchain = [ - { name = "nvidia-nat-langchain" }, -] -opentelemetry = [ - { name = "nvidia-nat-opentelemetry" }, + +[[package]] +name = "nvidia-nat-eval" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nat-atif" }, ] -phoenix = [ - { name = "nvidia-nat-phoenix" }, +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/64/98b02f25d20609b781f7af39ecaee7af1a991d41f443a82b98bebb418ad1/nvidia_nat_eval-1.7.0-py3-none-any.whl", hash = "sha256:724fa6410a7a66050b525d75660d7fb753ed83aba32c37d31111ae322d54a548", size = 69584 }, ] -profiling = [ - { name = "nvidia-nat-profiling" }, + +[package.optional-dependencies] +full = [ + { name = "aioboto3" }, + { name = "datasets" }, + { name = "nvidia-nat-core" }, + { name = "openpyxl" }, + { name = "tqdm" }, ] [[package]] name = "nvidia-nat-langchain" -version = "1.3.1" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "langchain-aws" }, + { name = "langchain-classic" }, + { name = "langchain-community" }, { name = "langchain-core" }, + { name = "langchain-exa" }, + { name = "langchain-huggingface" }, { name = "langchain-litellm" }, { name = "langchain-milvus" }, { name = "langchain-nvidia-ai-endpoints" }, + { name = "langchain-oci" }, { name = "langchain-openai" }, { name = "langchain-tavily" }, { name = "langgraph" }, - { name = "nvidia-nat" }, + { name = "nvidia-nat-core" }, + { name = "nvidia-nat-eval" }, + { name = "nvidia-nat-opentelemetry" }, + { name = "openevals" }, + { name = "pyopenssl" }, + { name = "wikipedia" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/92/75/e330e16fd09ebba4a945b4096a4fcb80f7f3159a604e5be1c14536572654/nvidia_nat_langchain-1.3.1-py3-none-any.whl", hash = "sha256:8b43a3f1afb37b164467dbbebf300570e430977218264f11f956d062e5637cc9", size = 59808, upload-time = "2025-11-07T20:45:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/b9/30/5c43dd14e0c5ba1a3e03001646542863a2a8560893ee7a4ba8d7a0a9faff/nvidia_nat_langchain-1.7.0-py3-none-any.whl", hash = "sha256:e00812f9d2c602bb59cc006081a5deef2e71cc46357e70f51503780196f9cc20", size = 197908 }, +] + +[[package]] +name = "nvidia-nat-mcp" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiorwlock" }, + { name = "mcp" }, + { name = "nvidia-nat-core" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/0a/b529c5cfc6d6d75d972d94ac2e51ab2bb59f7ec7fa768c514d0c0d50b647/nvidia_nat_mcp-1.7.0-py3-none-any.whl", hash = "sha256:ff8f51406925b46e543d1fa800f8795797abd4f38cd39ef75b378224313acd59", size = 132802 }, ] [[package]] name = "nvidia-nat-opentelemetry" -version = "1.3.1" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nat" }, + { name = "nvidia-nat-core" }, + { name = "openinference-semantic-conventions" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, { name = "opentelemetry-sdk" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d4/66c3c518efd610fa92ba10354618919307c2948aae206ccebc9cec37b64f/nvidia_nat_opentelemetry-1.3.1-py3-none-any.whl", hash = "sha256:68b93750f7a1f2ae167397f39aeef2d60de22fc8d7cd036ea7c8b3d6945e1780", size = 67025, upload-time = "2025-11-07T20:47:53.937Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/9ec9a91d9317158e200d424968d5ea49524fa6e0af85a6ec77f2a9f730d2/nvidia_nat_opentelemetry-1.7.0-py3-none-any.whl", hash = "sha256:e468fb4bd2a9e2fa4fb0d67daa1dea22b900c70301249d2785aa96f305808f7f", size = 68848 }, ] [[package]] name = "nvidia-nat-phoenix" -version = "1.3.1" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arize-phoenix-otel" }, - { name = "nvidia-nat", extra = ["opentelemetry"] }, + { name = "nvidia-nat-core" }, + { name = "nvidia-nat-opentelemetry" }, { name = "openinference-instrumentation" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/9c/684584394c08f0ffeee2bede2e0824248c74888f3e288c71002e04faa672/nvidia_nat_phoenix-1.3.1-py3-none-any.whl", hash = "sha256:dba9776f9daec6550885d7f2cb588c8ebf84ae0f04e740b311601d575250a41a", size = 53263, upload-time = "2025-11-07T21:00:38.499Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/d5f4e52f133cea2d79bf4101d8f9632c00fd8705831caf80d6ba9a65de2e/nvidia_nat_phoenix-1.7.0-py3-none-any.whl", hash = "sha256:312c00e61cb90db79e2b3e31a2a7f625e31423554fc03cdd6a2b2c97171b0c02", size = 67129 }, +] + +[[package]] +name = "nvidia-nat-profiler" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "nvidia-nat-core" }, + { name = "nvidia-nat-eval" }, + { name = "prefixspan" }, + { name = "scikit-learn" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/b8/77250ba90a5304f959329567989c8b5e09c654a39dee1a0f904e7c3f01ea/nvidia_nat_profiler-1.7.0-py3-none-any.whl", hash = "sha256:701b5704b8c34573e98a964c7a3ff6cf15755eda5554dbc866e5643a1d48798a", size = 108154 }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, +] + +[[package]] +name = "oci" +version = "2.177.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "circuitbreaker" }, + { name = "crc32c" }, + { name = "cryptography" }, + { name = "pyopenssl" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/eb/f4e9a840c2c703bf78f1ca8506514bb1195792715dcf52fbb92cab4a6cec/oci-2.177.0.tar.gz", hash = "sha256:941c15283677ec5ca65d82a4bc71bae28692d73e79abbaf5eccb305a0ddb1251", size = 17454232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a6/260b0e2ef9de7356030ea82dec56b61c40ac2907590f6aecaa9ce7b11059/oci-2.177.0-py3-none-any.whl", hash = "sha256:fd45a4a4d81764315123283f66cf33cc536ede3e3008dd37c871c89103012a81", size = 35629529 }, +] + +[[package]] +name = "oci-openai" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "oci" }, + { name = "openai" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/93/c395a92c8019dec50bd5760a9fcfd718cedb416824f8ebba0b26568ab6f4/oci_openai-1.1.0.tar.gz", hash = "sha256:1819ef7d17c1fdbe05c5c0653301fdca0d2fa99f6f8b1b7bd7667da9704d62a1", size = 199961 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/a5/48aa98c4b68f3cc55bf74ef7a8d691d230ff09d5fab521e17ffd8b155ac4/oci_openai-1.1.0-py3-none-any.whl", hash = "sha256:a028ee3e1a1b1ad4e0495b10ef70b81b5e6cd50e7f13cf485a112762641a9160", size = 11449 }, ] [[package]] -name = "nvidia-nat-profiling" -version = "1.3.1" +name = "onnxruntime" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib" }, - { name = "nvidia-nat" }, - { name = "prefixspan" }, - { name = "scikit-learn" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/0e/49792c03ac370dee5635bd8679f2d81a1437d908ee1bac73c1841503163d/nvidia_nat_profiling-1.3.1-py3-none-any.whl", hash = "sha256:02c629e379cace04fe4d35f4db9295b9cab06223bc8f90696983e094c8010946", size = 47923, upload-time = "2025-11-07T20:46:26.307Z" }, + { url = "https://files.pythonhosted.org/packages/fb/2b/54208fd03ad410480bc17edf4869376362da8bbf46fe186ddf4cb5cc20fe/onnxruntime-1.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:b3e5b58b8c89c2b20e086e890aa9527377e5c240dc3ecc1640d18e07705eeb1c", size = 18432958 }, + { url = "https://files.pythonhosted.org/packages/ce/88/24fc51fcbb126da6d032372314e47b55c3faad58f2aa78c0e199ccd20b9c/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b3d87eb560ff6a772240506f3c78d6d27c63cafedd5c775672e1194f968cfd", size = 16438180 }, + { url = "https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6872443f236a554921cda6f318c900e2d0c226792cf3534d00e5057c6926e5d2", size = 18658445 }, + { url = "https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:760021bca514d64a811837820d351a08a41741f16f8b4c26450da708fecf14e6", size = 13357856 }, + { url = "https://files.pythonhosted.org/packages/79/51/d1ec60ec7b1e2ae2d7340ba52b8a13529140039cd4407ba8dddbbc046582/onnxruntime-1.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:2fdfa9df40a0ded0028ce6f9cd863264237f3970559dea2b81456e9ac4622b94", size = 13104412 }, + { url = "https://files.pythonhosted.org/packages/5e/7d/e6bb1c6445c94f708c38cd8fbb7bf0264108c33498b9445c93e60fe6d329/onnxruntime-1.27.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54c0c4e9202c36c4ecdb1f3443f5dfbfd5ee3b54d1362c4b4c6134110e74fb32", size = 16443331 }, + { url = "https://files.pythonhosted.org/packages/72/1b/b18b31e806eabc41077810199fbbb36fbc2d5f19912416e5ccfbf73053d1/onnxruntime-1.27.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b215aa662c8f983f7d6dedafe65a9be72c26e5338e0fe98b3e0422c32c85428", size = 18670967 }, ] [[package]] name = "openai" -version = "2.8.1" +version = "2.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2318,9 +3130,24 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/9f/136562ec6c3b1a50fe06eb0bb34ed21f0d7426ec0140e5cc43ac785b69a5/openai-2.40.0.tar.gz", hash = "sha256:9a756f91f274a24ad6026cbcb2042fd356c8d4a10e8f347b08d34465e585f7a2", size = 781177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/46/180e14be801a75bc13f234cb1b594b232adeb9c84e60a9ab1832e8333591/openai-2.40.0-py3-none-any.whl", hash = "sha256:2b205637ff214477f9ce9ab035e9f494db0e3fa8f1e599008953735fbf6ff1ff", size = 1350935 }, +] + +[[package]] +name = "openevals" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-openai" }, + { name = "langsmith" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/37/31e23ef661fa4c3c6a3c979afd884b30205512b4dde680b36d5909550500/openevals-0.1.3.tar.gz", hash = "sha256:9b00df1a7738464676aa887d4d950b77d3ef7024f6e8a54be3a83c82f485ea65", size = 100828 } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" }, + { url = "https://files.pythonhosted.org/packages/0d/68/162b0d273ffef5b0ad557ebccb790725bf94d78969702324dd5726828cf0/openevals-0.1.3-py3-none-any.whl", hash = "sha256:aed448df0cfdded732e24cda026eda065435a71ffb8c406a3ce73e590156d9f9", size = 67802 }, ] [[package]] @@ -2333,9 +3160,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/d0/b19061a21fd6127d2857c77744a36073bba9c1502d1d5e8517b708eb8b7c/openinference_instrumentation-0.1.42.tar.gz", hash = "sha256:2275babc34022e151b5492cfba41d3b12e28377f8e08cb45e5d64fe2d9d7fe37", size = 23954, upload-time = "2025-11-05T01:37:46.869Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/d0/b19061a21fd6127d2857c77744a36073bba9c1502d1d5e8517b708eb8b7c/openinference_instrumentation-0.1.42.tar.gz", hash = "sha256:2275babc34022e151b5492cfba41d3b12e28377f8e08cb45e5d64fe2d9d7fe37", size = 23954 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/71/43ee4616fc95dbd2f560550f199c6652a5eb93f84e8aa0039bc95c19cfe0/openinference_instrumentation-0.1.42-py3-none-any.whl", hash = "sha256:e7521ff90833ef7cc65db526a2f59b76a496180abeaaee30ec6abbbc0b43f8ec", size = 30086, upload-time = "2025-11-05T01:37:43.866Z" }, + { url = "https://files.pythonhosted.org/packages/c3/71/43ee4616fc95dbd2f560550f199c6652a5eb93f84e8aa0039bc95c19cfe0/openinference_instrumentation-0.1.42-py3-none-any.whl", hash = "sha256:e7521ff90833ef7cc65db526a2f59b76a496180abeaaee30ec6abbbc0b43f8ec", size = 30086 }, ] [[package]] @@ -2350,18 +3177,18 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/18/0242af5d7ea1170287b4d1e89aa9a3bcd545788cf748b5494a19b9499b18/openinference_instrumentation_langchain-0.1.55.tar.gz", hash = "sha256:57a2875ca845950f944700d9352efb080c8b20049253408e7739d960c43f34e8", size = 68602, upload-time = "2025-11-10T21:45:29.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/18/0242af5d7ea1170287b4d1e89aa9a3bcd545788cf748b5494a19b9499b18/openinference_instrumentation_langchain-0.1.55.tar.gz", hash = "sha256:57a2875ca845950f944700d9352efb080c8b20049253408e7739d960c43f34e8", size = 68602 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5a/55a99fd64cc1c8c21b1c47211c7389af5a9cd98f75d83a21c8b97e6c1d33/openinference_instrumentation_langchain-0.1.55-py3-none-any.whl", hash = "sha256:3f755950254fdf7741e0327a585cb0609c6c5553109a2b52929c29ce2a02c2d3", size = 22195, upload-time = "2025-11-10T21:45:28.147Z" }, + { url = "https://files.pythonhosted.org/packages/22/5a/55a99fd64cc1c8c21b1c47211c7389af5a9cd98f75d83a21c8b97e6c1d33/openinference_instrumentation_langchain-0.1.55-py3-none-any.whl", hash = "sha256:3f755950254fdf7741e0327a585cb0609c6c5553109a2b52929c29ce2a02c2d3", size = 22195 }, ] [[package]] name = "openinference-semantic-conventions" version = "0.1.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/68/81c8a0b90334ff11e4f285e4934c57f30bea3ef0c0b9f99b65e7b80fae3b/openinference_semantic_conventions-0.1.25.tar.gz", hash = "sha256:f0a8c2cfbd00195d1f362b4803518341e80867d446c2959bf1743f1894fce31d", size = 12767, upload-time = "2025-11-05T01:37:45.89Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/68/81c8a0b90334ff11e4f285e4934c57f30bea3ef0c0b9f99b65e7b80fae3b/openinference_semantic_conventions-0.1.25.tar.gz", hash = "sha256:f0a8c2cfbd00195d1f362b4803518341e80867d446c2959bf1743f1894fce31d", size = 12767 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/3d/dd14ee2eb8a3f3054249562e76b253a1545c76adbbfd43a294f71acde5c3/openinference_semantic_conventions-0.1.25-py3-none-any.whl", hash = "sha256:3814240f3bd61f05d9562b761de70ee793d55b03bca1634edf57d7a2735af238", size = 10395, upload-time = "2025-11-05T01:37:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/dd14ee2eb8a3f3054249562e76b253a1545c76adbbfd43a294f71acde5c3/openinference_semantic_conventions-0.1.25-py3-none-any.whl", hash = "sha256:3814240f3bd61f05d9562b761de70ee793d55b03bca1634edf57d7a2735af238", size = 10395 }, ] [[package]] @@ -2371,9 +3198,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "et-xmlfile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, ] [[package]] @@ -2384,9 +3211,9 @@ dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947 }, ] [[package]] @@ -2397,9 +3224,9 @@ dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2d/16e3487ddde2dee702bd746dd41950a8789b846d22a1c7e64824aac5ebea/opentelemetry_exporter_otlp-1.38.0.tar.gz", hash = "sha256:2f55acdd475e4136117eff20fbf1b9488b1b0b665ab64407516e1ac06f9c3f9d", size = 6147, upload-time = "2025-10-16T08:35:52.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/2d/16e3487ddde2dee702bd746dd41950a8789b846d22a1c7e64824aac5ebea/opentelemetry_exporter_otlp-1.38.0.tar.gz", hash = "sha256:2f55acdd475e4136117eff20fbf1b9488b1b0b665ab64407516e1ac06f9c3f9d", size = 6147 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/8a/81cd252b16b7d95ec1147982b6af81c7932d23918b4c3b15372531242ddd/opentelemetry_exporter_otlp-1.38.0-py3-none-any.whl", hash = "sha256:bc6562cef229fac8887ed7109fc5abc52315f39d9c03fd487bb8b4ef8fbbc231", size = 7018, upload-time = "2025-10-16T08:35:32.995Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8a/81cd252b16b7d95ec1147982b6af81c7932d23918b4c3b15372531242ddd/opentelemetry_exporter_otlp-1.38.0-py3-none-any.whl", hash = "sha256:bc6562cef229fac8887ed7109fc5abc52315f39d9c03fd487bb8b4ef8fbbc231", size = 7018 }, ] [[package]] @@ -2409,9 +3236,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 }, ] [[package]] @@ -2427,9 +3254,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695 }, ] [[package]] @@ -2445,9 +3272,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579 }, ] [[package]] @@ -2460,9 +3287,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020 }, ] [[package]] @@ -2472,9 +3299,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535 }, ] [[package]] @@ -2486,9 +3313,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349 }, ] [[package]] @@ -2499,110 +3326,67 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 }, ] [[package]] -name = "optuna" -version = "4.4.0" +name = "orjson" +version = "3.11.9" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alembic" }, - { name = "colorlog" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/e0/b303190ae8032d12f320a24c42af04038bacb1f3b17ede354dd1044a5642/optuna-4.4.0.tar.gz", hash = "sha256:a9029f6a92a1d6c8494a94e45abd8057823b535c2570819072dbcdc06f1c1da4", size = 467708, upload-time = "2025-06-16T05:13:00.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/5e/068798a8c7087863e7772e9363a880ab13fe55a5a7ede8ec42fab8a1acbb/optuna-4.4.0-py3-none-any.whl", hash = "sha256:fad8d9c5d5af993ae1280d6ce140aecc031c514a44c3b639d8c8658a8b7920ea", size = 395949, upload-time = "2025-06-16T05:12:58.37Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458 }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368 }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070 }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892 }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217 }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980 }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738 }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033 }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492 }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087 }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031 }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915 }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613 }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086 }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696 }, ] [[package]] -name = "ordered-set" -version = "4.1.0" +name = "ormsgpack" +version = "1.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/67/d5ef41c3b4a94400be801984ef7c7fc9623e1a82b643e74eeec367e7462b/ormsgpack-1.12.0.tar.gz", hash = "sha256:94be818fdbb0285945839b88763b269987787cb2f7ef280cad5d6ec815b7e608", size = 49959 } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e8/35f11ce9313111488b26b3035e4cbe55caa27909c0b6c8b5b5cd59f9661e/ormsgpack-1.12.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:766f2f3b512d85cd375b26a8b1329b99843560b50b93d3880718e634ad4a5de5", size = 369574 }, + { url = "https://files.pythonhosted.org/packages/61/b0/77461587f412d4e598d3687bafe23455ed0f26269f44be20252eddaa624e/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84b285b1f3f185aad7da45641b873b30acfd13084cf829cf668c4c6480a81583", size = 195893 }, + { url = "https://files.pythonhosted.org/packages/c6/67/e197ceb04c3b550589e5407fc9fdae10f4e2e2eba5fdac921a269e02e974/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e23604fc79fe110292cb365f4c8232e64e63a34f470538be320feae3921f271b", size = 206503 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/7fa8ba82a25cef678983c7976f85edeef5014f5c26495f338258e6a3cf1c/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc32b156c113a0fae2975051417d8d9a7a5247c34b2d7239410c46b75ce9348a", size = 208257 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/759e999390000d2589e6d0797f7265e6ec28378547075d28d3736248ab63/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94ac500dd10c20fa8b8a23bc55606250bfe711bf9716828d9f3d44dfd1f25668", size = 377852 }, + { url = "https://files.pythonhosted.org/packages/51/e7/0af737c94272494d9d84a3c29cc42c973ef7fd2342917020906596db863c/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c5201ff7ec24f721f813a182885a17064cffdbe46b2412685a52e6374a872c8f", size = 471456 }, + { url = "https://files.pythonhosted.org/packages/f4/ba/c81f0aa4f19fbf457213395945b672e6fde3ce777e3587456e7f0fca2147/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a9740bb3839c9368aacae1cbcfc474ee6976458f41cc135372b7255d5206c953", size = 381813 }, + { url = "https://files.pythonhosted.org/packages/ce/15/429c72d64323503fd42cc4ca8398930ded8aa8b3470df8a86b3bbae7a35c/ormsgpack-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ed37f29772432048b58174e920a1d4c4cde0404a5d448d3d8bbcc95d86a6918", size = 112949 }, + { url = "https://files.pythonhosted.org/packages/55/b9/e72c451a40f8c57bfc229e0b8e536ecea7203c8f0a839676df2ffb605c62/ormsgpack-1.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:b03994bbec5d6d42e03d6604e327863f885bde67aa61e06107ce1fa5bdd3e71d", size = 106689 }, ] [[package]] -name = "orjson" -version = "3.11.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/1d/1ea6005fffb56715fd48f632611e163d1604e8316a5bad2288bee9a1c9eb/orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", size = 243498, upload-time = "2025-10-24T15:48:48.101Z" }, - { url = "https://files.pythonhosted.org/packages/37/d7/ffed10c7da677f2a9da307d491b9eb1d0125b0307019c4ad3d665fd31f4f/orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", size = 128961, upload-time = "2025-10-24T15:48:49.571Z" }, - { url = "https://files.pythonhosted.org/packages/a2/96/3e4d10a18866d1368f73c8c44b7fe37cc8a15c32f2a7620be3877d4c55a3/orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", size = 130321, upload-time = "2025-10-24T15:48:50.713Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1f/465f66e93f434f968dd74d5b623eb62c657bdba2332f5a8be9f118bb74c7/orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", size = 129207, upload-time = "2025-10-24T15:48:52.193Z" }, - { url = "https://files.pythonhosted.org/packages/28/43/d1e94837543321c119dff277ae8e348562fe8c0fafbb648ef7cb0c67e521/orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", size = 136323, upload-time = "2025-10-24T15:48:54.806Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/93303776c8890e422a5847dd012b4853cdd88206b8bbd3edc292c90102d1/orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", size = 137440, upload-time = "2025-10-24T15:48:56.326Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ef/75519d039e5ae6b0f34d0336854d55544ba903e21bf56c83adc51cd8bf82/orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", size = 136680, upload-time = "2025-10-24T15:48:57.476Z" }, - { url = "https://files.pythonhosted.org/packages/b5/18/bf8581eaae0b941b44efe14fee7b7862c3382fbc9a0842132cfc7cf5ecf4/orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", size = 136160, upload-time = "2025-10-24T15:48:59.631Z" }, - { url = "https://files.pythonhosted.org/packages/c4/35/a6d582766d351f87fc0a22ad740a641b0a8e6fc47515e8614d2e4790ae10/orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", size = 140318, upload-time = "2025-10-24T15:49:00.834Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/5a4801803ab2e2e2d703bce1a56540d9f99a9143fbec7bf63d225044fef8/orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", size = 406330, upload-time = "2025-10-24T15:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/80/55/a8f682f64833e3a649f620eafefee175cbfeb9854fc5b710b90c3bca45df/orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", size = 149580, upload-time = "2025-10-24T15:49:03.517Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e4/c132fa0c67afbb3eb88274fa98df9ac1f631a675e7877037c611805a4413/orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", size = 139846, upload-time = "2025-10-24T15:49:04.761Z" }, - { url = "https://files.pythonhosted.org/packages/54/06/dc3491489efd651fef99c5908e13951abd1aead1257c67f16135f95ce209/orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", size = 135781, upload-time = "2025-10-24T15:49:05.969Z" }, - { url = "https://files.pythonhosted.org/packages/79/b7/5e5e8d77bd4ea02a6ac54c42c818afb01dd31961be8a574eb79f1d2cfb1e/orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", size = 131391, upload-time = "2025-10-24T15:49:07.355Z" }, - { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" }, - { url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571, upload-time = "2025-10-24T15:49:10.008Z" }, - { url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891, upload-time = "2025-10-24T15:49:11.297Z" }, - { url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137, upload-time = "2025-10-24T15:49:12.544Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152, upload-time = "2025-10-24T15:49:13.754Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834, upload-time = "2025-10-24T15:49:15.307Z" }, - { url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519, upload-time = "2025-10-24T15:49:16.557Z" }, - { url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749, upload-time = "2025-10-24T15:49:17.796Z" }, - { url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325, upload-time = "2025-10-24T15:49:19.347Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204, upload-time = "2025-10-24T15:49:20.545Z" }, - { url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242, upload-time = "2025-10-24T15:49:21.884Z" }, - { url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013, upload-time = "2025-10-24T15:49:23.185Z" }, - { url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951, upload-time = "2025-10-24T15:49:24.428Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049, upload-time = "2025-10-24T15:49:25.973Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461, upload-time = "2025-10-24T15:49:27.259Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167, upload-time = "2025-10-24T15:49:28.511Z" }, -] - -[[package]] -name = "ormsgpack" -version = "1.12.0" +name = "overrides" +version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/67/d5ef41c3b4a94400be801984ef7c7fc9623e1a82b643e74eeec367e7462b/ormsgpack-1.12.0.tar.gz", hash = "sha256:94be818fdbb0285945839b88763b269987787cb2f7ef280cad5d6ec815b7e608", size = 49959, upload-time = "2025-11-04T18:30:10.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/ba/3cae83cf36420c1c8dd294f16c852c03313aafe2439a165c4c6ac611b1d0/ormsgpack-1.12.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c40d86d77391b18dd34de5295e3de2b8ad818bcab9c9def4121c8ec5c9714ae4", size = 369159, upload-time = "2025-11-04T18:29:27.057Z" }, - { url = "https://files.pythonhosted.org/packages/97/d4/5e176309e01a8b9098d80201aac1eb7db9336c3b5b4fa6254a2bbb0d0fa0/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:777b7fab364dc0f200bb382a98a385c8222ffa6a2333d627d763797326202c86", size = 195744, upload-time = "2025-11-04T18:29:28.069Z" }, - { url = "https://files.pythonhosted.org/packages/4f/83/6d80c8c5571639c000a39f38f77752dfaf9d9e552d775331e8d280f66a4e/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b5089ad9dd5b3d3013b245a55e4abaea2f8ad70f4a78e1b002127b02340004", size = 206474, upload-time = "2025-11-04T18:29:29.034Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e6/940311e48dc0cfc3e212bd7007a21ed0825158638057687d804f2c5c2cca/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaf0c87cace7bc08fbf68c5cc66605b593df6427e9f4de235b2da358787e008", size = 207959, upload-time = "2025-11-04T18:29:30.315Z" }, - { url = "https://files.pythonhosted.org/packages/1a/e3/fbe94b0a311815343b86a95a0627e4901b11ff6fd522679ca29a2a88c99b/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f62d476fe28bc5675d9aff30341bfa9f41d7de332c5b63fbbe9aaf6bb7ec74d4", size = 377666, upload-time = "2025-11-04T18:29:31.38Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/229cfa28076798ffb619aaa854b842de3f2ed5ea4e6509bf34d14c038c4d/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ded7810095b887e28434f32f5a345d354e88cf851bab3c5435aeb86a718618d2", size = 471394, upload-time = "2025-11-04T18:29:32.521Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bd/4eae4ab35586e4175c07acb5f98aec83aa9d8987f71ea0443aa900191bdf/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f72a1dea0c4ae7c4101dcfbe8133f274a9d769d0b87fe5188db4fab07ffabaee", size = 381506, upload-time = "2025-11-04T18:29:33.533Z" }, - { url = "https://files.pythonhosted.org/packages/dd/51/f9d56d6d015cbfa1ce9a4358ca30a41744644f0cf606e060d7203efe5af8/ormsgpack-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f479bfef847255d7d0b12c7a198f6a21490155da2da3062e082ba370893d4a1", size = 112707, upload-time = "2025-11-04T18:29:34.898Z" }, - { url = "https://files.pythonhosted.org/packages/f4/07/bb189ef7072979f2f96e8716e952172efdce9c54930aa0814bec73aee19b/ormsgpack-1.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:3583ca410e4502144b2594170542e4bbef7b15643fd1208703ae820f11029036", size = 106533, upload-time = "2025-11-04T18:29:36.112Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f2/c1036b2775fcc0cfa5fd618c53bcd3b862ee07298fb627f03af4c7982f84/ormsgpack-1.12.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0c1e08b64d99076fee155276097489b82cc56e8d5951c03c721a65a32f44494", size = 369538, upload-time = "2025-11-04T18:29:37.125Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ca/526c4ae02f3cb34621af91bf8282a10d666757c2e0c6ff391ff5d403d607/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd43bcb299131690b8e0677af172020b2ada8e625169034b42ac0c13adf84aa", size = 195872, upload-time = "2025-11-04T18:29:38.34Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0f/83bb7968e9715f6a85be53d041b1e6324a05428f56b8b980dac866886871/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0149d595341e22ead340bf281b2995c4cc7dc8d522a6b5f575fe17aa407604", size = 206469, upload-time = "2025-11-04T18:29:39.749Z" }, - { url = "https://files.pythonhosted.org/packages/02/e3/9e93ca1065f2d4af035804a842b1ff3025bab580c7918239bb225cd1fee2/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19a1b27d169deb553c80fd10b589fc2be1fc14cee779fae79fcaf40db04de2b", size = 208273, upload-time = "2025-11-04T18:29:40.769Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d8/6d6ef901b3a8b8f3ab8836b135a56eb7f66c559003e251d9530bedb12627/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f28896942d655064940dfe06118b7ce1e3468d051483148bf02c99ec157483a", size = 377839, upload-time = "2025-11-04T18:29:42.092Z" }, - { url = "https://files.pythonhosted.org/packages/4c/72/fcb704bfa4c2c3a37b647d597cc45a13cffc9d50baac635a9ad620731d29/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9396efcfa48b4abbc06e44c5dbc3c4574a8381a80cb4cd01eea15d28b38c554e", size = 471446, upload-time = "2025-11-04T18:29:43.133Z" }, - { url = "https://files.pythonhosted.org/packages/84/f8/402e4e3eb997c2ee534c99bec4b5bb359c2a1f9edadf043e254a71e11378/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96586ed537a5fb386a162c4f9f7d8e6f76e07b38a990d50c73f11131e00ff040", size = 381783, upload-time = "2025-11-04T18:29:44.466Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8d/5897b700360bc00911b70ae5ef1134ee7abf5baa81a92a4be005917d3dfd/ormsgpack-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e70387112fb3870e4844de090014212cdcf1342f5022047aecca01ec7de05d7a", size = 112943, upload-time = "2025-11-04T18:29:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/5b/44/1e73649f79bb96d6cf9e5bcbac68b6216d238bba80af351c4c0cbcf7ee15/ormsgpack-1.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:d71290a23de5d4829610c42665d816c661ecad8979883f3f06b2e3ab9639962e", size = 106688, upload-time = "2025-11-04T18:29:46.411Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] @@ -2610,28 +3394,26 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, ] [[package]] @@ -2642,9 +3424,36 @@ dependencies = [ { name = "locket" }, { name = "toolz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905 }, +] + +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094 } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252 }, +] + +[[package]] +name = "pdfplumber" +version = "0.11.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pypdfium2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/56/6f450312ba05a27d7713b73857c1a25100dbda04fbc1331b13fb227a607d/pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57", size = 102892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/9a/07d658e1e7fad860f1c541ab941348125dbdab773be3a0afaf32361866c7/pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580", size = 60047 }, ] [[package]] @@ -2655,100 +3464,87 @@ dependencies = [ { name = "cryptography" }, { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/4a/49f87efd9c320d25b2edda4d9bda520607fbc12338cd1030468d830f29bb/pgpy13-0.6.1rc1.tar.gz", hash = "sha256:398d71b8c5b0852c79c7b4598c0c98f7458f39b439b62068cfb49b19caaf5912", size = 603304, upload-time = "2025-03-01T22:27:18.879Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/4a/49f87efd9c320d25b2edda4d9bda520607fbc12338cd1030468d830f29bb/pgpy13-0.6.1rc1.tar.gz", hash = "sha256:398d71b8c5b0852c79c7b4598c0c98f7458f39b439b62068cfb49b19caaf5912", size = 603304 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/de/8e43636f1244989fb733513fa7c1252a3f82feda4ee861ec1b6377f2e6e7/pgpy13-0.6.1rc1-py3-none-any.whl", hash = "sha256:c8d6cf3dea0cc23373ff1c4ed79a6a4bf39216bf54970bae7cca656003d1aa4f", size = 84589, upload-time = "2025-03-01T22:27:17.242Z" }, + { url = "https://files.pythonhosted.org/packages/4f/de/8e43636f1244989fb733513fa7c1252a3f82feda4ee861ec1b6377f2e6e7/pgpy13-0.6.1rc1-py3-none-any.whl", hash = "sha256:c8d6cf3dea0cc23373ff1c4ed79a6a4bf39216bf54970bae7cca656003d1aa4f", size = 84589 }, ] [[package]] name = "pillow" -version = "12.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, - { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, - { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, - { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, - { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, - { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, - { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, - { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, - { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, - { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, - { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, - { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837 }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528 }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401 }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094 }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402 }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005 }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669 }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194 }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423 }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667 }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580 }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896 }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266 }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508 }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927 }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624 }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252 }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550 }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114 }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667 }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966 }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241 }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592 }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542 }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765 }, ] [[package]] name = "pip" version = "25.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014 } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622 }, ] [[package]] name = "pkce" version = "1.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/ea/ddd845c2ec21bf1e8555c782b32dc39b82f0b12764feb9f73ccbb2470f13/pkce-1.0.3.tar.gz", hash = "sha256:9775fd76d8a743d39b87df38af1cd04a58c9b5a5242d5a6350ef343d06814ab6", size = 2757, upload-time = "2021-02-08T18:29:07.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/ea/ddd845c2ec21bf1e8555c782b32dc39b82f0b12764feb9f73ccbb2470f13/pkce-1.0.3.tar.gz", hash = "sha256:9775fd76d8a743d39b87df38af1cd04a58c9b5a5242d5a6350ef343d06814ab6", size = 2757 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/51/52c22ec0812d25f5bf297a01153604bfa7bfa59ed66f6cd8345beb3c2b2a/pkce-1.0.3-py3-none-any.whl", hash = "sha256:55927e24c7d403b2491ebe182b95d9dcb1807643243d47e3879fbda5aad4471d", size = 3200, upload-time = "2021-02-08T18:29:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/15/51/52c22ec0812d25f5bf297a01153604bfa7bfa59ed66f6cd8345beb3c2b2a/pkce-1.0.3-py3-none-any.whl", hash = "sha256:55927e24c7d403b2491ebe182b95d9dcb1807643243d47e3879fbda5aad4471d", size = 3200 }, ] [[package]] name = "pkginfo" version = "1.12.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b", size = 451828, upload-time = "2025-02-19T15:27:37.188Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b", size = 451828 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343", size = 32717, upload-time = "2025-02-19T15:27:33.071Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343", size = 32717 }, ] [[package]] name = "platformdirs" version = "4.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632 } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651 }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "ply" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] @@ -2759,132 +3555,248 @@ dependencies = [ { name = "docopt" }, { name = "extratools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/b0/e66e9f6e07a0b37aa0f5703c46f54bafbdf65dfba63994247676b19076c4/prefixspan-0.5.2.tar.gz", hash = "sha256:8fbd2f94b3a7f4399d04f9bd6aa214b830fb7828799c472cd43dda10b03f671c", size = 10404, upload-time = "2018-09-29T06:12:27.708Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/b0/e66e9f6e07a0b37aa0f5703c46f54bafbdf65dfba63994247676b19076c4/prefixspan-0.5.2.tar.gz", hash = "sha256:8fbd2f94b3a7f4399d04f9bd6aa214b830fb7828799c472cd43dda10b03f671c", size = 10404 } [[package]] name = "prometheus-client" version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 }, ] [[package]] name = "propcache" version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, ] [[package]] name = "protobuf" version = "6.33.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432 } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, - { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, - { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, - { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, - { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, + { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593 }, + { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883 }, + { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522 }, + { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445 }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161 }, + { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171 }, + { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477 }, ] [[package]] name = "psutil" version = "7.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751 }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368 }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134 }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904 }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642 }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518 }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359 }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261 }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635 }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633 }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608 }, +] + +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001 }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377 }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023 }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423 }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137 }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671 }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601 }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513 }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243 }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347 }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393 }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592 }, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023 }, ] [[package]] name = "pyarrow" version = "22.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, - { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, - { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629 }, + { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999 }, + { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601 }, + { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050 }, + { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877 }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099 }, + { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685 }, + { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158 }, + { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060 }, + { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395 }, + { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216 }, + { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552 }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504 }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901 }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807 }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932 }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394 }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078 }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158 }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672 }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244 }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620 }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930 }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425 }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327 }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294 }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858 }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629 }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448 }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841 }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910 }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335 }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486 }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978 }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684 }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832 }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175 }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497 }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076 }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317 }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534 }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399 }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959 }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874 }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572 }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636 }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193 }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655 }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471 }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119 }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791 }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701 }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965 }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207 }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505 }, ] [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594 }, ] [[package]] name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, ] [[package]] @@ -2897,9 +3809,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 }, ] [[package]] @@ -2909,52 +3821,22 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, ] [[package]] @@ -2966,9 +3848,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880 }, ] [[package]] @@ -2982,27 +3864,41 @@ dependencies = [ { name = "six" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/46/2d67a4bf8aec626bc5aec883d0f613e4fffa49cbbd93e8c2fdf17689a21d/pydpkg-1.9.4.tar.gz", hash = "sha256:e55cb6b7c38c4af3c2d935ea9766378857c404951c951df7add02993039be316", size = 17908, upload-time = "2025-03-01T22:40:16.311Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/46/2d67a4bf8aec626bc5aec883d0f613e4fffa49cbbd93e8c2fdf17689a21d/pydpkg-1.9.4.tar.gz", hash = "sha256:e55cb6b7c38c4af3c2d935ea9766378857c404951c951df7add02993039be316", size = 17908 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/43/16fb8a3ed0d09446810babd95d2e341de6a265aacf96e4d845b7143457b6/pydpkg-1.9.4-py3-none-any.whl", hash = "sha256:2b2060861fcdce86c8aa31fefddfe97f8c87ecfe205c5fc6db128326c0d8c78c", size = 16796, upload-time = "2025-03-01T22:40:13.716Z" }, + { url = "https://files.pythonhosted.org/packages/c7/43/16fb8a3ed0d09446810babd95d2e341de6a265aacf96e4d845b7143457b6/pydpkg-1.9.4-py3-none-any.whl", hash = "sha256:2b2060861fcdce86c8aa31fefddfe97f8c87ecfe205c5fc6db128326c0d8c78c", size = 16796 }, ] [[package]] name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551 }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, +] + +[[package]] +name = "pyjwt" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, ] [[package]] @@ -3018,16 +3914,17 @@ dependencies = [ { name = "platformdirs" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/9d/81c84a312d1fa8133b0db0c76148542a98349298a01747ab122f9314b04e/pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a", size = 1525946, upload-time = "2025-10-05T18:41:43.786Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/9d/81c84a312d1fa8133b0db0c76148542a98349298a01747ab122f9314b04e/pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a", size = 1525946 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/a7/69460c4a6af7575449e615144aa2205b89408dc2969b87bc3df2f262ad0b/pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7", size = 523465, upload-time = "2025-10-05T18:41:41.766Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a7/69460c4a6af7575449e615144aa2205b89408dc2969b87bc3df2f262ad0b/pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7", size = 523465 }, ] [[package]] name = "pymilvus" -version = "2.6.3" +version = "2.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cachetools" }, { name = "grpcio" }, { name = "orjson" }, { name = "pandas" }, @@ -3035,27 +3932,86 @@ dependencies = [ { name = "python-dotenv" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/c9/d83e22ae440f2331769e15f4d55e42af39215a04a412b0513a4e30a94089/pymilvus-2.6.3.tar.gz", hash = "sha256:10647808f201003d24eead2890722ffcba4b1ef713eff86c8da9623f93071ed9", size = 1343434, upload-time = "2025-10-31T06:26:07.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/0c/92adff800a04cd3e9b3f17c06fa972c8d590846b1e0bac0ccf39e054b596/pymilvus-2.6.9.tar.gz", hash = "sha256:c53a3d84ff15814e251be13edda70a98a1c8a6090d7597a908387cbb94a9504a", size = 1493560 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/56/ab7f0a5aba6fc06dc210a059d6f6d2ee1f3371d40e2b4366a409576554b8/pymilvus-2.6.9-py3-none-any.whl", hash = "sha256:3e14e8072f6429dcd79d52a24dc021c594cb80841ddd76cb974bc539d1f4cdda", size = 301225 }, +] + +[[package]] +name = "pyopenssl" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/33/b0309aefe3d2046262c6174dd5765449064e31ae364839f4eb572777f44e/pymilvus-2.6.3-py3-none-any.whl", hash = "sha256:c8551491a194ecfb0b22d44aa809ce749bf6969f2fa8d321523833e2d6c1313d", size = 273801, upload-time = "2025-10-31T06:26:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823 }, ] [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781 }, +] + +[[package]] +name = "pypdfium2" +version = "5.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/78/d9b45abb97a3686643f7c6472a5f7688f2013a373226121dc76b9debbacf/pypdfium2-5.10.1.tar.gz", hash = "sha256:f257d2011eb43c846b7e9f5a802e28646b29732763e4a35dd6ca76f9be580538", size = 272963 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/49/0b85fcc0d236582143a25cf275b0b4f5d786f51eb07a89ec9c79d43efe18/pypdfium2-5.10.1-py3-none-android_23_arm64_v8a.whl", hash = "sha256:13abf7a9f5e0ddebc8bbcccea5f13ae5abe8a298ea219e125b0fc24c1d2171b4", size = 3409176 }, + { url = "https://files.pythonhosted.org/packages/20/d2/2f522c5b2ad5166edf256bb4dbab97de5d07b2573f8a5630ddfcd1f8d4ee/pypdfium2-5.10.1-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:a0dc52b56631e2f7edcdb22bae3b155aa840bec32c5bd05781e90baaecee88e7", size = 2866175 }, + { url = "https://files.pythonhosted.org/packages/a7/be/477548c026c2badfdbf4afc3358b7135121fb5bec2e2effcb67e3a674d0b/pypdfium2-5.10.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:ebb9e63f92d15fc41b359fe7a187233dfae37548800e1fa09cb2fc466ac89951", size = 3621427 }, + { url = "https://files.pythonhosted.org/packages/8c/9b/0131c7f711b62c6edd6b200e9eb6340be6de4f6dc5baae625e3394d8d5fb/pypdfium2-5.10.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:d04f2050b6b32bb18624688b600543342a4ab3aacf64bf66521a5af72bbc7de1", size = 3682825 }, + { url = "https://files.pythonhosted.org/packages/9d/3d/bddfceb6e67e54d6dd1ab6c0f1feff796a89596e40f6345a3b4cc6a3d408/pypdfium2-5.10.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de53d2710ca9509fc2812340acc57c3f043697609068592d87de654f8cabf44", size = 3682206 }, + { url = "https://files.pythonhosted.org/packages/58/08/dedeb25c6645fc8a5eda24f54e0b1d083b7334ababf5f518bb939d729cc0/pypdfium2-5.10.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10a26ce04795f8ec079e81c707fcb5737061e8a78025babc3d6e36642e9c903a", size = 3413720 }, + { url = "https://files.pythonhosted.org/packages/cc/48/69e1fc8b1216005243c6415183bbf6de1cda3f5a06758b3fa4a26a7385c6/pypdfium2-5.10.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be6d2a8d1bcfd777188e7aa55a25c83f34d54e8350ad8810fb32017787d9b0d9", size = 3812913 }, + { url = "https://files.pythonhosted.org/packages/72/7f/132455a58ad736d76815c6cd1307532c3f433299d945b9d2f8cc2387c309/pypdfium2-5.10.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf4d2527f79f31c550490cc74c9f32e19385a944630a3ef4cd4d9b6f961fbf77", size = 4223220 }, + { url = "https://files.pythonhosted.org/packages/bf/8c/4d5804eca598bbe894e0a9a510807e221c1623e7276ce6b68fa2660dc933/pypdfium2-5.10.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ba127750bc3f4161461538d532d74491cd976f584f1753a6cee9cb821338ec1", size = 3738950 }, + { url = "https://files.pythonhosted.org/packages/d1/7f/baac59bf14ff914d97789ee0368c22ae233aa857aa9c0726bcb515dbc4d7/pypdfium2-5.10.1-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80f30517ee089dfbbc6e9de6da365b6e8c0ce8f80c40b7d4025a3b1d3bbd8a70", size = 4029869 }, + { url = "https://files.pythonhosted.org/packages/d0/d7/a5d58a0bcba31a0e37ed636a76ef3d2d215733f28af61637287d061b0c54/pypdfium2-5.10.1-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad5f5de15febb788c6eb3853e58aceda9cd8c5187c92472abaeecf9558deb0cf", size = 3990927 }, + { url = "https://files.pythonhosted.org/packages/da/1a/98eebd14b36812176297cf765d504ebeebc982f895c8a3a9fbd2717797de/pypdfium2-5.10.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8947fa3cd808da33960bdf8ff9e5247aa94ccd94f0b09bb3402e99498d83bcc9", size = 4989624 }, + { url = "https://files.pythonhosted.org/packages/1c/20/f2e124d607b8bb90a9f1ce976afff38c70e215cd7ea86af784cb2e8a19dd/pypdfium2-5.10.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:139a6387a3a2652f288e53164268bb03af4fa221d78484ee18407053a60082a3", size = 4535124 }, + { url = "https://files.pythonhosted.org/packages/59/ef/469ea87f668a32ff3280ea15e522e3a7858d2c80f1ecc320ece1244624c9/pypdfium2-5.10.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:172ff3e10358d66456e27fb0b8b5098e28ec24e51072eb4b5b86077d550e21bb", size = 5229373 }, + { url = "https://files.pythonhosted.org/packages/c3/48/c7ed3001f0c5e28114c98bf918c8121e422a65ba7323e1e12f3f28e2d278/pypdfium2-5.10.1-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:09fda0609dc4749c9865a0315c447d6f2583a580693bcabd69980d3fbb22ad51", size = 5140010 }, + { url = "https://files.pythonhosted.org/packages/b2/bc/00b731bfc1fdc0f3c7d91108fceb54978ff4f5a92336820e5ed6c12535ff/pypdfium2-5.10.1-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:6b01adcfe9aaf7a635a59bed5687fd4fd7b0da292664f050d4ebd2bfa5c70584", size = 4643310 }, + { url = "https://files.pythonhosted.org/packages/9a/e8/3ad242233f657c19092a8c83684c8154b8d02a826535a6a2591d91aa5dde/pypdfium2-5.10.1-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:610e14c37d2090b826dccc0604fc7e7612c0ce591190780ae228abdb9abb971e", size = 5087879 }, + { url = "https://files.pythonhosted.org/packages/80/3f/9d0747a02ac3021ed3db7ac27c5187d97e78b0253a4bdfbf386666968474/pypdfium2-5.10.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f2803020952afa57e1e148adc19369b835154e1fa251a1ca85008ab6466a710f", size = 5047369 }, + { url = "https://files.pythonhosted.org/packages/9e/41/7d5187e9527eae81890a3b13193442112ef788d61ddaa68a6d59ac447bec/pypdfium2-5.10.1-py3-none-win32.whl", hash = "sha256:8702bb4f01ddfc8e7757b41b4c2c8392ac17c9f0234476e1e69672ea7c6d6aa0", size = 3680056 }, + { url = "https://files.pythonhosted.org/packages/16/1d/c62bd59dd8345cc4b640f942f465633f6b07b859d01ddb648610a7bf5c7c/pypdfium2-5.10.1-py3-none-win_amd64.whl", hash = "sha256:58da5b51fb7884c7d21a05062ab13edb011d1a08dfd9694f3d5d685df62796b9", size = 3812105 }, + { url = "https://files.pythonhosted.org/packages/10/d5/21bac39125df8a93e99c04583486b58a62b5997d6b3541e3ad0f69053392/pypdfium2-5.10.1-py3-none-win_arm64.whl", hash = "sha256:e3301c2f7a66fb8cb57dba857d0c9e90215e178f6602a87c5a306cd98513dab8", size = 3600043 }, +] + +[[package]] +name = "pypika" +version = "0.51.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585 }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, ] [[package]] name = "pystache" version = "0.6.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/89/0a712ca22930b8c71bced8703e5bb45669c31690ea81afe15f6cb284550c/pystache-0.6.8.tar.gz", hash = "sha256:3707518e6a4d26dd189b07c10c669b1fc17df72684617c327bd3550e7075c72c", size = 101892, upload-time = "2025-03-18T11:54:47.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/89/0a712ca22930b8c71bced8703e5bb45669c31690ea81afe15f6cb284550c/pystache-0.6.8.tar.gz", hash = "sha256:3707518e6a4d26dd189b07c10c669b1fc17df72684617c327bd3550e7075c72c", size = 101892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/78/ffd13a516219129cef6a754a11ba2a1c0d69f1e281af4f6bca9ed5327219/pystache-0.6.8-py3-none-any.whl", hash = "sha256:7211e000974a6e06bce2d4d5cad8df03bcfffefd367209117376e4527a1c3cb8", size = 82051, upload-time = "2025-03-18T11:54:45.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/78/ffd13a516219129cef6a754a11ba2a1c0d69f1e281af4f6bca9ed5327219/pystache-0.6.8-py3-none-any.whl", hash = "sha256:7211e000974a6e06bce2d4d5cad8df03bcfffefd367209117376e4527a1c3cb8", size = 82051 }, ] [[package]] @@ -3069,9 +4025,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, ] [[package]] @@ -3081,9 +4037,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855, upload-time = "2024-08-22T08:03:18.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024, upload-time = "2024-08-22T08:03:15.536Z" }, + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, ] [[package]] @@ -3091,13 +4047,13 @@ name = "pytest-cov" version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398 } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" }, + { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115 }, ] [[package]] @@ -3107,9 +4063,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000 }, ] [[package]] @@ -3120,9 +4076,9 @@ dependencies = [ { name = "pytest" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/18/30ad0408295f3157f7a4913f0eaa51a0a377ebad0ffa51ff239e833c6c72/pytest_pretty-1.2.0.tar.gz", hash = "sha256:105a355f128e392860ad2c478ae173ff96d2f03044692f9818ff3d49205d3a60", size = 6542, upload-time = "2023-04-05T17:11:50.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/18/30ad0408295f3157f7a4913f0eaa51a0a377ebad0ffa51ff239e833c6c72/pytest_pretty-1.2.0.tar.gz", hash = "sha256:105a355f128e392860ad2c478ae173ff96d2f03044692f9818ff3d49205d3a60", size = 6542 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/fe/d44d391312c1b8abee2af58ee70fabb1c00b6577ac4e0bdf25b70c1caffb/pytest_pretty-1.2.0-py3-none-any.whl", hash = "sha256:6f79122bf53864ae2951b6c9e94d7a06a87ef753476acd4588aeac018f062036", size = 6180, upload-time = "2023-04-05T17:11:49.801Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fe/d44d391312c1b8abee2af58ee70fabb1c00b6577ac4e0bdf25b70c1caffb/pytest_pretty-1.2.0-py3-none-any.whl", hash = "sha256:6f79122bf53864ae2951b6c9e94d7a06a87ef753476acd4588aeac018f062036", size = 6180 }, ] [[package]] @@ -3132,113 +4088,79 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] [[package]] name = "python-dotenv" version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, ] [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881 } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042 }, ] [[package]] -name = "pytz" -version = "2025.2" +name = "python-pptx" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +dependencies = [ + { name = "lxml" }, + { name = "pillow" }, + { name = "typing-extensions" }, + { name = "xlsxwriter" }, ] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788 }, ] [[package]] -name = "pyyaml" -version = "6.0.3" +name = "pytz" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, ] [[package]] -name = "ragas" -version = "0.2.15" +name = "pywin32" +version = "312" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "appdirs" }, - { name = "datasets" }, - { name = "diskcache" }, - { name = "langchain" }, - { name = "langchain-community" }, - { name = "langchain-core" }, - { name = "langchain-openai" }, - { name = "nest-asyncio" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "tiktoken" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/0f/04fddfa94744b1c3d8901aed8832a6b4193cc8e4886881f1bb88ff055350/ragas-0.2.15.tar.gz", hash = "sha256:2d0cd77b315a9c9c02ceb0a19ca8a48e82e1d02416587a2944ea51e6e327cd7b", size = 40867766, upload-time = "2025-04-24T16:39:28.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/9b/a5641da8aab06e069885a9ffa1b4897878f14c5b9807a9e3c5f1f532a6a9/ragas-0.2.15-py3-none-any.whl", hash = "sha256:298cd3d1fe3bd21ca4d31023a55079740d7bdd27a8c915bb371cec3c50cde608", size = 190947, upload-time = "2025-04-24T16:39:25.841Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184 }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298 }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640 }, ] [[package]] -name = "rank-bm25" -version = "0.2.2" +name = "pyyaml" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347, upload-time = "2022-02-16T12:10:52.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/21/f691fb2613100a62b3fa91e9988c991e9ca5b89ea31c0d3152a3210344f9/rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae", size = 8584, upload-time = "2022-02-16T12:10:50.626Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, ] [[package]] @@ -3248,47 +4170,46 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, ] [[package]] name = "regex" version = "2025.11.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, - { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, - { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, - { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, - { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, - { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, - { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, - { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, - { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, - { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, - { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, - { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, - { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 }, ] [[package]] @@ -3301,21 +4222,22 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, ] [[package]] -name = "requests-futures" -version = "1.0.2" +name = "requests-oauthlib" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "oauthlib" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/f8/175b823241536ba09da033850d66194c372c65c38804847ac9cef0239542/requests_futures-1.0.2.tar.gz", hash = "sha256:6b7eb57940336e800faebc3dab506360edec9478f7b22dc570858ad3aa7458da", size = 10356, upload-time = "2024-11-15T22:14:51.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/23/7c1096731c15c83826cb0dd42078b561a838aed44c36f370aeb815168106/requests_futures-1.0.2-py2.py3-none-any.whl", hash = "sha256:a3534af7c2bf670cd7aa730716e9e7d4386497554f87792be7514063b8912897", size = 7671, upload-time = "2024-11-15T22:14:50.255Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, ] [[package]] @@ -3325,72 +4247,59 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, ] [[package]] name = "rich" -version = "13.9.4" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963 }, ] [[package]] name = "rpds-py" version = "0.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, - { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, - { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, - { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, - { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, - { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, - { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, - { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, - { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, - { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, - { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, - { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, - { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, - { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, - { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, - { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, - { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, - { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, - { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, - { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, - { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710 }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582 }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172 }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586 }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339 }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201 }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095 }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077 }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548 }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661 }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937 }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496 }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126 }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771 }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994 }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886 }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262 }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826 }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234 }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008 }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569 }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188 }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587 }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641 }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683 }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730 }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361 }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227 }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248 }, ] [[package]] @@ -3400,9 +4309,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547 } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712 }, ] [[package]] @@ -3411,23 +4320,22 @@ version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, - { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, - { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, - { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, - { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, - { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, - { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, - { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, - { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382 }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042 }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180 }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660 }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057 }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731 }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852 }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094 }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436 }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749 }, ] [[package]] @@ -3435,58 +4343,57 @@ name = "scipy" version = "1.16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, - { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, - { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, - { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, - { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, - { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, - { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856 }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306 }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371 }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877 }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103 }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756 }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566 }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877 }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366 }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931 }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081 }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244 }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753 }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912 }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371 }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477 }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678 }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178 }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246 }, ] [[package]] name = "semantic-version" version = "2.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552 }, ] [[package]] name = "semver" version = "3.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912 }, ] [[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, ] [[package]] @@ -3497,63 +4404,63 @@ dependencies = [ { name = "packaging" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/b1/19587742aad604f1988a8a362e660e8c3ac03adccdb71c96d86526e5eb62/setuptools_scm-9.2.2.tar.gz", hash = "sha256:1c674ab4665686a0887d7e24c03ab25f24201c213e82ea689d2f3e169ef7ef57", size = 203385, upload-time = "2025-10-19T22:08:05.608Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/b1/19587742aad604f1988a8a362e660e8c3ac03adccdb71c96d86526e5eb62/setuptools_scm-9.2.2.tar.gz", hash = "sha256:1c674ab4665686a0887d7e24c03ab25f24201c213e82ea689d2f3e169ef7ef57", size = 203385 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ea/ac2bf868899d0d2e82ef72d350d97a846110c709bacf2d968431576ca915/setuptools_scm-9.2.2-py3-none-any.whl", hash = "sha256:30e8f84d2ab1ba7cb0e653429b179395d0c33775d54807fc5f1dd6671801aef7", size = 62975, upload-time = "2025-10-19T22:08:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ea/ac2bf868899d0d2e82ef72d350d97a846110c709bacf2d968431576ca915/setuptools_scm-9.2.2-py3-none-any.whl", hash = "sha256:30e8f84d2ab1ba7cb0e653429b179395d0c33775d54807fc5f1dd6671801aef7", size = 62975 }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, ] [[package]] name = "soupsieve" version = "2.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679 }, ] [[package]] @@ -3564,25 +4471,17 @@ dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/81/15d7c161c9ddf0900b076b55345872ed04ff1ed6a0666e5e94ab44b0163c/sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", size = 2140517, upload-time = "2025-10-10T15:36:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d5/4abd13b245c7d91bdf131d4916fd9e96a584dac74215f8b5bc945206a974/sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", size = 2130738, upload-time = "2025-10-10T15:36:16.91Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3c/8418969879c26522019c1025171cefbb2a8586b6789ea13254ac602986c0/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", size = 3304145, upload-time = "2025-10-10T15:34:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/94/2d/fdb9246d9d32518bda5d90f4b65030b9bf403a935cfe4c36a474846517cb/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", size = 3304511, upload-time = "2025-10-10T15:47:05.088Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/40f2ad1da97d5c83f6c1269664678293d3fe28e90ad17a1093b735420549/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", size = 3235161, upload-time = "2025-10-10T15:34:21.193Z" }, - { url = "https://files.pythonhosted.org/packages/95/cb/7cf4078b46752dca917d18cf31910d4eff6076e5b513c2d66100c4293d83/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", size = 3261426, upload-time = "2025-10-10T15:47:07.196Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3b/55c09b285cb2d55bdfa711e778bdffdd0dc3ffa052b0af41f1c5d6e582fa/sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", size = 2105392, upload-time = "2025-10-10T15:38:20.051Z" }, - { url = "https://files.pythonhosted.org/packages/c7/23/907193c2f4d680aedbfbdf7bf24c13925e3c7c292e813326c1b84a0b878e/sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", size = 2130293, upload-time = "2025-10-10T15:38:21.601Z" }, - { url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" }, - { url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" }, - { url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479 }, + { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212 }, + { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353 }, + { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222 }, + { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614 }, + { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248 }, + { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275 }, + { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901 }, + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718 }, ] [package.optional-dependencies] @@ -3594,44 +4493,51 @@ asyncio = [ name = "sqlean-py" version = "3.49.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/eb/ac95fab0bc4658124b4ec8fbc31fc494165ab4544606ae91b9a489907dad/sqlean_py-3.49.1.tar.gz", hash = "sha256:210d89989226b988d7d6391f837387d3b81e8cd608c997e0bd37826e395970e7", size = 3319012, upload-time = "2025-05-02T11:58:24.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/eb/ac95fab0bc4658124b4ec8fbc31fc494165ab4544606ae91b9a489907dad/sqlean_py-3.49.1.tar.gz", hash = "sha256:210d89989226b988d7d6391f837387d3b81e8cd608c997e0bd37826e395970e7", size = 3319012 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/d3/30a7dc9f6030ea2a1c76fcbb220d1f5d95a16f22f50d4cd80c8778ca5018/sqlean_py-3.49.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:ba8e5fc5b9d6682f3fbb626e1f936a4c4ac7377c3d044beaef695e6306a9a39b", size = 1129238 }, + { url = "https://files.pythonhosted.org/packages/87/73/ccd43c0d6ca4248005687c5c1ce121924e368c0b195b98880810f468312d/sqlean_py-3.49.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3112cce35bb97f17b55419e955dc74c7deebb0b548e54d99b3e32bb3d3c62ce4", size = 1053654 }, + { url = "https://files.pythonhosted.org/packages/82/d5/cfdb8098117fff9f7856d5c86c93d9c7bebdb5801b551cfabf36e117b764/sqlean_py-3.49.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9974027675e1edf8a8c90383f95e4038052e96ef7a9a8fa7f8125ed275cc28ba", size = 3000002 }, + { url = "https://files.pythonhosted.org/packages/96/52/7e713a61ded163a5fece9fb144b60107889e5d813ba9cff278e65673b6f9/sqlean_py-3.49.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fe4e2e74c1749bf98e4aae8fb02978e4c03d4a491be18c4e5b244b29408fdf", size = 3004709 }, + { url = "https://files.pythonhosted.org/packages/c4/d6/455e75e1e540b64e230f7282ec55777e030190a486e98a2fce5d6437afa2/sqlean_py-3.49.1-cp313-cp313-win_amd64.whl", hash = "sha256:de820bdb39729044f9ed94f0addfe308b020479334146036a773f470d7594d87", size = 804300 }, + { url = "https://files.pythonhosted.org/packages/99/bf/b63830855455fd22278ddc78cc7c64dffb5e1a69c15245c18275317ae9d5/sqlean_py-3.49.1-cp313-cp313-win_arm64.whl", hash = "sha256:3c1661f2fcf4d10ec3940ef8d2146bb58260b409c9033f7a727a6962e2032b7c", size = 739448 }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/0e/5abd9f12008918dfbce0add356eee8a6f076c3cb24af4a5d2694a8b9f009/sqlean_py-3.49.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:9858cd066c7105145d56aae2d9c0a6c8baf343b642e41c9a96a83ff3033e5395", size = 1128201, upload-time = "2025-05-02T11:57:44.744Z" }, - { url = "https://files.pythonhosted.org/packages/39/21/f39ee0b16050b79fb86507e0affd623f6f7fba04f3d3b1dc7e41b93ada7d/sqlean_py-3.49.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b034fd390fccbb62198e0163e885111de86f2a20767a2f31d38313008cb57442", size = 1053419, upload-time = "2025-05-02T11:57:46.545Z" }, - { url = "https://files.pythonhosted.org/packages/e2/10/2f2bd23d2ec66662bebed7d83fcc912af97dd12be739fd5bd3c08444a55c/sqlean_py-3.49.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09d7b002f490fd0dad98e884a6e05f062d02ca6520c1910b486fb9e19c5e61ab", size = 2995449, upload-time = "2025-05-02T11:57:47.853Z" }, - { url = "https://files.pythonhosted.org/packages/03/d7/c9970eedb0876f3bcf3f0e230ef68f41fa4bb1c88a67f437f88f0cb0d62b/sqlean_py-3.49.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8738695e3ca05cd47b16808d8ad8cd973f1eb6e32497364512a26cb4e61ad63", size = 3000141, upload-time = "2025-05-02T11:57:49.388Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ce/a41b35ecaf7825fbf63cedabfe3ee89f26b75556432ab547476c0f039650/sqlean_py-3.49.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bf2e118ea3c5c16c6a0be4b80d31c9392ffee231e61f08464cc8f892e40c628", size = 803486, upload-time = "2025-05-02T11:57:50.872Z" }, - { url = "https://files.pythonhosted.org/packages/09/dd/6dbcda7d883701eb8a6f55fc5636919b04f84ad9c9be33b3309199150dd4/sqlean_py-3.49.1-cp311-cp311-win_arm64.whl", hash = "sha256:ff575bb11a3013963e4b2edb0eaafc771e8dbe193f22151ee2c3ecc694f25eee", size = 739176, upload-time = "2025-05-02T11:57:52.08Z" }, - { url = "https://files.pythonhosted.org/packages/23/0e/95379c801907f8da939be30c61b63ef881eb18cc0a90713cc74002deaf16/sqlean_py-3.49.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:b43397ac27312a20fef8f5db0d011d3676fddea04850d6c35ad1d7644d146f71", size = 1129522, upload-time = "2025-05-02T11:57:53.238Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/b4c5d0ff47f179a0729dc71832558f63a1605e931f773b6d435a767baca9/sqlean_py-3.49.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56a3388f4fe08e1332af424eaaf378eb9e258165885a4e284c5238e4837b2232", size = 1053757, upload-time = "2025-05-02T11:57:54.966Z" }, - { url = "https://files.pythonhosted.org/packages/2d/11/f8a5c0f7fb889cabbd8c7f887f9e261b65bf45fa1d33a245490855bff53e/sqlean_py-3.49.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bbc00d57f68dbb3f5dea20380bdad7fa9299b3c2a8d8b3798c2bf315146be9", size = 3003708, upload-time = "2025-05-02T11:57:57.035Z" }, - { url = "https://files.pythonhosted.org/packages/47/84/065a21e9a578ef13d9749c2bf3c382f7e50a1644af7d7a109924fbc6faee/sqlean_py-3.49.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae92413470409b3b3678c797db82c1d247b6ed2198017b5e893d9fc0606e061f", size = 3008238, upload-time = "2025-05-02T11:57:58.927Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e5/d980bf0c5e67cfd4486b5e6d8daea6815fdac57b3d0899c23127a30283b5/sqlean_py-3.49.1-cp312-cp312-win_amd64.whl", hash = "sha256:b45ed4adb8442299019988d5e90aa1474f3ef8c71f6e8921a70705b9d971c590", size = 804506, upload-time = "2025-05-02T11:58:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d1/8828ec685022e2b86ebd717743359dd5e8eab70a9c1ebff78aa733848216/sqlean_py-3.49.1-cp312-cp312-win_arm64.whl", hash = "sha256:c2537f69879adaed22b16e840d494c440ece5b49d28a5a51094e6c8ca6a2b0a5", size = 739690, upload-time = "2025-05-02T11:58:02.262Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171 }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434 }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076 }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388 }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804 }, ] [[package]] name = "sse-starlette" -version = "3.0.3" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514 }, ] [[package]] name = "starlette" -version = "0.46.2" +version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272 }, ] [[package]] @@ -3644,63 +4550,71 @@ dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/aa/9d8a53fbf0271e7a5c5155b76d256caf4b94f422676226253648871cc7c1/strawberry_graphql-0.270.1.tar.gz", hash = "sha256:d64524a943851d83252e39b82ef768ee494259edecf9510b37bad9a46b745db8", size = 207190, upload-time = "2025-05-22T12:29:28.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/aa/9d8a53fbf0271e7a5c5155b76d256caf4b94f422676226253648871cc7c1/strawberry_graphql-0.270.1.tar.gz", hash = "sha256:d64524a943851d83252e39b82ef768ee494259edecf9510b37bad9a46b745db8", size = 207190 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/e0/c45d74578e7b8cb7e082697d998cebd8ef97afa3d7aedc22e4acd8ae7163/strawberry_graphql-0.270.1-py3-none-any.whl", hash = "sha256:3593086dc08614ae241cb88f7691e90f90b01cab6ee6351cb3838fc5ba8bfab0", size = 301232 }, +] + +[[package]] +name = "synchronicity" +version = "0.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/38/901ed2557527541e90e89366212538463de96f9393de95fee2f7928d1321/synchronicity-0.12.4.tar.gz", hash = "sha256:f9e38d0c37d8e56b5dcb22cc0083591a535a42187d3d97ea9aef6e84bd122640", size = 60339 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/e0/c45d74578e7b8cb7e082697d998cebd8ef97afa3d7aedc22e4acd8ae7163/strawberry_graphql-0.270.1-py3-none-any.whl", hash = "sha256:3593086dc08614ae241cb88f7691e90f90b01cab6ee6351cb3838fc5ba8bfab0", size = 301232, upload-time = "2025-05-22T12:29:25.739Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/ed8f5c1ce8d6a4d986a5629fcd17fca6e91952e43ffa9bdb7d640341e50d/synchronicity-0.12.4-py3-none-any.whl", hash = "sha256:87c53625f680ed1a6992fccda5adeb6705904fdb0c820e04d384358708f2607c", size = 41013 }, ] [[package]] name = "tabulate" version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, ] [[package]] -name = "tantivy" -version = "0.22.2" +name = "tavily-python" +version = "0.7.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/f6/5f1e233e0064e3981d738e3517965f8ce8ce6ecb3756d914ab969051ef45/tantivy-0.22.2.tar.gz", hash = "sha256:b3d0f19271365ccf6e69f0318ea0c3c3438515fee453618977787d6b41da833a", size = 71220, upload-time = "2025-03-20T03:09:36.214Z" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/197419d6133643848514e5e84e8f41886e825b73bf91ae235a1595c964f5/tavily_python-0.7.23.tar.gz", hash = "sha256:3b92232e0e29ab68898b765f281bb4f2c650b02210b64affbc48e15292e96161", size = 25968 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/54/d19c72072c64a896f9db0be7f36dc4d283555ade13090553401353817f0b/tantivy-0.22.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e2f4706df74a23e0ee1a33190dd60330ed071ea2d1488024b0a8ce16be439d65", size = 7120786, upload-time = "2025-03-20T03:09:10.163Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/3d800d5236fb4c07fa1c3805ca2ea429ccc2d6eb89084d35ef06b09be4ed/tantivy-0.22.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ff01a95ea391e747493ebea058f1f74ba7116f05a6bff6d35d5d5c0832b51195", size = 3684522, upload-time = "2025-03-20T03:09:12.071Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9a/0a5a9d6590046c7065430cc8d98fba754f167b9b4c18f94e23df76d01b59/tantivy-0.22.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a263039084534099f4d506463aa0765fbcdfe0791e8019170f0d376c08e15c49", size = 3901525, upload-time = "2025-03-20T03:09:13.899Z" }, - { url = "https://files.pythonhosted.org/packages/69/a4/3a0cd9c577c37682e185d2295f99513020ccd392eaf861cf7d5a5de1aeca/tantivy-0.22.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0aba594be4e6d7f7343b03b4506bbf23ec5491519e9d82f6f835a32e615587d6", size = 4032340, upload-time = "2025-03-20T03:09:15.797Z" }, - { url = "https://files.pythonhosted.org/packages/7a/82/b5f5f5c13fc9e06c0cd7ffe6c2146b2f6e5f6d54a4f0e6f36b5a14843834/tantivy-0.22.2-cp311-none-win_amd64.whl", hash = "sha256:e3e07d6353421fcb7894759e6185075498cf47b3f841b2e9994b59d4dc0aa3f2", size = 3132470, upload-time = "2025-03-20T03:09:17.466Z" }, - { url = "https://files.pythonhosted.org/packages/fb/47/b38e02a5d0f27e2611f99acc493794ac1b3dc3ab6488872c600f51a00549/tantivy-0.22.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:90b8601a8860f9a12229dda022920685becf49bbd807e9f6fce82bfe20fb494a", size = 7129649, upload-time = "2025-03-20T03:09:18.916Z" }, - { url = "https://files.pythonhosted.org/packages/af/93/3d7860ebb0fe00819edf5500bc6648d21868f6990f6156a931122dc3b261/tantivy-0.22.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b4f2805a83873b53af20fd7b04d30d9a2a640ee105930baae4e1c6260d346915", size = 3687826, upload-time = "2025-03-20T03:09:20.663Z" }, - { url = "https://files.pythonhosted.org/packages/56/36/3e2624c4a4ff978189147033e011a1259fb9953a179a4b9b677de7cea283/tantivy-0.22.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5da1b7905c1e884d3214979eb7a20349a9951225b317dfc0cfb2d5fdff365c3", size = 3913166, upload-time = "2025-03-20T03:09:22.111Z" }, - { url = "https://files.pythonhosted.org/packages/59/e4/6131e6824886ea97498cb5b5cd326279a649a96b9bd7e9a7448d862d6bbf/tantivy-0.22.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7841ed6b38fcadf7bab40b748a511044ce0c7b9f92b350d09d23251751932f1", size = 4048497, upload-time = "2025-03-20T03:09:23.933Z" }, - { url = "https://files.pythonhosted.org/packages/5c/50/e8eade950a56473f745fa084787b2741543eb21b5c75c9374ba87bc1932f/tantivy-0.22.2-cp312-none-win_amd64.whl", hash = "sha256:61942263b54e71f3f99c566f6a630d4fd2408b0bfbe201e6757dd8bea57732c9", size = 3142846, upload-time = "2025-03-20T03:09:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/f9c6e9249367be0772fb754849e03cbbc6ad8d80a479bf30ea8811828b2e/tavily_python-0.7.23-py3-none-any.whl", hash = "sha256:52ef85c44b926bce3f257570cd32bc1bd4db54666acf3105617f27411a59e188", size = 19079 }, ] [[package]] name = "tblib" version = "3.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046, upload-time = "2025-11-12T12:21:16.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" }, + { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893 }, ] [[package]] name = "tenacity" version = "9.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, ] [[package]] name = "threadpoolctl" version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, ] [[package]] @@ -3711,22 +4625,31 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 }, +] + +[[package]] +name = "tinytag" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/59/8a8cb2331e2602b53e4dc06960f57d1387a2b18e7efd24e5f9cb60ea4925/tinytag-2.2.1.tar.gz", hash = "sha256:e6d06610ebe7cd66fd07be2d3b9495914ab32654a5e47657bb8cd44c2484523c", size = 38214 } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/ce/34/d50e338631baaf65ec5396e70085e5de0b52b24b28db1ffbc1c6e82190dc/tinytag-2.2.1-py3-none-any.whl", hash = "sha256:ed8b1e6d25367937e3321e054f4974f9abfde1a3e0a538824c87da377130c2b6", size = 32927 }, ] [[package]] @@ -3736,84 +4659,68 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, - { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318 }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478 }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994 }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141 }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049 }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730 }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560 }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221 }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569 }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599 }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862 }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250 }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003 }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 }, ] [[package]] -name = "tomli" -version = "2.3.0" +name = "toml" +version = "0.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, ] [[package]] name = "tomlkit" version = "0.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, ] [[package]] name = "toolz" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093 }, ] [[package]] name = "tornado" version = "6.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, - { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, - { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563 }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729 }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295 }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644 }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878 }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549 }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973 }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954 }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023 }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427 }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456 }, ] [[package]] @@ -3823,92 +4730,51 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] [[package]] -name = "traitlets" -version = "5.14.3" +name = "typer" +version = "0.26.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, ] - -[[package]] -name = "tree-sitter" -version = "0.21.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/9e/b7cb190aa08e4ea387f2b1531da03efb4b8b033426753c0b97e3698645f6/tree-sitter-0.21.3.tar.gz", hash = "sha256:b5de3028921522365aa864d95b3c41926e0ba6a85ee5bd000e10dc49b0766988", size = 155688, upload-time = "2024-03-26T10:53:35.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/ed/ef06584ccdd5c410df0837951ecd7e15d9a6144ea1bd4c73cecab1a89891/typer-0.26.7.tar.gz", hash = "sha256:e314a34c617e419c091b2830dda3ea1f257134ff593061a8f5b9717ab8dddb3a", size = 201709 } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/b5/72657d5874d7f0a722c0288f04e5e2bc33d7715b13a858885b6593047dce/tree_sitter-0.21.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54b22c3c2aab3e3639a4b255d9df8455da2921d050c4829b6a5663b057f10db5", size = 133429, upload-time = "2024-03-26T10:52:46.345Z" }, - { url = "https://files.pythonhosted.org/packages/d3/64/c5d397efbb6d0dbed4254cd2ca389ed186a2e1e7e32661059f6eeaaf6424/tree_sitter-0.21.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab6e88c1e2d5e84ff0f9e5cd83f21b8e5074ad292a2cf19df3ba31d94fbcecd4", size = 126088, upload-time = "2024-03-26T10:52:47.759Z" }, - { url = "https://files.pythonhosted.org/packages/ba/88/941669acc140f94e6c6196d6d8676ac4cd57c3b3fbc1ee61bb11c1b2da71/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3fd34ed4cd5db445bc448361b5da46a2a781c648328dc5879d768f16a46771", size = 487879, upload-time = "2024-03-26T10:52:49.091Z" }, - { url = "https://files.pythonhosted.org/packages/29/4e/798154f2846d620bf9fa3bc244e056d4858f2108f834656bf9f1219d4f30/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fabc7182f6083269ce3cfcad202fe01516aa80df64573b390af6cd853e8444a1", size = 498776, upload-time = "2024-03-26T10:52:50.709Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d1/05ea77487bc7a3946d0e80fb6c5cb61515953f5e7a4f6804b98e113ed4b0/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f874c3f7d2a2faf5c91982dc7d88ff2a8f183a21fe475c29bee3009773b0558", size = 483348, upload-time = "2024-03-26T10:52:52.267Z" }, - { url = "https://files.pythonhosted.org/packages/42/fa/bf938e7c6afbc368d503deeda060891c3dba57e2d1166e4b884271f55616/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ee61ee3b7a4eedf9d8f1635c68ba4a6fa8c46929601fc48a907c6cfef0cfbcb2", size = 493757, upload-time = "2024-03-26T10:52:54.845Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a7/98da36a6eab22f5729989c9e0137b1b04cbe368d1e024fccd72c0b00719b/tree_sitter-0.21.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b7256c723642de1c05fbb776b27742204a2382e337af22f4d9e279d77df7aa2", size = 109735, upload-time = "2024-03-26T10:52:57.243Z" }, - { url = "https://files.pythonhosted.org/packages/81/e1/cceb06eae617a6bf5eeeefa9813d9fd57d89b50f526ce02486a336bcd2a9/tree_sitter-0.21.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:669b3e5a52cb1e37d60c7b16cc2221c76520445bb4f12dd17fd7220217f5abf3", size = 133640, upload-time = "2024-03-26T10:52:59.135Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ce/ac14e5cbb0f30b7bd338122491ee2b8e6c0408cfe26741cbd66fa9b53d35/tree_sitter-0.21.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2aa2a5099a9f667730ff26d57533cc893d766667f4d8a9877e76a9e74f48f0d3", size = 125954, upload-time = "2024-03-26T10:53:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/c2/df/76dbf830126e566c48db0d1bf2bef3f9d8cac938302a9b0f762ded8206c2/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3e06ae2a517cf6f1abb682974f76fa760298e6d5a3ecf2cf140c70f898adf0", size = 490092, upload-time = "2024-03-26T10:53:03.144Z" }, - { url = "https://files.pythonhosted.org/packages/ec/87/0c3593552cb0d09ab6271d37fc0e6a9476919d2a975661d709d4b3289fc7/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af992dfe08b4fefcfcdb40548d0d26d5d2e0a0f2d833487372f3728cd0772b48", size = 502155, upload-time = "2024-03-26T10:53:04.76Z" }, - { url = "https://files.pythonhosted.org/packages/05/92/b2cb22cf52c18fcc95662897f380cf230c443dfc9196b872aad5948b7bb3/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c7cbab1dd9765138505c4a55e2aa857575bac4f1f8a8b0457744a4fefa1288e6", size = 486020, upload-time = "2024-03-26T10:53:06.414Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ea/69b543538a46d763f3e787234d1617b718ab90f32ffa676ca856f1d9540e/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1e66aeb457d1529370fcb0997ae5584c6879e0e662f1b11b2f295ea57e22f54", size = 496348, upload-time = "2024-03-26T10:53:07.939Z" }, - { url = "https://files.pythonhosted.org/packages/eb/4f/df4ea84476443021707b537217c32147ccccbc3e10c17b216a969991e1b3/tree_sitter-0.21.3-cp312-cp312-win_amd64.whl", hash = "sha256:013c750252dc3bd0e069d82e9658de35ed50eecf31c6586d0de7f942546824c5", size = 109771, upload-time = "2024-03-26T10:53:10.342Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/2201973529af2c954de0bb725323c3aaed6d7f0ceee8f550dec9185df013/typer-0.26.7-py3-none-any.whl", hash = "sha256:5c87cfbc5d34491c5346ebf49c23e18d56ccb863268d3a8d592b26087c2f5e58", size = 122456 }, ] [[package]] -name = "tree-sitter-languages" -version = "1.10.2" +name = "types-certifi" +version = "2021.10.8.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tree-sitter" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/6c/c310e958296ce12076bec846c0bb779bc114897b33901c4c51c09bb6b695/tree_sitter_languages-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7eb7d7542b2091c875fe52719209631fca36f8c10fa66970d2c576ae6a1b8289", size = 8884893, upload-time = "2024-02-04T10:28:14.963Z" }, - { url = "https://files.pythonhosted.org/packages/65/82/183b039abe46d6753357019b4f0484d5b74973ee4675da2f26af5ba8dfdf/tree_sitter_languages-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b41bcb00974b1c8a1800c7f1bb476a1d15a0463e760ee24872f2d53b08ee424", size = 9724629, upload-time = "2024-02-04T10:28:17.776Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a2/e8272617901f896ae36459ed2a2ff06d9b1ff5e6157d034c5e2c9885c741/tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f370cd7845c6c81df05680d5bd96db8a99d32b56f4728c5d05978911130a853", size = 8669175, upload-time = "2024-02-04T10:28:19.819Z" }, - { url = "https://files.pythonhosted.org/packages/a6/97/2c72765a807ea226759a827324ed6a74382b4ae1b18321c67333199a4622/tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1dc195c88ef4c72607e112a809a69190e096a2e5ebc6201548b3e05fdd169ad", size = 8584029, upload-time = "2024-02-04T10:28:22.464Z" }, - { url = "https://files.pythonhosted.org/packages/96/81/ab4eda8dbd3f736fcc9a508bc69232d3b9076cd46b932d9bf9d49b9a1ec9/tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae34ac314a7170be24998a0f994c1ac80761d8d4bd126af27ee53a023d3b849", size = 8422544, upload-time = "2024-02-04T10:28:25.104Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/9af34d7259399179ecc2a9f8e73a795c1caf3220b01d566c3ddd20ed5e1c/tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:01b5742d5f5bd675489486b582bd482215880b26dde042c067f8265a6e925d9c", size = 9186540, upload-time = "2024-02-04T10:28:27.322Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/3e3d5a83578f9942ab882c9c89e757fd3e98ca7d68f7608c9702d8608a1c/tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ab1cbc46244d34fd16f21edaa20231b2a57f09f092a06ee3d469f3117e6eb954", size = 9166371, upload-time = "2024-02-04T10:28:29.953Z" }, - { url = "https://files.pythonhosted.org/packages/f2/81/7792b474916541081533942598feaabc6e1df993892375a1a3d8f7100483/tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b1149e7467a4e92b8a70e6005fe762f880f493cf811fc003554b29f04f5e7c8", size = 8945341, upload-time = "2024-02-04T10:28:32.696Z" }, - { url = "https://files.pythonhosted.org/packages/6d/80/5e9679325e260cce2893b4a97a3914d5ed729024bb9b08a32d9b0d83ef7a/tree_sitter_languages-1.10.2-cp311-cp311-win32.whl", hash = "sha256:049276343962f4696390ee555acc2c1a65873270c66a6cbe5cb0bca83bcdf3c6", size = 8363372, upload-time = "2024-02-04T10:28:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/d9/52/e122dfc6739664c963a62f4b6717853e86295659c8531e2f1842bad9aba5/tree_sitter_languages-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:7f3fdd468a577f04db3b63454d939e26e360229b53c80361920aa1ebf2cd7491", size = 8269020, upload-time = "2024-02-04T10:28:37.43Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bf/a9bd2d6ecbd053de0a5a50c150105b69c90eb49089f9e1d4fc4937e86adc/tree_sitter_languages-1.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c0f4c8b2734c45859edc7fcaaeaab97a074114111b5ba51ab4ec7ed52104763c", size = 8884771, upload-time = "2024-02-04T10:28:39.655Z" }, - { url = "https://files.pythonhosted.org/packages/14/fb/1f6fe5903aeb7435cc66d4b56621e9a30a4de64420555b999de65b31fcae/tree_sitter_languages-1.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eecd3c1244ac3425b7a82ba9125b4ddb45d953bbe61de114c0334fd89b7fe782", size = 9724562, upload-time = "2024-02-04T10:28:42.275Z" }, - { url = "https://files.pythonhosted.org/packages/20/6c/1855a65c9d6b50600f7a68e0182153db7cb12ff81fdebd93e87851dfdd8f/tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15db3c8510bc39a80147ee7421bf4782c15c09581c1dc2237ea89cefbd95b846", size = 8678682, upload-time = "2024-02-04T10:28:44.642Z" }, - { url = "https://files.pythonhosted.org/packages/d0/75/eff180f187ce4dc3e5177b3f8508e0061ea786ac44f409cf69cf24bf31a6/tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92c6487a6feea683154d3e06e6db68c30e0ae749a7ce4ce90b9e4e46b78c85c7", size = 8595099, upload-time = "2024-02-04T10:28:47.767Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e6/eddc76ad899d77adcb5fca6cdf651eb1d33b4a799456bf303540f6cf8204/tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2f1cd1d1bdd65332f9c2b67d49dcf148cf1ded752851d159ac3e5ee4f4d260", size = 8433569, upload-time = "2024-02-04T10:28:50.404Z" }, - { url = "https://files.pythonhosted.org/packages/06/95/a13da048c33a876d0475974484bf66b1fae07226e8654b1365ab549309cd/tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:976c8039165b8e12f17a01ddee9f4e23ec6e352b165ad29b44d2bf04e2fbe77e", size = 9196003, upload-time = "2024-02-04T10:28:52.466Z" }, - { url = "https://files.pythonhosted.org/packages/ec/13/9e5cb03914d60dd51047ecbfab5400309fbab14bb25014af388f492da044/tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:dafbbdf16bf668a580902e1620f4baa1913e79438abcce721a50647564c687b9", size = 9175560, upload-time = "2024-02-04T10:28:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/19/76/25bb32a9be1c476e388835d5c8de5af2920af055e295770003683896cfe2/tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1aeabd3d60d6d276b73cd8f3739d595b1299d123cc079a317f1a5b3c5461e2ca", size = 8956249, upload-time = "2024-02-04T10:28:57.094Z" }, - { url = "https://files.pythonhosted.org/packages/52/01/8e2f97a444d25dde1380ec20b338722f733b6cc290524357b1be3dd452ab/tree_sitter_languages-1.10.2-cp312-cp312-win32.whl", hash = "sha256:fab8ee641914098e8933b87ea3d657bea4dd00723c1ee7038b847b12eeeef4f5", size = 8363094, upload-time = "2024-02-04T10:28:59.156Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/0262e875dd899447476a8ffde7829df3716ffa772990095c65d6de1f053c/tree_sitter_languages-1.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e606430d736367e5787fa5a7a0c5a1ec9b85eded0b3596bbc0d83532a40810b", size = 8268983, upload-time = "2024-02-04T10:29:00.987Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136 }, ] [[package]] -name = "typer-slim" -version = "0.20.0" +name = "types-toml" +version = "0.10.8.20260518" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561, upload-time = "2025-10-20T17:03:46.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/11/6ece999e91f2ccb848ab4420f3f4816e78ac0541f739e6864affdaaa5737/types_toml-0.10.8.20260518.tar.gz", hash = "sha256:80e10facd24fdeda9d5c672187d72be3ac284843788d67f5aae59e3e016db6fe", size = 9419 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/91/25/489751806bf5c95e4007f8e17409199c54d31e49ffbea07c5729b1286c8e/types_toml-0.10.8.20260518-py3-none-any.whl", hash = "sha256:0e564ab05f6fde62a315b3b5a9b6624fda569399795d30a37e64705a70459303", size = 9669 }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, ] [[package]] @@ -3919,9 +4785,9 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 }, ] [[package]] @@ -3931,18 +4797,30 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, ] [[package]] name = "tzdata" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 }, ] [[package]] @@ -3955,43 +4833,53 @@ dependencies = [ { name = "semantic-version" }, { name = "semver" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/82/8c07ede2b810485cd16cc88d24afa927f7cb6d0315c038440f264f2519c1/univers-30.12.0.tar.gz", hash = "sha256:35bb2861f4e4e525b251066739d9be6425caef00fe0d5feea5d447a87a14e3d7", size = 270924, upload-time = "2024-07-25T10:54:04.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/82/8c07ede2b810485cd16cc88d24afa927f7cb6d0315c038440f264f2519c1/univers-30.12.0.tar.gz", hash = "sha256:35bb2861f4e4e525b251066739d9be6425caef00fe0d5feea5d447a87a14e3d7", size = 270924 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/15/72e27a7725b6431d46953c4cae8555ada62bbf2dcc5895d91a225c0b6020/univers-30.12.0-py3-none-any.whl", hash = "sha256:d01c21a86216bdd092c32b527742263cbe2ee72b4c71797e2ddfa4b688dde930", size = 92611, upload-time = "2024-07-25T10:54:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/d7/15/72e27a7725b6431d46953c4cae8555ada62bbf2dcc5895d91a225c0b6020/univers-30.12.0-py3-none-any.whl", hash = "sha256:d01c21a86216bdd092c32b527742263cbe2ee72b4c71797e2ddfa4b688dde930", size = 92611 }, ] [[package]] -name = "url-normalize" -version = "1.4.3" +name = "urllib3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/ea/780a38c99fef750897158c0afb83b979def3b379aaac28b31538d24c4e8f/url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2", size = 6024, upload-time = "2020-10-26T02:07:11.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/1c/6c6f408be78692fc850006a2b6dea37c2b8592892534e09996e401efc74b/url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed", size = 6804, upload-time = "2020-10-26T02:07:13.218Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, ] [[package]] -name = "urllib3" -version = "2.5.0" +name = "uuid-utils" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786 }, + { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943 }, + { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467 }, + { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333 }, + { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859 }, + { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988 }, + { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784 }, + { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750 }, + { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818 }, + { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831 }, + { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333 }, + { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319 }, + { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566 }, + { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809 }, ] [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502 }, ] [package.optional-dependencies] @@ -4009,20 +4897,14 @@ standard = [ name = "uvloop" version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, - { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, - { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, - { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, ] [[package]] @@ -4030,29 +4912,23 @@ name = "vuln-analysis" source = { editable = "." } dependencies = [ { name = "aiofiles" }, - { name = "aiohttp-client-cache" }, { name = "aiolimiter" }, - { name = "aioresponses" }, + { name = "aiq-agent" }, { name = "arize-phoenix" }, - { name = "brotli" }, - { name = "esprima" }, - { name = "faiss-cpu" }, + { name = "bokeh" }, + { name = "deepagents" }, + { name = "filelock" }, { name = "gitpython" }, - { name = "google-search-results" }, - { name = "json5" }, + { name = "httpx" }, { name = "langchain" }, { name = "langchain-core" }, - { name = "mcp" }, - { name = "nbformat" }, - { name = "nemollm" }, - { name = "nvidia-nat", extra = ["async-endpoints", "langchain", "phoenix", "profiling"] }, + { name = "langchain-nvidia-ai-endpoints" }, + { name = "langgraph" }, + { name = "nvidia-nat", extra = ["async-endpoints", "eval", "langchain", "phoenix", "profiler"] }, { name = "openinference-instrumentation-langchain" }, - { name = "ordered-set" }, + { name = "packaging" }, { name = "pydpkg" }, - { name = "rank-bm25" }, - { name = "tantivy" }, - { name = "tree-sitter" }, - { name = "tree-sitter-languages" }, + { name = "tavily-python" }, { name = "univers" }, ] @@ -4077,29 +4953,23 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofiles" }, - { name = "aiohttp-client-cache", specifier = "==0.11" }, { name = "aiolimiter", specifier = "~=1.2" }, - { name = "aioresponses", specifier = "==0.7.6" }, + { name = "aiq-agent", editable = "aiq" }, { name = "arize-phoenix" }, - { name = "brotli" }, - { name = "esprima" }, - { name = "faiss-cpu", specifier = "==1.9.0" }, + { name = "bokeh", specifier = ">=3.1.0" }, + { name = "deepagents", specifier = ">=0.6.4,<0.7" }, + { name = "filelock" }, { name = "gitpython" }, - { name = "google-search-results", specifier = "==2.4" }, - { name = "json5" }, - { name = "langchain", specifier = ">=0.3,<1.0" }, - { name = "langchain-core", specifier = ">=0.3.80,<0.4.0" }, - { name = "mcp", specifier = ">=1.0,<1.20" }, - { name = "nbformat" }, - { name = "nemollm" }, - { name = "nvidia-nat", extras = ["async-endpoints", "langchain", "phoenix", "profiling"], specifier = "~=1.3.0" }, - { name = "openinference-instrumentation-langchain", specifier = "~=0.1.31" }, - { name = "ordered-set" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "langchain" }, + { name = "langchain-core" }, + { name = "langchain-nvidia-ai-endpoints", specifier = ">=1.4.1,<2.0.0" }, + { name = "langgraph", specifier = ">=1.0.5,<2.0.0" }, + { name = "nvidia-nat", extras = ["async-endpoints", "eval", "langchain", "phoenix", "profiler"], specifier = "==1.7.0" }, + { name = "openinference-instrumentation-langchain" }, + { name = "packaging", specifier = ">=24.0" }, { name = "pydpkg", specifier = "==1.9.4" }, - { name = "rank-bm25", specifier = "==0.2.2" }, - { name = "tantivy", specifier = "==0.22.2" }, - { name = "tree-sitter", specifier = "==0.21.3" }, - { name = "tree-sitter-languages", specifier = "==1.10.2" }, + { name = "tavily-python", specifier = ">=0.5.0" }, { name = "univers", specifier = "==30.12" }, ] @@ -4128,69 +4998,72 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854 }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] [[package]] @@ -4200,9 +5073,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, ] [[package]] @@ -4213,78 +5086,81 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/35/25e68fbc99e672127cc6fbb14b8ec1ba3dfef035bf1e4c90f78f24a80b7d/wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2", size = 27748, upload-time = "2014-11-15T15:59:49.808Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/35/25e68fbc99e672127cc6fbb14b8ec1ba3dfef035bf1e4c90f78f24a80b7d/wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2", size = 27748 } [[package]] name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315 }, ] [[package]] name = "xxhash" version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738 }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821 }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127 }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975 }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241 }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471 }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936 }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440 }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990 }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689 }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068 }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495 }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620 }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542 }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880 }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956 }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072 }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409 }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736 }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833 }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348 }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070 }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907 }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839 }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304 }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930 }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787 }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916 }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799 }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044 }, +] + +[[package]] +name = "xyzservices" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/022795fc1201e7c29e742a509913badb53ce0b38f64b6db859e2f6339da9/xyzservices-2025.11.0.tar.gz", hash = "sha256:2fc72b49502b25023fd71e8f532fb4beddbbf0aa124d90ea25dba44f545e17ce", size = 1135703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5c/2c189d18d495dd0fa3f27ccc60762bbc787eed95b9b0147266e72bb76585/xyzservices-2025.11.0-py3-none-any.whl", hash = "sha256:de66a7599a8d6dad63980b77defd1d8f5a5a9cb5fc8774ea1c6e89ca7c2a3d2f", size = 93916 }, ] [[package]] @@ -4294,9 +5170,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907 } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158, upload-time = "2024-11-14T00:11:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158 }, ] [[package]] @@ -4308,59 +5184,59 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 }, ] [[package]] name = "zict" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/ac/3c494dd7ec5122cff8252c1a209b282c0867af029f805ae9befd73ae37eb/zict-3.0.0.tar.gz", hash = "sha256:e321e263b6a97aafc0790c3cfb3c04656b7066e6738c37fffcca95d803c9fba5", size = 33238, upload-time = "2023-04-17T21:41:16.041Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/ac/3c494dd7ec5122cff8252c1a209b282c0867af029f805ae9befd73ae37eb/zict-3.0.0.tar.gz", hash = "sha256:e321e263b6a97aafc0790c3cfb3c04656b7066e6738c37fffcca95d803c9fba5", size = 33238 } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl", hash = "sha256:5796e36bd0e0cc8cf0fbc1ace6a68912611c1dbd74750a3f3026b9b9d6a327ae", size = 43332, upload-time = "2023-04-17T21:41:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl", hash = "sha256:5796e36bd0e0cc8cf0fbc1ace6a68912611c1dbd74750a3f3026b9b9d6a327ae", size = 43332 }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, ] [[package]] @@ -4370,38 +5246,22 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, - { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, - { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, - { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, - { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, - { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, - { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, - { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, - { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, - { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, - { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, - { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, ] diff --git a/vulnerability-analysis.code-workspace b/vulnerability-analysis.code-workspace index 2ce1e558c..855f568e6 100644 --- a/vulnerability-analysis.code-workspace +++ b/vulnerability-analysis.code-workspace @@ -35,7 +35,6 @@ { "args": [ "serve", - "--dask_workers=threads", "--config_file=configs/config.yml", "--host", "0.0.0.0", @@ -106,7 +105,6 @@ ], "python.analysis.inlayHints.pytestParameters": true, "python.analysis.typeCheckingMode": "basic", - "python.languageServer": "Pylance", "python.envFile": "${workspaceFolder:workspace}/.env", "python.testing.pytestArgs": [ "-s",