From f0249f8aa3a3876269af044181ecd25ef43b7a46 Mon Sep 17 00:00:00 2001 From: Tad ZeMicheal Date: Tue, 28 Jan 2025 14:08:45 -0800 Subject: [PATCH 01/48] Initial commit --- README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..d79ca4673 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Vulnerability Analysis + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab-master.nvidia.com/morpheus/agent-morpheus/vulnerability-analysis.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab-master.nvidia.com/morpheus/agent-morpheus/vulnerability-analysis/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README + +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. From 429f2f52ede4d3424e153d578604be2584beb20b Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Wed, 4 Feb 2026 19:09:51 -0500 Subject: [PATCH 02/48] Update to NAT 1.4.0 (#172) * Add first GitHub Actions workflow to test Deploy notebook (#169) * feat: Add first GitHub Actions workflow * Add pull request trigger * fix: update from review commnets --------- Co-authored-by: Eli Fajardo * Implement email notification for workflow results (#171) Added email notification step to workflow with results. * Add dask config options to config.yml * Concurrency improvements * Update to NAT 1.4 prerelease with async endpoint fix * Update to nvidia-nat 1.4.0 * Update to import to use langchain_classic after NAT upgrade to langchain 1.2.8 * Remove note in README about server log visibility issue for async endpoint since it was fixed in NAT 1.4. * Add explicit dependencies for langchain-classic and filelock * Update the file locking cleanup code to use a non-blocking acquire to avoid the race condition * Guard fetch/checkout when no origin remote exists * Remove commented line from workspace file * Add bokeh dependency for dask dashboard * Add dask dashboard port mapping to docker compose * Add .dockerignore * Mention Dask dashboard in readme --------- Co-authored-by: ryanzhang1230 --- .dockerignore | 36 ++ .../workflows/test-deploy-cve-notebook.yml | 122 ++++++ README.md | 8 +- docker-compose.yml | 3 +- pyproject.toml | 12 +- src/vuln_analysis/functions/cve_agent.py | 6 +- .../functions/cve_generate_vdbs.py | 5 +- src/vuln_analysis/tools/local_vdb.py | 2 +- src/vuln_analysis/utils/concurrency.py | 109 +++++ src/vuln_analysis/utils/document_embedding.py | 38 +- src/vuln_analysis/utils/full_text_search.py | 2 +- src/vuln_analysis/utils/git_utils.py | 14 +- src/vuln_analysis/utils/serp_api_wrapper.py | 17 +- .../utils/source_code_git_loader.py | 66 +-- uv.lock | 375 +++++++++++------- vulnerability-analysis.code-workspace | 1 - 16 files changed, 596 insertions(+), 220 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/test-deploy-cve-notebook.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..e64b08983 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# 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 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/README.md b/README.md index a1c839bec..eed89e007 100644 --- a/README.md +++ b/README.md @@ -494,13 +494,9 @@ The status of the request can then be polled using `/generate/async/job/{job_id} curl --request GET --url http://localhost:26466/generate/async/job/f79951ee-aeea-4a2d-b103-a0a8c15e314b ``` +When running `nat serve`, a Dask cluster is automatically started to handle async request processing. You can monitor the Dask cluster and view task execution details by accessing the Dask dashboard at http://localhost:8787/status. The dashboard provides real-time insights into task scheduling, worker status, and performance metrics, which is particularly useful when processing multiple concurrent requests via the `/generate/async` endpoint. + > [!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 diff --git a/docker-compose.yml b/docker-compose.yml index 441c9f8fc..38d4bc020 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: - "${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 @@ -74,7 +75,7 @@ services: - EMBEDDER_MODEL_NAME=${EMBEDDER_MODEL_NAME:-nvidia/nv-embedqa-e5-v5} volumes: - ./:/workspace - - /workspace/.venv + - /workspace/.venv # Anonymous volume prevents host .venv from being mounted cap_add: - sys_nice depends_on: diff --git a/pyproject.toml b/pyproject.toml index 610ff8903..83f3ce93b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,19 +14,21 @@ dependencies = [ "aiolimiter~=1.2", "aioresponses==0.7.6", "arize-phoenix", # Install phoenix server, which is not included in nvidia-nat phoenix telemetry requirements + "bokeh>=3.1.0", # Required for Dask dashboard visualization when using nat serve "brotli", "esprima", "faiss-cpu==1.9.0", "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. + "langchain", + "langchain-classic", + "langchain-core", + "filelock", "nbformat", "nemollm", - "nvidia-nat[async_endpoints,langchain,phoenix,profiling]~=1.3.0", - "openinference-instrumentation-langchain~=0.1.31", + "nvidia-nat[async_endpoints,langchain,phoenix,profiling]~=1.4.0", + "openinference-instrumentation-langchain", "ordered_set", "pydpkg==1.9.4", "rank_bm25==0.2.2", diff --git a/src/vuln_analysis/functions/cve_agent.py b/src/vuln_analysis/functions/cve_agent.py index 44f241dbc..a6e15a578 100644 --- a/src/vuln_analysis/functions/cve_agent.py +++ b/src/vuln_analysis/functions/cve_agent.py @@ -21,9 +21,9 @@ 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_classic.agents import AgentExecutor +from langchain_classic.agents import create_react_agent +from langchain_classic.agents.agent import RunnableAgent from langchain_core.callbacks import AsyncCallbackHandler from langchain_core.exceptions import OutputParserException from langchain_core.prompts import PromptTemplate diff --git a/src/vuln_analysis/functions/cve_generate_vdbs.py b/src/vuln_analysis/functions/cve_generate_vdbs.py index 3a946057a..362744e10 100644 --- a/src/vuln_analysis/functions/cve_generate_vdbs.py +++ b/src/vuln_analysis/functions/cve_generate_vdbs.py @@ -214,10 +214,11 @@ async def _arun(message: AgentMorpheusInput) -> AgentMorpheusEngineInput: # 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) + # Pass the ref to get_repo_from_path to match the new path structure + repo = get_repo_from_path(config.base_git_dir, si.git_repo, si.ref) 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) + logger.warning("Failed to get commit hash for %s/%s (ref: %s): %s", config.base_git_dir, si.git_repo, si.ref, e) continue except Exception as e: diff --git a/src/vuln_analysis/tools/local_vdb.py b/src/vuln_analysis/tools/local_vdb.py index df7491bfb..fc9982972 100644 --- a/src/vuln_analysis/tools/local_vdb.py +++ b/src/vuln_analysis/tools/local_vdb.py @@ -40,7 +40,7 @@ class LocalVDBRetrieverToolConfig(FunctionBaseConfig, name="local_vdb_retriever" @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_classic.chains.retrieval_qa.base import RetrievalQA from langchain_community.vectorstores import FAISS from langchain_core.prompts import PromptTemplate diff --git a/src/vuln_analysis/utils/concurrency.py b/src/vuln_analysis/utils/concurrency.py index 85b597eca..1c2819e27 100644 --- a/src/vuln_analysis/utils/concurrency.py +++ b/src/vuln_analysis/utils/concurrency.py @@ -18,16 +18,27 @@ 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 contextvars +import hashlib +import logging +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 +logger = logging.getLogger(__name__) + # 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 @@ -86,3 +97,101 @@ 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 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/am_cache/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/am_cache/locks) + + Returns: + Path to the lock file + """ + if lock_dir is None: + lock_dir = Path(".cache/am_cache/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/am_cache/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/am_cache/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) diff --git a/src/vuln_analysis/utils/document_embedding.py b/src/vuln_analysis/utils/document_embedding.py index 861f1eac7..301f10f58 100644 --- a/src/vuln_analysis/utils/document_embedding.py +++ b/src/vuln_analysis/utils/document_embedding.py @@ -17,6 +17,7 @@ import json import logging import os +import re import sys import time import typing @@ -24,9 +25,9 @@ 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_classic.docstore.document import Document +from langchain_classic.text_splitter import Language +from langchain_classic.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 @@ -36,6 +37,7 @@ from langchain_core.document_loaders.blob_loaders import Blob from vuln_analysis.data_models.input import SourceDocumentsInfo +from vuln_analysis.utils.concurrency import file_lock from vuln_analysis.utils.js_extended_parser import ExtendedJavaScriptSegmenter from vuln_analysis.utils.source_code_git_loader import SourceCodeGitLoader @@ -307,6 +309,10 @@ def get_repo_path(self, source_info: SourceDocumentsInfo): """ Returns the path to the git repository for a source document info. + Each combination of git_repo and ref gets its own directory to prevent + conflicts when multiple refs of the same repository are being processed + concurrently. + Parameters ---------- source_info : SourceDocumentsInfo @@ -317,7 +323,14 @@ def get_repo_path(self, source_info: SourceDocumentsInfo): Path Returns the path to the git repository. """ - return self._git_directory / PurePath(source_info.git_repo) + # Include the ref in the path to avoid conflicts when multiple refs + # of the same repo are processed concurrently + base_path = self._git_directory / PurePath(source_info.git_repo) + + # Sanitize ref name for use in filesystem path (remove special chars) + safe_ref = re.sub(r'[^\w\-.]', '_', source_info.ref) + + return base_path / safe_ref def collect_documents(self, source_info: SourceDocumentsInfo) -> list[Document]: """ @@ -481,12 +494,17 @@ def build_vdbs(self, # 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) + # Use file lock to prevent race conditions when multiple processes check/create/load the same VDB + with file_lock(vdb_output_dir, timeout=1800): + # Re-check existence after acquiring lock in case another process created it + 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 diff --git a/src/vuln_analysis/utils/full_text_search.py b/src/vuln_analysis/utils/full_text_search.py index 58b6fcb21..f5c3d2d0b 100644 --- a/src/vuln_analysis/utils/full_text_search.py +++ b/src/vuln_analysis/utils/full_text_search.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Iterable -from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_classic.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 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/serp_api_wrapper.py b/src/vuln_analysis/utils/serp_api_wrapper.py index 2c4db8250..9431cf1e9 100644 --- a/src/vuln_analysis/utils/serp_api_wrapper.py +++ b/src/vuln_analysis/utils/serp_api_wrapper.py @@ -14,7 +14,7 @@ # limitations under the License. import aiohttp -from langchain.utils.env import get_from_env +from langchain_classic.utils.env import get_from_env from langchain_community.utilities import SerpAPIWrapper from pydantic import model_validator @@ -48,11 +48,10 @@ async def _session_get_with_retry(self, session: aiohttp.ClientSession, url: str 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." - ) + 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}") @@ -90,10 +89,8 @@ def _process_response(res: dict) -> str: 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_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: diff --git a/src/vuln_analysis/utils/source_code_git_loader.py b/src/vuln_analysis/utils/source_code_git_loader.py index 61d27293d..c54fc975c 100644 --- a/src/vuln_analysis/utils/source_code_git_loader.py +++ b/src/vuln_analysis/utils/source_code_git_loader.py @@ -24,6 +24,8 @@ from langchain_core.document_loaders.blob_loaders import Blob from tqdm import tqdm +from vuln_analysis.utils.concurrency import file_lock + PathLike = typing.Union[str, os.PathLike] logger = logging.getLogger(__name__) @@ -80,6 +82,9 @@ def load_repo(self): """ Load the Git repository and return the GitPython `Repo` object. + Uses file locking to prevent concurrent access issues when multiple processes + try to clone, fetch, or checkout the same repository simultaneously. + Returns ------- Repo @@ -98,36 +103,47 @@ def load_repo(self): 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) + # Use file lock to prevent concurrent git operations on the same repository + with file_lock(Path(self.repo_path), timeout=600): + if 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())) - # 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) - else: - repo = Repo(self.repo_path) - logger.info("Using existing Git repo at path: '%s' @ '%s'", self.repo_path, self.ref) + # Check if "origin" remote exists before attempting fetch + has_origin_remote = any(remote.name == "origin" for remote in repo.remotes) - # Reliable way to check out the ref using a shallow clone - repo.git.fetch("origin", self.ref, depth=1) - repo.git.checkout("FETCH_HEAD") + if has_origin_remote: + # Reliable way to check out the ref using a shallow clone + repo.git.fetch("origin", self.ref, depth=1) + repo.git.checkout("FETCH_HEAD") + else: + # For local-only repos without origin remote, directly checkout the ref + logger.debug("No 'origin' remote found, checking out ref directly: %s", self.ref) + repo.git.checkout(self.ref) - logger.info("Loaded Git repository at path: '%s' @ '%s'", self.repo_path, self.ref) + logger.info("Loaded Git repository at path: '%s' @ '%s'", self.repo_path, self.ref) self._repo = repo diff --git a/uv.lock b/uv.lock index d6f8e9779..deef54de3 100644 --- a/uv.lock +++ b/uv.lock @@ -197,6 +197,15 @@ 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" }, ] +[[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, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -388,6 +397,28 @@ 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" }, ] +[[package]] +name = "bokeh" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "jinja2" }, + { name = "narwhals" }, + { 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 = "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, upload-time = "2026-01-06T00:20:06.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131, upload-time = "2026-01-06T00:20:04.917Z" }, +] + [[package]] name = "boto3" version = "1.40.61" @@ -718,21 +749,21 @@ wheels = [ [[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 = "importlib-metadata", marker = "python_full_version < '3.12'" }, { 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, upload-time = "2026-01-30T21:04:20.54Z" } 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, upload-time = "2026-01-30T21:04:18.363Z" }, ] [[package]] @@ -794,7 +825,7 @@ wheels = [ [[package]] name = "distributed" -version = "2023.6.0" +version = "2026.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -813,9 +844,9 @@ 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, upload-time = "2026-01-30T21:07:03.307Z" } 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, upload-time = "2026-01-30T21:07:01.363Z" }, ] [[package]] @@ -913,16 +944,17 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.14" +version = "0.128.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, ] [[package]] @@ -1299,23 +1331,21 @@ wheels = [ [[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, upload-time = "2026-02-02T10:46:58.287Z" } 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/94/cb/8f5141b3c21d1ecdf87852506eb583fec497c7e9803a168fe4aec64252bb/huggingface_hub-0.36.1-py3-none-any.whl", hash = "sha256:c6fa8a8f7b8559bc624ebb7e218fb72171b30f6049ebe08f8bfc2a44b38ece50", size = 566283, upload-time = "2026-02-02T10:46:56.459Z" }, ] [[package]] @@ -1561,25 +1591,21 @@ wheels = [ [[package]] name = "langchain" -version = "0.3.27" +version = "1.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, - { name = "langchain-text-splitters" }, - { name = "langsmith" }, + { name = "langgraph" }, { 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/52/b7/a1d95dbb58e5e82dbd05e3730e2d4b99f784a4c6d39435579a1c2b8a8d12/langchain-1.2.8.tar.gz", hash = "sha256:d2bc45f8279f6291b152f28df3bb060b27c9a71163fe2e2a1ac878bd314d0dec", size = 558326, upload-time = "2026-02-02T15:51:59.425Z" } 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/66/1a/e1cabc08d8b12349fa6a898f033cc6b00a9a031b470582f4a9eb4cf8e55b/langchain-1.2.8-py3-none-any.whl", hash = "sha256:74a9595420b90e2fd6dc42e323e5e6c9f2a5d059b0ab51e4ad383893b86f8fbe", size = 108986, upload-time = "2026-02-02T15:51:58.465Z" }, ] [[package]] name = "langchain-aws" -version = "0.2.35" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, @@ -1588,9 +1614,27 @@ dependencies = [ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { 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, upload-time = "2025-11-24T14:35:24.216Z" } 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, upload-time = "2025-11-24T14:35:23.004Z" }, +] + +[[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, upload-time = "2025-12-23T22:55:22.615Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0f/eab87f017d7fe28e8c11fff614f4cdbfae32baadb77d0f79e9f922af1df2/langchain_classic-1.0.1-py3-none-any.whl", hash = "sha256:131d83a02bb80044c68fedc1ab4ae885d5b8f8c2c742d8ab9e7534ad9cda8e80", size = 1040666, upload-time = "2025-12-23T22:55:21.025Z" }, ] [[package]] @@ -1619,7 +1663,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "0.3.80" +version = "1.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1629,69 +1673,84 @@ dependencies = [ { name = "pyyaml" }, { name = "tenacity" }, { name = "typing-extensions" }, + { name = "uuid-utils" }, ] -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/75/cc/55bf57b83cbc164cbf84cbf0c5e4fb640d673546af131db70797b97b125b/langchain_core-1.2.8.tar.gz", hash = "sha256:76d933c3f4cfd8484d8131c39bf25f562e2df4d0d5fe3218e05ff773210713b6", size = 814506, upload-time = "2026-02-02T15:35:33.056Z" } 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/cc/d4/37fef9639b701c1fb1eea9e68447b72d86852ca3dc3253cdfd9c0afe228d/langchain_core-1.2.8-py3-none-any.whl", hash = "sha256:c732301272d63cfbcd75d114540257678627878f11b87046241272a25ba12ea7", size = 495753, upload-time = "2026-02-02T15:35:31.284Z" }, +] + +[[package]] +name = "langchain-huggingface" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "langchain-core" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/2c/4fddeb3387baa05b6a95870ad514f649cafb46e0c0ef9caf949d974e55d2/langchain_huggingface-1.2.0.tar.gz", hash = "sha256:18a2d79955271261fb245b233fea6aa29625576e841f2b4f5bee41e51cc70949", size = 255602, upload-time = "2025-12-12T22:19:51.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/ce/502157ef7390a31cc67e5873ad66e737a25d1d33fcf6936e5c9a0a451409/langchain_huggingface-1.2.0-py3-none-any.whl", hash = "sha256:0ff6a17d3eb36ce2304f446e3285c74b59358703e8f7916c15bfcf9ec7b57bf1", size = 30671, upload-time = "2025-12-12T22:19:50.023Z" }, ] [[package]] name = "langchain-litellm" -version = "0.2.3" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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/80/01/e73baaaf7af7b4f403f5f83e884d33b286f0ba8995c4edebc909d512cbf3/langchain_litellm-0.4.0.tar.gz", hash = "sha256:2bb58d3b805cbddf3944a546b953f78f124e278ce2e726045fe0d87ac8b06e8f", size = 15297, upload-time = "2026-02-02T23:23:25.753Z" } 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/4e/95b1b4f9c40db0f2ef40b014f82c6f495b789372c223542e777cd9599275/langchain_litellm-0.4.0-py3-none-any.whl", hash = "sha256:5842d753de54e91166a1ef39476628433d4312af7941eb74e01db1ea93e4e313", size = 15674, upload-time = "2026-02-02T23:23:26.904Z" }, ] [[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, upload-time = "2026-01-05T10:01:16.386Z" } 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, upload-time = "2026-01-05T10:01:15.186Z" }, ] [[package]] name = "langchain-nvidia-ai-endpoints" -version = "0.3.19" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "filetype" }, { name = "langchain-core" }, ] -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/5a/9e/30814da280f7a79b168f83180f6a0396c166f86a566e56bb9877bf562611/langchain_nvidia_ai_endpoints-1.0.3.tar.gz", hash = "sha256:11c48fd24e4a9d4c86c65bcef943400f4e709497c93254c7dc97c43f68c2be89", size = 46526, upload-time = "2026-01-28T22:04:33.93Z" } 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/67/04/c83f61106a245b74de11c1e075c1cc1e70462ece1dd9fc0584ad992a776d/langchain_nvidia_ai_endpoints-1.0.3-py3-none-any.whl", hash = "sha256:e5f170ad0a335637298bb90fb3df119793821e316355f61ab82f0106913eebbf", size = 50130, upload-time = "2026-01-28T22:04:33.065Z" }, ] [[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, upload-time = "2026-01-07T19:44:59.728Z" } 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, upload-time = "2026-01-07T19:44:58.629Z" }, ] [[package]] name = "langchain-tavily" -version = "0.2.13" +version = "0.2.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1699,26 +1758,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/e6/32/f7b5487efbcd5fca5d4095f03dce7dcf0301ed81b2505d9888427c03619b/langchain_tavily-0.2.17.tar.gz", hash = "sha256:738abd790c50f19565023ad279c8e47e87e1aeb971797fec30a614b418ae6503", size = 25298, upload-time = "2026-01-18T13:09:04.112Z" } 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/35/f9/bb6f1cea2a19215e4169a3bcec3af707ff947cf62f6ef7d28e7280f03e29/langchain_tavily-0.2.17-py3-none-any.whl", hash = "sha256:da4e5e7e328d054dc70a9c934afa1d1e62038612106647ff81ad8bfbe3622256", size = 30734, upload-time = "2026-01-18T13:09:03.1Z" }, ] [[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, upload-time = "2025-12-14T01:15:38.659Z" } 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, upload-time = "2025-12-14T01:15:37.382Z" }, ] [[package]] name = "langgraph" -version = "0.6.11" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -1728,9 +1787,9 @@ 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/72/5b/f72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f/langgraph-1.0.7.tar.gz", hash = "sha256:0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a", size = 497098, upload-time = "2026-01-22T16:57:47.303Z" } 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/7e/0e/fe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14/langgraph-1.0.7-py3-none-any.whl", hash = "sha256:9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2", size = 157353, upload-time = "2026-01-22T16:57:45.997Z" }, ] [[package]] @@ -1748,28 +1807,28 @@ wheels = [ [[package]] name = "langgraph-prebuilt" -version = "0.6.5" +version = "1.0.7" 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/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" } 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/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.2.9" +version = "0.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "orjson" }, ] -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/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" } 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/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" }, ] [[package]] @@ -1934,28 +1993,6 @@ 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" }, ] -[[package]] -name = "mcp" -version = "1.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { 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" } -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" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2064,6 +2101,15 @@ 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" }, ] +[[package]] +name = "narwhals" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, +] + [[package]] name = "nbformat" version = "5.10.4" @@ -2104,6 +2150,15 @@ 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" }, ] +[[package]] +name = "nest-asyncio2" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/eb/ecf8bbf9d22a4e8f7be1628336fe0202da7660790053aa28abeb6c15eb14/nest_asyncio2-1.7.1.tar.gz", hash = "sha256:a1fe5bbbd20894dcceb1842322d74992c5834d5ab692af2c4f59a9a4fcf75fe8", size = 13797, upload-time = "2025-11-20T20:46:07.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/48/c1f1ddcfd04bba60470235c2f83733ecff43ebe068dc7715aab60bc92ad8/nest_asyncio2-1.7.1-py3-none-any.whl", hash = "sha256:f83bc1744c3cfa7d47fd29431e5e168db6cb76eda1bb20108955c32f60d7eddf", size = 7504, upload-time = "2025-11-20T20:46:05.704Z" }, +] + [[package]] name = "networkx" version = "3.5" @@ -2184,7 +2239,7 @@ wheels = [ [[package]] name = "nvidia-nat" -version = "1.3.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aioboto3" }, @@ -2197,18 +2252,19 @@ dependencies = [ { 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 = "pandas" }, { name = "pip" }, { name = "pkce" }, { name = "pkginfo" }, { name = "platformdirs" }, + { name = "plotly" }, { name = "pydantic" }, { name = "pymilvus" }, { name = "python-dotenv" }, @@ -2216,11 +2272,12 @@ dependencies = [ { name = "ragas" }, { name = "rich" }, { name = "tabulate" }, + { 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/29/c6/21cf6bd5ce2126264912a696f229a559b6bf1483b712a22b5b8e787405ba/nvidia_nat-1.4.0-py3-none-any.whl", hash = "sha256:82356d49498597ca11be07082133112795ea2d7b3ec1f9c572e8dce23773f7dd", size = 1011876, upload-time = "2026-02-03T03:07:20.462Z" }, ] [package.optional-dependencies] @@ -2245,12 +2302,14 @@ profiling = [ [[package]] name = "nvidia-nat-langchain" -version = "1.3.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "langchain-aws" }, + { name = "langchain-classic" }, { name = "langchain-core" }, + { name = "langchain-huggingface" }, { name = "langchain-litellm" }, { name = "langchain-milvus" }, { name = "langchain-nvidia-ai-endpoints" }, @@ -2260,12 +2319,12 @@ dependencies = [ { name = "nvidia-nat" }, ] 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/e5/83/9bf4ede51d0f2be7a3c5c5ec4c2bfa56f52a084ecedb8fb67cc5e8f5e776/nvidia_nat_langchain-1.4.0-py3-none-any.whl", hash = "sha256:7b7ac79d28f7a4bf21a68ee26e969fcf0b2fc84628042e162c74d4a3233cdeb6", size = 64528, upload-time = "2026-02-03T03:03:47.095Z" }, ] [[package]] name = "nvidia-nat-opentelemetry" -version = "1.3.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-nat" }, @@ -2274,12 +2333,12 @@ dependencies = [ { 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/a9/6f/fd6b03ef118840a2dac1328712239c5fa94ef1f748b8eaaf929a94cf07c2/nvidia_nat_opentelemetry-1.4.0-py3-none-any.whl", hash = "sha256:20430d00ace474eac2c348631fb20a5f3b48af9f7da033083bfdee69d4da5950", size = 67395, upload-time = "2026-02-03T03:08:15.838Z" }, ] [[package]] name = "nvidia-nat-phoenix" -version = "1.3.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arize-phoenix-otel" }, @@ -2287,12 +2346,12 @@ dependencies = [ { 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/2b/c4/2b2a37a98615c36ee09b4e3168e175434f3ef1270d29046a7953b7389a3b/nvidia_nat_phoenix-1.4.0-py3-none-any.whl", hash = "sha256:bca503c5ef1455086aac2ec840751b6304d616b3eda49998f5bd0c9aee16d2e6", size = 53388, upload-time = "2026-02-03T03:20:35.1Z" }, ] [[package]] name = "nvidia-nat-profiling" -version = "1.3.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib" }, @@ -2301,7 +2360,7 @@ dependencies = [ { name = "scikit-learn" }, ] 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/48/1a/824445541e8274696ebdfbc5eb1dadbfd40e5337ab91e587a0de9838372d/nvidia_nat_profiling-1.4.0-py3-none-any.whl", hash = "sha256:b71c8cc8fd4866dada425477872b4895fa8bcea83bf787197fa88be39e878e7b", size = 47931, upload-time = "2026-02-03T03:08:34.369Z" }, ] [[package]] @@ -2733,6 +2792,19 @@ 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" }, ] +[[package]] +name = "plotly" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -3164,19 +3236,6 @@ 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" }, ] -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -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" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -3332,15 +3391,15 @@ wheels = [ [[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, upload-time = "2026-02-01T16:20:47.908Z" } 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, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] @@ -3502,15 +3561,6 @@ 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" }, ] -[[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" } -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" }, -] - [[package]] name = "six" version = "1.17.0" @@ -3610,18 +3660,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "sse-starlette" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -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" } -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" }, -] - [[package]] name = "starlette" version = "0.46.2" @@ -3889,19 +3927,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "typer-slim" -version = "0.20.0" -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" } -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" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -3974,24 +3999,53 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" 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/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } 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/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" } +wheels = [ + { 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, upload-time = "2026-01-20T20:37:09.843Z" }, + { 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, upload-time = "2026-01-20T20:37:18.767Z" }, + { 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, upload-time = "2026-01-20T20:37:11.794Z" }, + { 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, upload-time = "2026-01-20T20:37:12.818Z" }, + { 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, upload-time = "2026-01-20T20:37:01.512Z" }, + { 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, upload-time = "2026-01-20T20:37:22.881Z" }, + { 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, upload-time = "2026-01-20T20:37:10.808Z" }, + { 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, upload-time = "2026-01-20T20:37:06.152Z" }, + { 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, upload-time = "2026-01-20T20:37:21.816Z" }, + { 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, upload-time = "2026-01-20T20:37:19.691Z" }, + { 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, upload-time = "2026-01-20T20:37:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" }, + { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" }, ] [[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, upload-time = "2025-12-21T14:16:22.45Z" } 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, upload-time = "2025-12-21T14:16:21.041Z" }, ] [package.optional-dependencies] @@ -4034,15 +4088,17 @@ dependencies = [ { name = "aiolimiter" }, { name = "aioresponses" }, { name = "arize-phoenix" }, + { name = "bokeh" }, { name = "brotli" }, { name = "esprima" }, { name = "faiss-cpu" }, + { name = "filelock" }, { name = "gitpython" }, { name = "google-search-results" }, { name = "json5" }, { name = "langchain" }, + { name = "langchain-classic" }, { name = "langchain-core" }, - { name = "mcp" }, { name = "nbformat" }, { name = "nemollm" }, { name = "nvidia-nat", extra = ["async-endpoints", "langchain", "phoenix", "profiling"] }, @@ -4081,19 +4137,21 @@ requires-dist = [ { name = "aiolimiter", specifier = "~=1.2" }, { name = "aioresponses", specifier = "==0.7.6" }, { name = "arize-phoenix" }, + { name = "bokeh", specifier = ">=3.1.0" }, { name = "brotli" }, { name = "esprima" }, { name = "faiss-cpu", specifier = "==1.9.0" }, + { 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 = "langchain" }, + { name = "langchain-classic" }, + { name = "langchain-core" }, { 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 = "nvidia-nat", extras = ["async-endpoints", "langchain", "phoenix", "profiling"], specifier = "~=1.4.0" }, + { name = "openinference-instrumentation-langchain" }, { name = "ordered-set" }, { name = "pydpkg", specifier = "==1.9.4" }, { name = "rank-bm25", specifier = "==0.2.2" }, @@ -4287,6 +4345,15 @@ wheels = [ { 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" }, ] +[[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, upload-time = "2025-11-22T11:31:51.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5c/2c189d18d495dd0fa3f27ccc60762bbc787eed95b9b0147266e72bb76585/xyzservices-2025.11.0-py3-none-any.whl", hash = "sha256:de66a7599a8d6dad63980b77defd1d8f5a5a9cb5fc8774ea1c6e89ca7c2a3d2f", size = 93916, upload-time = "2025-11-22T11:31:50.525Z" }, +] + [[package]] name = "yapf" version = "0.43.0" diff --git a/vulnerability-analysis.code-workspace b/vulnerability-analysis.code-workspace index 2ce1e558c..114728379 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", From 64c760d3fa94fe483b99da6aae749dcc4efccb50 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Wed, 11 Mar 2026 14:46:33 -0400 Subject: [PATCH 03/48] Initial cve-agent-v3 NAT migration Made-with: Cursor --- .gitignore | 1 + pyproject.toml | 29 +- .../configs/config-container-analyzer.yml | 56 + .../configs/config-cve-researcher.yml | 31 + src/vuln_analysis/configs/config-eval.yml | 217 -- .../config-exploitability-analyzer.yml | 51 + src/vuln_analysis/configs/config-tracing.yml | 223 -- src/vuln_analysis/configs/config-ttc.yml | 203 -- .../configs/config-vex-categorizer.yml | 16 + src/vuln_analysis/configs/config.yml | 334 +- .../configs/openapi/openapi.json | 2919 ----------------- .../data/eval_datasets/eval_dataset.json | 103 - .../examples/container_analyzer_input.json | 6 + .../data/examples/cve_researcher_input.json | 5 + .../exploitability_analyzer_input.json | 5 + .../data/examples/pipeline_input.json | 8 + .../data/examples/vex_categorizer_input.json | 4 + .../morpheus:23.11-runtime.json | 67 - .../morpheus:24.03-runtime.json | 64 - .../profiler_datasets/profiler_dataset.json | 7 - src/vuln_analysis/data/sboms/README.md | 75 - .../morpheus/morpheus:v23.11.01-runtime.sbom | 465 --- .../morpheus/morpheus:v24.03.02-runtime.sbom | 493 --- src/vuln_analysis/data_models/__init__.py | 14 - src/vuln_analysis/data_models/common.py | 76 - .../data_models/container_analyzer.py | 42 + src/vuln_analysis/data_models/cve_intel.py | 333 -- .../data_models/cve_researcher.py | 29 + src/vuln_analysis/data_models/dependencies.py | 59 - src/vuln_analysis/data_models/eval_input.py | 89 - .../data_models/exploitability_analyzer.py | 117 + src/vuln_analysis/data_models/info.py | 64 - src/vuln_analysis/data_models/input.py | 219 +- src/vuln_analysis/data_models/output.py | 132 +- src/vuln_analysis/data_models/state.py | 54 +- src/vuln_analysis/data_models/vdb_type.py | 21 - .../data_models/vex_categorizer.py | 40 + src/vuln_analysis/eval/evaluators/accuracy.py | 240 -- .../eval/evaluators/consistency.py | 151 - src/vuln_analysis/eval/parse_eval_input.py | 190 -- .../visualizations/box_and_whisker_plot.py | 131 - src/vuln_analysis/functions/__init__.py | 14 - .../functions/container_analyzer.py | 143 + src/vuln_analysis/functions/cve_agent.py | 276 -- .../functions/cve_check_vuln_deps.py | 138 - src/vuln_analysis/functions/cve_checklist.py | 94 - .../functions/cve_fetch_intel.py | 75 - .../functions/cve_file_output.py | 83 - .../functions/cve_generate_vdbs.py | 279 -- .../functions/cve_http_output.py | 59 - src/vuln_analysis/functions/cve_justify.py | 98 - .../functions/cve_process_sbom.py | 119 - src/vuln_analysis/functions/cve_researcher.py | 84 + src/vuln_analysis/functions/cve_summarize.py | 91 - .../functions/exploitability_analyzer.py | 160 + .../functions/vex_categorizer.py | 144 + src/vuln_analysis/prompts/__init__.py | 0 .../prompts/container_analyzer.py | 964 ++++++ src/vuln_analysis/prompts/cve_researcher.py | 348 ++ .../prompts/exploitability_analyzer.py | 706 ++++ src/vuln_analysis/prompts/vex_categorizer.py | 444 +++ src/vuln_analysis/register.py | 596 ++-- .../execute_then_select_function.py | 241 -- .../majority_voting_selector.py | 144 - src/vuln_analysis/tools/__init__.py | 14 - src/vuln_analysis/tools/binary_tools.py | 236 ++ src/vuln_analysis/tools/filesystem_tools.py | 348 ++ .../tools/lexical_full_search.py | 59 - src/vuln_analysis/tools/local_vdb.py | 108 - src/vuln_analysis/tools/osv_tools.py | 182 + src/vuln_analysis/tools/serp.py | 44 - src/vuln_analysis/tools/web_tools.py | 129 + src/vuln_analysis/utils/agentic_loop.py | 264 ++ .../utils/checklist_prompt_generator.py | 134 - src/vuln_analysis/utils/clients/__init__.py | 14 - .../utils/clients/first_client.py | 72 - .../utils/clients/ghsa_client.py | 95 - .../utils/clients/intel_client.py | 90 - src/vuln_analysis/utils/clients/nvd_client.py | 344 -- .../utils/clients/rhsa_client.py | 75 - .../utils/clients/ubuntu_client.py | 71 - src/vuln_analysis/utils/document_embedding.py | 522 --- src/vuln_analysis/utils/full_text_search.py | 193 -- src/vuln_analysis/utils/helpers.py | 34 + src/vuln_analysis/utils/intel_retriever.py | 257 -- src/vuln_analysis/utils/intel_utils.py | 156 - src/vuln_analysis/utils/js_extended_parser.py | 103 - .../utils/justification_parser.py | 118 - src/vuln_analysis/utils/llm_engine_utils.py | 298 -- src/vuln_analysis/utils/output_formatter.py | 687 ---- src/vuln_analysis/utils/pre_triage.py | 319 ++ src/vuln_analysis/utils/prompting.py | 308 -- src/vuln_analysis/utils/report_generator.py | 88 + src/vuln_analysis/utils/report_parser.py | 11 + src/vuln_analysis/utils/serp_api_wrapper.py | 103 - .../utils/source_code_git_loader.py | 208 -- .../utils/vulnerable_dependency_checker.py | 680 ---- src/vuln_analysis/utils/web_researcher.py | 59 + 98 files changed, 5652 insertions(+), 13454 deletions(-) create mode 100644 src/vuln_analysis/configs/config-container-analyzer.yml create mode 100644 src/vuln_analysis/configs/config-cve-researcher.yml delete mode 100644 src/vuln_analysis/configs/config-eval.yml create mode 100644 src/vuln_analysis/configs/config-exploitability-analyzer.yml delete mode 100644 src/vuln_analysis/configs/config-tracing.yml delete mode 100644 src/vuln_analysis/configs/config-ttc.yml create mode 100644 src/vuln_analysis/configs/config-vex-categorizer.yml delete mode 100644 src/vuln_analysis/configs/openapi/openapi.json delete mode 100644 src/vuln_analysis/data/eval_datasets/eval_dataset.json create mode 100644 src/vuln_analysis/data/examples/container_analyzer_input.json create mode 100644 src/vuln_analysis/data/examples/cve_researcher_input.json create mode 100644 src/vuln_analysis/data/examples/exploitability_analyzer_input.json create mode 100644 src/vuln_analysis/data/examples/pipeline_input.json create mode 100644 src/vuln_analysis/data/examples/vex_categorizer_input.json delete mode 100644 src/vuln_analysis/data/input_messages/morpheus:23.11-runtime.json delete mode 100644 src/vuln_analysis/data/input_messages/morpheus:24.03-runtime.json delete mode 100644 src/vuln_analysis/data/profiler_datasets/profiler_dataset.json delete mode 100644 src/vuln_analysis/data/sboms/README.md delete mode 100644 src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v23.11.01-runtime.sbom delete mode 100644 src/vuln_analysis/data/sboms/nvcr.io/nvidia/morpheus/morpheus:v24.03.02-runtime.sbom delete mode 100644 src/vuln_analysis/data_models/common.py create mode 100644 src/vuln_analysis/data_models/container_analyzer.py delete mode 100644 src/vuln_analysis/data_models/cve_intel.py create mode 100644 src/vuln_analysis/data_models/cve_researcher.py delete mode 100644 src/vuln_analysis/data_models/dependencies.py delete mode 100644 src/vuln_analysis/data_models/eval_input.py create mode 100644 src/vuln_analysis/data_models/exploitability_analyzer.py delete mode 100644 src/vuln_analysis/data_models/info.py delete mode 100644 src/vuln_analysis/data_models/vdb_type.py create mode 100644 src/vuln_analysis/data_models/vex_categorizer.py delete mode 100644 src/vuln_analysis/eval/evaluators/accuracy.py delete mode 100644 src/vuln_analysis/eval/evaluators/consistency.py delete mode 100644 src/vuln_analysis/eval/parse_eval_input.py delete mode 100644 src/vuln_analysis/eval/visualizations/box_and_whisker_plot.py create mode 100644 src/vuln_analysis/functions/container_analyzer.py delete mode 100644 src/vuln_analysis/functions/cve_agent.py delete mode 100644 src/vuln_analysis/functions/cve_check_vuln_deps.py delete mode 100644 src/vuln_analysis/functions/cve_checklist.py delete mode 100644 src/vuln_analysis/functions/cve_fetch_intel.py delete mode 100644 src/vuln_analysis/functions/cve_file_output.py delete mode 100644 src/vuln_analysis/functions/cve_generate_vdbs.py delete mode 100644 src/vuln_analysis/functions/cve_http_output.py delete mode 100644 src/vuln_analysis/functions/cve_justify.py delete mode 100644 src/vuln_analysis/functions/cve_process_sbom.py create mode 100644 src/vuln_analysis/functions/cve_researcher.py delete mode 100644 src/vuln_analysis/functions/cve_summarize.py create mode 100644 src/vuln_analysis/functions/exploitability_analyzer.py create mode 100644 src/vuln_analysis/functions/vex_categorizer.py create mode 100644 src/vuln_analysis/prompts/__init__.py create mode 100644 src/vuln_analysis/prompts/container_analyzer.py create mode 100644 src/vuln_analysis/prompts/cve_researcher.py create mode 100644 src/vuln_analysis/prompts/exploitability_analyzer.py create mode 100644 src/vuln_analysis/prompts/vex_categorizer.py delete mode 100644 src/vuln_analysis/test_time_compute/execute_then_select_function.py delete mode 100644 src/vuln_analysis/test_time_compute/majority_voting_selector.py create mode 100644 src/vuln_analysis/tools/binary_tools.py create mode 100644 src/vuln_analysis/tools/filesystem_tools.py delete mode 100644 src/vuln_analysis/tools/lexical_full_search.py delete mode 100644 src/vuln_analysis/tools/local_vdb.py create mode 100644 src/vuln_analysis/tools/osv_tools.py delete mode 100644 src/vuln_analysis/tools/serp.py create mode 100644 src/vuln_analysis/tools/web_tools.py create mode 100644 src/vuln_analysis/utils/agentic_loop.py delete mode 100644 src/vuln_analysis/utils/checklist_prompt_generator.py delete mode 100644 src/vuln_analysis/utils/clients/__init__.py delete mode 100644 src/vuln_analysis/utils/clients/first_client.py delete mode 100644 src/vuln_analysis/utils/clients/ghsa_client.py delete mode 100644 src/vuln_analysis/utils/clients/intel_client.py delete mode 100644 src/vuln_analysis/utils/clients/nvd_client.py delete mode 100644 src/vuln_analysis/utils/clients/rhsa_client.py delete mode 100644 src/vuln_analysis/utils/clients/ubuntu_client.py delete mode 100644 src/vuln_analysis/utils/document_embedding.py delete mode 100644 src/vuln_analysis/utils/full_text_search.py create mode 100644 src/vuln_analysis/utils/helpers.py delete mode 100644 src/vuln_analysis/utils/intel_retriever.py delete mode 100644 src/vuln_analysis/utils/intel_utils.py delete mode 100644 src/vuln_analysis/utils/js_extended_parser.py delete mode 100644 src/vuln_analysis/utils/justification_parser.py delete mode 100644 src/vuln_analysis/utils/llm_engine_utils.py delete mode 100644 src/vuln_analysis/utils/output_formatter.py create mode 100644 src/vuln_analysis/utils/pre_triage.py delete mode 100644 src/vuln_analysis/utils/prompting.py create mode 100644 src/vuln_analysis/utils/report_generator.py create mode 100644 src/vuln_analysis/utils/report_parser.py delete mode 100644 src/vuln_analysis/utils/serp_api_wrapper.py delete mode 100644 src/vuln_analysis/utils/source_code_git_loader.py delete mode 100644 src/vuln_analysis/utils/vulnerable_dependency_checker.py create mode 100644 src/vuln_analysis/utils/web_researcher.py 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/pyproject.toml b/pyproject.toml index 83f3ce93b..5346e99a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,34 +10,23 @@ 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 - "bokeh>=3.1.0", # Required for Dask dashboard visualization when using nat serve - "brotli", - "esprima", - "faiss-cpu==1.9.0", + "arize-phoenix", + "bokeh>=3.1.0", "gitpython", - "google-search-results==2.4", - "json5", + "httpx>=0.27", "langchain", - "langchain-classic", "langchain-core", + "langgraph>=1.0.5,<2.0.0", "filelock", - "nbformat", - "nemollm", - "nvidia-nat[async_endpoints,langchain,phoenix,profiling]~=1.4.0", + "nvidia-nat[async_endpoints,langchain,phoenix,profiling]==1.5.0rc5", "openinference-instrumentation-langchain", - "ordered_set", + "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"] 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..8f60ea4a5 --- /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: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.1 + max_tokens: 4096 + container_phase2_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.15 + max_tokens: 16384 + +functions: + web_search: + _type: tavily_internet_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-researcher.yml b/src/vuln_analysis/configs/config-cve-researcher.yml new file mode 100644 index 000000000..de8570ffc --- /dev/null +++ b/src/vuln_analysis/configs/config-cve-researcher.yml @@ -0,0 +1,31 @@ +general: + use_uvloop: true + +llms: + cve_researcher_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.15 + max_tokens: 16384 + +functions: + web_search: + _type: tavily_internet_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: 30 + context_window_tokens: 262144 + wrap_up_fraction: 0.8 + output_subdir: vulnerability_reports diff --git a/src/vuln_analysis/configs/config-eval.yml b/src/vuln_analysis/configs/config-eval.yml deleted file mode 100644 index c3aedf564..000000000 --- a/src/vuln_analysis/configs/config-eval.yml +++ /dev/null @@ -1,217 +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 - -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 - 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"] - -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 - -eval: - general: - output_dir: ./.tmp/evaluators - dataset: - _type: custom - file_path: data/eval_datasets/eval_dataset.json - function: vuln_analysis.eval.parse_eval_input.parse_input - max_concurrency: 1 - - evaluators: - status_accuracy: - _type: accuracy - field: status - duplicates_policy: drop_all # Options: drop_all, keep_first, keep_all - label_accuracy: - _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 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..63fa85f6c --- /dev/null +++ b/src/vuln_analysis/configs/config-exploitability-analyzer.yml @@ -0,0 +1,51 @@ +general: + use_uvloop: true + +llms: + exploitability_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + 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 + +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] + 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-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..7ce32a1f6 --- /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: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + 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..0c85b46d6 100644 --- a/src/vuln_analysis/configs/config.yml +++ b/src/vuln_analysis/configs/config.yml @@ -1,213 +1,139 @@ -# 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: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.15 + max_tokens: 16384 + + container_phase1_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.1 + max_tokens: 4096 + + container_phase2_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + 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: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.1 + max_tokens: 16384 + + vex_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.0 + max_tokens: 4096 + +functions: + web_search: + _type: tavily_internet_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 + + cve_researcher: + _type: cve_researcher + llm_name: cve_researcher_llm + tool_names: [web_search, web_fetch, osv_lookup] + max_iterations: 30 + context_window_tokens: 262144 + wrap_up_fraction: 0.8 + loop_detection_threshold: 2 + 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] + 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 + 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/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/examples/container_analyzer_input.json b/src/vuln_analysis/data/examples/container_analyzer_input.json new file mode 100644 index 000000000..9c9ebc3aa --- /dev/null +++ b/src/vuln_analysis/data/examples/container_analyzer_input.json @@ -0,0 +1,6 @@ +{ + "image_name": "nvcr.io/nvidia/morpheus/morpheus: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..d49baf1a6 --- /dev/null +++ b/src/vuln_analysis/data/examples/cve_researcher_input.json @@ -0,0 +1,5 @@ +{ + "cve_id": "CVE-2025-47273", + "package_hint": "setuptools", + "version_hint": "75.8.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..28f691e8e --- /dev/null +++ b/src/vuln_analysis/data/examples/pipeline_input.json @@ -0,0 +1,8 @@ +{ + "cve_id": "CVE-2025-47273", + "image_name": "nvcr.io/nvidia/morpheus/morpheus:25.06-runtime", + "filesystem_path": "/Users/efajardo/containers/morpheus/filesystem", + "image_config_path": "/Users/efajardo/containers/morpheus/image_config.json", + "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..223a9438f --- /dev/null +++ b/src/vuln_analysis/data_models/container_analyzer.py @@ -0,0 +1,42 @@ +from __future__ import annotations +import re +from pydantic import BaseModel, model_validator + + +class ContainerAnalyzerInput(BaseModel): + image_name: 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.filesystem_path: + raise ValueError("filesystem_path must be provided.") + return self + + +class ContainerAnalyzerResult(BaseModel): + image_name: str + report_path: str | None = None + report_content: str | None = None + success: bool = True + error: str | None = None + phase1_skipped: bool = False + tool_call_count: int = 0 + llm_call_count: int = 0 + + +def derive_report_filename(image_name: str) -> str: + name = image_name.strip() + if ":" in name: + path_part, tag = name.rsplit(":", 1) + else: + path_part = name + tag = None + base = path_part.rsplit("/", 1)[-1] + if tag: + return f"{base}_{tag}.md" + return f"{base}.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..5fd97896b --- /dev/null +++ b/src/vuln_analysis/data_models/cve_researcher.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from pydantic import BaseModel, model_validator + + +class CVEResearcherInput(BaseModel): + 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 of cve_id or ghsa_id must be provided.") + return self + + @property + def identifier(self) -> str: + return self.cve_id or self.ghsa_id + + +class CVEResearcherResult(BaseModel): + identifier: str + report_path: str | None = None + report_content: str | None = None + success: bool = True + error: str | None = None + tool_call_count: int = 0 + llm_call_count: int = 0 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..424a2dec1 --- /dev/null +++ b/src/vuln_analysis/data_models/exploitability_analyzer.py @@ -0,0 +1,117 @@ +from __future__ import annotations +import re +from pathlib import Path +from pydantic import BaseModel, Field, model_validator + + +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(BaseModel): + cve_id: str + container_name: str + report_path: str | None = None + report_content: str | None = None + verdict: str | None = None + confidence: str | None = None + success: bool = True + error: str | None = None + tool_call_count: int = 0 + llm_call_count: int = 0 + + +class AffectedRange(BaseModel): + branch: str + lower: str + upper: str + first_fixed: 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) + + +class ParsedContainerReport(BaseModel): + image_name: str + 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 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 + 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..1ef0fbb4d 100644 --- a/src/vuln_analysis/data_models/input.py +++ b/src/vuln_analysis/data_models/input.py @@ -1,188 +1,31 @@ -# 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") - - 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 +from __future__ import annotations +from pydantic import BaseModel, Field, model_validator + + +class PipelineInput(BaseModel): + """Everything needed to run the full 4-agent vulnerability analysis pipeline.""" + cve_id: str | None = None + ghsa_id: str | None = None + package_hint: str | None = None + version_hint: str | None = None + image_name: str = "" + filesystem_path: str = "" + image_config_path: str | None = None + skip_web_research: bool = False + output_dir: str = "outputs" + model: str | None = None + cache: bool = True + + @model_validator(mode="after") + def _check_ids(self): + if not self.cve_id and not self.ghsa_id: + raise ValueError("At least one of cve_id or ghsa_id must be provided.") + if not self.image_name: + raise ValueError("image_name must be provided.") + if not self.filesystem_path: + raise ValueError("filesystem_path must be provided.") + return self + + @property + def identifier(self) -> str: + return self.cve_id or self.ghsa_id diff --git a/src/vuln_analysis/data_models/output.py b/src/vuln_analysis/data_models/output.py index 7d8481329..32e644c5f 100644 --- a/src/vuln_analysis/data_models/output.py +++ b/src/vuln_analysis/data_models/output.py @@ -1,88 +1,44 @@ -# 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 -from pydantic import model_validator - -from .input import AgentMorpheusEngineInput - - -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. - """ - 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 +from __future__ import annotations +from pydantic import BaseModel, Field + + +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 PipelineResult(BaseModel): + """Composite result for the full 4-agent pipeline.""" + cve_id: str + image_name: str + success: bool = False + cve_researcher: StageResult | None = None + container_analyzer: StageResult | None = None + exploitability_analyzer: StageResult | None = None + vex_categorizer: StageResult | None = None + vex_status: str | None = None + vex_justification: str | None = None + elapsed_seconds: float = 0.0 + + @property + def total_tool_calls(self) -> int: + return sum( + (s.tool_call_count if s else 0) + for s in [self.cve_researcher, self.container_analyzer, + 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.container_analyzer, + self.exploitability_analyzer, self.vex_categorizer] + ) diff --git a/src/vuln_analysis/data_models/state.py b/src/vuln_analysis/data_models/state.py index 66973fc19..f8cd4c617 100644 --- a/src/vuln_analysis/data_models/state.py +++ b/src/vuln_analysis/data_models/state.py @@ -1,41 +1,13 @@ -# 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 + cve_report: StageResult | None + container_report: StageResult | None + exploit_report: StageResult | None + vex_result: StageResult | None + vex_status: str | None + vex_justification: str | None + success: bool diff --git a/src/vuln_analysis/data_models/vdb_type.py b/src/vuln_analysis/data_models/vdb_type.py deleted file mode 100644 index 8407e6941..000000000 --- a/src/vuln_analysis/data_models/vdb_type.py +++ /dev/null @@ -1,21 +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 enum import Enum - - -class VdbType(str, Enum): - CODE = "code" - DOC = "doc" 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..1dc6f15c8 --- /dev/null +++ b/src/vuln_analysis/data_models/vex_categorizer.py @@ -0,0 +1,40 @@ +from __future__ import annotations +import re +from pathlib import Path +from pydantic import BaseModel, model_validator + + +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(BaseModel): + cve_id: str + container_name: str + status: str | None = None + justification: str | None = None + reasoning: str | None = None + report_content: str | None = None + summary_report_path: str | None = None + json_path: str | None = None + success: bool = True + error: str | None = None + tool_call_count: int = 0 + llm_call_count: int = 0 + + +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/eval/evaluators/accuracy.py b/src/vuln_analysis/eval/evaluators/accuracy.py deleted file mode 100644 index b54d902d1..000000000 --- a/src/vuln_analysis/eval/evaluators/accuracy.py +++ /dev/null @@ -1,240 +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 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 pydantic import Field -from tqdm import tqdm - -_STATUS_MAP = {"TRUE": "AFFECTED", "FALSE": "NOT AFFECTED", "UNKNOWN": "UNKNOWN"} - - -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.") - - -@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") - - -class AccuracyEvaluator: - '''Configurable accuracy evaluator for either the status or label field.''' - - def __init__(self, - max_concurrency: int, - field: str = "status", - duplicates_policy: Literal["drop_all", "keep_first", "keep_all"] = "keep_first"): - 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) - - try: - 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()]) - finally: - pbar.close() - 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, - } - - eval_output_items = [ - EvalOutputItem(id=rep_id, score=score, reasoning=reasoning) - for rep_id, score, reasoning in zip(rep_ids, all_averages, all_reasonings) - ] - - return EvalOutput(average_score=final_output, eval_output_items=eval_output_items) 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/parse_eval_input.py b/src/vuln_analysis/eval/parse_eval_input.py deleted file mode 100644 index baf6478b8..000000000 --- a/src/vuln_analysis/eval/parse_eval_input.py +++ /dev/null @@ -1,190 +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 -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 - - -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) - - -# For debugging - saves the EvalInputItem string to a file -def save_as_baseline_string_format(eval_input: EvalInput, output_path: Path) -> None: - baseline_data = [] - - 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}") - - -def main(): - import argparse - - 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() - - try: - input_path = Path(args.input_file) - - # Convert to EvalInput format - eval_input = parse_input(input_path) - - # 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) - - 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 - - return 0 - - -if __name__ == "__main__": - 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..a06430801 --- /dev/null +++ b/src/vuln_analysis/functions/container_analyzer.py @@ -0,0 +1,143 @@ +"""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.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) + 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) + + async def _arun(inp: ContainerAnalyzerInput) -> ContainerAnalyzerResult: + logger.info("Starting container analysis for %s", inp.image_name) + + if not Path(inp.filesystem_path).is_dir(): + return ContainerAnalyzerResult( + image_name=inp.image_name, 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, 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(inp.image_name.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, inp.image_name, + max_iterations=config.phase1_max_iterations, + ) + 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", + ) + except Exception as exc: + logger.error("Phase 2 failed: %s", exc, exc_info=True) + return ContainerAnalyzerResult( + image_name=inp.image_name, success=False, error=str(exc), + phase1_skipped=phase1_skipped, + ) + + if not content: + return ContainerAnalyzerResult( + image_name=inp.image_name, 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)", + inp.image_name, len(content), tool_calls, llm_calls) + return ContainerAnalyzerResult( + image_name=inp.image_name, 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/cve_agent.py b/src/vuln_analysis/functions/cve_agent.py deleted file mode 100644 index a6e15a578..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_classic.agents import AgentExecutor -from langchain_classic.agents import create_react_agent -from langchain_classic.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 362744e10..000000000 --- a/src/vuln_analysis/functions/cve_generate_vdbs.py +++ /dev/null @@ -1,279 +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: - # Pass the ref to get_repo_from_path to match the new path structure - repo = get_repo_from_path(config.base_git_dir, si.git_repo, si.ref) - si.ref = repo.commit().hexsha - except ValueError as e: - logger.warning("Failed to get commit hash for %s/%s (ref: %s): %s", config.base_git_dir, si.git_repo, si.ref, 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..25fa37cbc --- /dev/null +++ b/src/vuln_analysis/functions/cve_researcher.py @@ -0,0 +1,84 @@ +"""CVE Researcher agent function for NAT.""" + +import logging + +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.cve_researcher import CVEResearcherInput, CVEResearcherResult +from vuln_analysis.prompts.cve_researcher import SYSTEM_PROMPT, build_user_message +from vuln_analysis.utils.agentic_loop import run_agentic_loop + +logger = logging.getLogger(__name__) + + +class CVEResearcherConfig(FunctionBaseConfig, name="cve_researcher"): + llm_name: str = Field(description="LLM for CVE research") + tool_names: list[FunctionRef] = Field(description="Tools: web_search, web_fetch, osv_lookup") + max_iterations: int = Field(default=30) + context_window_tokens: int = Field(default=262144) + wrap_up_fraction: float = Field(default=0.8) + loop_detection_threshold: int = Field(default=2) + output_subdir: str = Field(default="vulnerability_reports") + description: str = Field(default="Researches a CVE and produces a vulnerability report") + + +@register_function(config_type=CVEResearcherConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def cve_researcher(config: CVEResearcherConfig, 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) + + async def _arun(inp: CVEResearcherInput) -> CVEResearcherResult: + identifier = inp.identifier + logger.info("Starting CVE research for %s", identifier) + + messages = [ + SystemMessage(content=SYSTEM_PROMPT), + HumanMessage(content=build_user_message(inp)), + ] + + try: + content, tool_calls, llm_calls = await run_agentic_loop( + llm=llm, + messages=messages, + tools=tools, + max_iterations=config.max_iterations, + context_window_tokens=config.context_window_tokens, + wrap_up_fraction=config.wrap_up_fraction, + loop_threshold=config.loop_detection_threshold, + phase_label="CVE Researcher", + ) + except Exception as exc: + logger.error("CVE Researcher failed: %s", exc, exc_info=True) + return CVEResearcherResult(identifier=identifier, success=False, error=str(exc)) + + if not content: + return CVEResearcherResult( + identifier=identifier, success=False, + error="No report content produced by the LLM.", + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + logger.info("Research complete: %s (%d chars, %d tool calls, %d LLM calls)", + identifier, len(content), tool_calls, llm_calls) + return CVEResearcherResult( + identifier=identifier, report_content=content, success=True, + tool_call_count=tool_calls, llm_call_count=llm_calls, + ) + + def _str_to_input(s: str) -> CVEResearcherInput: + return CVEResearcherInput.model_validate_json(s) + + def _result_to_str(r: CVEResearcherResult) -> str: + return r.model_dump_json() + + yield FunctionInfo.from_fn( + _arun, input_schema=CVEResearcherInput, description=config.description, + converters=[_str_to_input, _result_to_str], + ) 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..caa3062d9 --- /dev/null +++ b/src/vuln_analysis/functions/exploitability_analyzer.py @@ -0,0 +1,160 @@ +"""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.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) + 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) + + async def _arun(inp: ExploitabilityAnalyzerInput) -> ExploitabilityAnalyzerResult: + 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}", + ) + + logger.info("Pre-triage complete (early_exit=%s)", pre_triage_result.early_exit) + + 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", + ) + 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..38361ae35 --- /dev/null +++ b/src/vuln_analysis/functions/vex_categorizer.py @@ -0,0 +1,144 @@ +"""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.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) + 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] + + 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 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", + ) + 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/prompts/__init__.py b/src/vuln_analysis/prompts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/vuln_analysis/prompts/container_analyzer.py b/src/vuln_analysis/prompts/container_analyzer.py new file mode 100644 index 000000000..412242ccd --- /dev/null +++ b/src/vuln_analysis/prompts/container_analyzer.py @@ -0,0 +1,964 @@ +"""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, 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** | [full registry URL] | +| **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 only. + +## Summary + +| Field | Value | +|-------|-------| +| **Image Name** | [full registry URL] | +| **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}**." + ) + + # 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/cve_researcher.py b/src/vuln_analysis/prompts/cve_researcher.py new file mode 100644 index 000000000..a4693983c --- /dev/null +++ b/src/vuln_analysis/prompts/cve_researcher.py @@ -0,0 +1,348 @@ +"""System prompt and user message builder for the CVE Researcher.""" + +from __future__ import annotations + +from vuln_analysis.data_models.cve_researcher import CVEResearcherInput + +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 + +#### 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. + +## 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` | Catalog doesn't render; 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_user_message(inp: CVEResearcherInput) -> str: + """Construct the initial user message for the LLM.""" + parts: list[str] = [] + + if inp.cve_id and inp.ghsa_id: + parts.append( + f"Research vulnerability **{inp.cve_id}** (also known as **{inp.ghsa_id}**)." + ) + elif inp.cve_id: + parts.append(f"Research vulnerability **{inp.cve_id}**.") + else: + parts.append( + f"Research vulnerability **{inp.ghsa_id}**. " + "This is a GitHub Security Advisory ID – start by finding the associated CVE ID." + ) + + if inp.package_hint or inp.version_hint: + hint_parts: list[str] = [] + if inp.package_hint: + hint_parts.append(f"affected package is **{inp.package_hint}**") + if inp.version_hint: + hint_parts.append(f"installed version is **{inp.version_hint}**") + parts.append( + f"Scanner hint: {', '.join(hint_parts)}. " + "Use this information to focus your search." + ) + + parts.append( + "\nOnce you have gathered all the intelligence, produce the full " + "vulnerability 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..bc44db250 --- /dev/null +++ b/src/vuln_analysis/prompts/exploitability_analyzer.py @@ -0,0 +1,706 @@ +"""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 fifteen 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) + +## CRITICAL INSTRUCTIONS + +- **PRE-TRIAGE IS A STARTING POINT, NOT FINAL:** Pre-triage runs deterministic package lookups (pip dist-info, conda, Go SBOM, npm). It 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 the pre-triage says "package not found", treat this as a starting point — **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 at a vulnerable version, trust that finding and proceed directly to reachability. + +- **CONTAINER TYPE DRIVES VERDICT LOGIC:** + * Runtime + function not called → NOT REACHABLE + * SDK + function not called but accessible via normal import/API → CONDITIONALLY EXPLOITABLE (users can write code that calls it) + * SDK + code behind architectural barrier (alternative implementation backend selected by compiled native extension, internal runtime dispatch not exposed through user API, compile-time feature flag) → NOT REACHABLE (reaching the vulnerable path requires modifying internal implementation details of a dependency, not writing normal SDK code) + * Infrastructure + function not called → NOT REACHABLE (no user code), but with elevated risk level due to blast radius + +- **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. + +- **ASSIGN HONEST CONFIDENCE:** Rate verdict confidence as High/Medium/Low based on evidence quality. Reduce confidence when: CVE lacks specific function, code is compiled without source, cross-architecture limitations apply. + +- **CONTEXT EFFICIENCY:** Your context window is limited. Prefer `grep_search` over `read_file` for large structured files (package databases, dependency lists, log files). The pre-triage has already verified package presence, version, and environment — do not re-read package metadata files to re-confirm what the pre-triage summary already reports. + +--- + +## 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**. + +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: +- 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 + +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:** +When answering "can the attacker control the input?", read the CVE's attack vector first. The CVE defines what capabilities the attacker has — answer the question from that position, not from how the system behaves under normal, non-adversarial conditions. + +**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 +- 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 (common for SDK containers) | +| **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 + +``` +Start (package confirmed present + vulnerable + in active env) + │ + ├─ Select reachability strategy based on CVE specificity: + │ ├─ Specific function known ──→ Function-level analysis (2.2) + │ └─ No specific function ────→ Module-level analysis (2.3, reduced confidence) + │ │ + ├─ Reachable? ─────────────── No ──┬── SDK container? ─── Yes ──┬── Accessible via ── Yes ──→ CONDITIONALLY + │ │ │ │ normal import/API? EXPLOITABLE + │ │ │ └── Behind architectural + │ │ │ barrier? ─── Yes ──→ NOT REACHABLE + │ │ ├── Runtime? ──────── Yes ──→ NOT REACHABLE + │ │ └── Infrastructure? ─ Yes ──→ NOT REACHABLE + │ Yes (elevated risk) + │ │ + ├─ Trigger conditions met? ── No ──→ NOT REACHABLE + │ (env vars, config, (or CONDITIONALLY EXPLOITABLE + │ network exposure) if user can change them) + │ │ + │ Yes + │ │ + ├─ Attacker controls ──────── No ──→ NOT REACHABLE + │ input? + │ │ + │ Yes + │ │ + ├─ Blocked by input ──────── Yes ──→ MITIGATED + │ validation? + │ │ + │ No + │ │ + ├─ Blocked by security ───── Yes ──→ MITIGATED + │ controls? (only CVE-specific controls count) + │ │ + │ No + │ │ + ├─ Assign confidence ─────── High / Medium / Low + │ (based on evidence quality) + │ │ + ├─ Assess post-exploitation impact (does NOT change verdict) + │ │ + └─────────────────────────────────→ EXPLOITABLE +``` + +--- + +## BEHAVIORAL RULES + +1. **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). + +2. **Handle Compiled/Bytecode Code:** Use `check_binary_dependencies` to extract dependency/module info and `search_binary_content` to search for symbols in compiled binaries instead of grep. These tools auto-detect the binary format. Note cross-architecture limitations. + +2a. **C System Libraries Require Package Database Queries:** Python dist-info and glob for .so files will NOT find packages like libtiff5, libssl3, or libxml2. For C/C++ native library CVEs on Debian/Ubuntu containers, call `query_dpkg_database` first to find installed C libraries. Use `find_shared_libraries` to confirm .so presence. Use `search_dpkg_file_lists` to find which package owns a specific .so file. + +2b. **Go Package Verification Requires Binary Analysis:** SBOM scanners may misreport Go dependencies (e.g., confusing nats-io/jwt with golang-jwt). Use `find_go_binaries` to enumerate ALL Go binaries in the container, then verify the actual module list with `go version -m`. Only conclude a Go package is absent after checking all binaries. + +3. **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. + +3c. **Guards Must Be Verified by Call Site, Not by Existence:** When you identify a guard, flag check, validation function, or security middleware that appears to block the vulnerable code path, you MUST verify it is actually invoked on the hot path — not just that it exists. Use `grep_search` to find all call sites of the guard function/method across the codebase. Confirm at least one call site lies on the path between the entry point and the vulnerable function. A guard that is defined but never called from the actual request-handling path provides zero protection. Common failure modes: + - A validation method exists on a class but is never called by the request handler that processes user input + - Security middleware is defined but not registered in the application's middleware stack + - A subclass overrides a parent method and calls `super()` in a way that bypasses the parent's guard + - A flag check exists in one code path (e.g., a CLI argument parser) but the hot path goes through a different route that skips it + + **Do not conclude NOT REACHABLE or REQUIRES CONFIGURATION based on a guard unless you can show a call site of that guard on the actual entry point → vulnerable function chain.** + +3d. **Derive the Attacker's Capabilities From the CVE, Not Your Own Assumptions:** Before reasoning about whether an attacker can trigger the vulnerable code, read the CVE's attack vector and description to understand what the attacker controls. Then ask: given those capabilities, can they reach the vulnerable code? Do not substitute your own threat model. The CVE defines the attacker's position — your job is to determine whether that position is sufficient to trigger the code in this container. + +3f. **`code_not_reachable` Means the Code Does Not Run — Not That Exploitation Is Difficult:** If the vulnerable code executes during container operation, the verdict cannot be `code_not_reachable`, regardless of how difficult the exploitation conditions are. Exploitation difficulty (e.g., requires specific attacker positioning, hard-to-achieve preconditions) affects confidence level and risk rating — not reachability. Use `code_not_reachable` only when the vulnerable code path does not execute at all. + + **The trustworthiness of external systems is not a reachability argument.** Whether a remote server, upstream service, or external dependency is "trusted" or "controlled by NVIDIA/the operator" describes the intended deployment — not the attack scenario. If the CVE is triggered by malicious data from an external system, the reachability question is whether the vulnerable code executes when that system behaves maliciously, not whether it is expected to behave correctly. + +3e. **Proving Absence Requires Searching All Callers, Not Just the Ones You Checked:** When you search for usage of a vulnerable function/API and don't find it in one part of the codebase, you have not proven absence — you have proven absence in that search scope. The vulnerable package may be used by other installed packages, utility scripts, or third-party libraries that also run in the container. Before concluding NOT REACHABLE based on "this API is not called," use `grep_search` broadly across the full filesystem to find ALL call sites, not just the application directories you already inspected. + +4. **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. + +5. **Filesystem Is Ground Truth:** Always verify claims against the actual filesystem contents. + +6. **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. + +7. **Environment Isolation Can Be Bypassed in SDK Containers:** For SDK containers, ENVIRONMENT ISOLATED is still valid (default environment is unaffected), but document that users can deliberately switch environments. + +8. **Confidence Communicates Uncertainty:** Medium and Low confidence verdicts signal that further investigation could change the conclusion. + +9. **Model-Serving Platforms Have Deployment-Time Code Paths:** Generic model-serving containers (e.g., NVIDIA Triton Inference Server, Ray Serve, BentoML, KServe) are blank inference platforms — they ship *code that can handle many model types* but contain NO model by default. A code path is only "in the default execution path" if it executes when the container starts with no model deployed and no user-provided model configuration. + + Ask explicitly: **"Would this code path be exercised in the container's out-of-the-box state — with no model loaded, no user-supplied model config, and no custom runtime args?"** If the answer is No, this code path is NOT in the default execution path and the verdict is **NOT REACHABLE**. + + Common traps: + - Multimodal processing code in a Triton container → only activates when a multimodal model is deployed; NOT in the default path if no model is baked into the image + - Streaming/chunked response handlers → only activate with models that use them + - Backend-specific code (vLLM backend, TensorRT backend) → only instantiated when a model using that backend is deployed + + To determine if a model is baked into the image: check for model weight files (`.pt`, `.onnx`, `.engine`, `.bin`, `.safetensors`) or model config files in the filesystem. If none are present, no model is deployed by default. + +--- + +## 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 | + +### Node.js + +| Pattern | What to Search | Exploitation Requires | +|---------|---------------|----------------------| +| Prototype Pollution | `grep_search("merge\|extend\|assign\|defaultsDeep", ...)` include="*.js" | User-controlled object reaching merge | +| Path Traversal | `grep_search("path.join\|path.resolve\|readFile\|createReadStream", ...)` include="*.js" | User-controlled path component | +| Command Injection | `grep_search("child_process\|exec(\|execSync\|spawn", ...)` include="*.js" | User input in commands | + +### 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 | + +### Java + +| Pattern | What to Search | Exploitation Requires | +|---------|---------------|----------------------| +| Library presence | `glob_find("**/*.jar", ...)` | JAR in classpath | +| Deserialization | `search_binary_content` in JARs for "ObjectInputStream" | Untrusted data reaching deserializer | +| JNDI Injection | `search_binary_content` for "lookup" or "JndiLookup" | User-controlled JNDI name | + +### Native Libraries (C/C++) + +| Pattern | What to Search | Exploitation Requires | +|---------|---------------|----------------------| +| Library presence | `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 | + +--- + +## QUICK REFERENCE: Dual-Mode Packages + +Some packages function as both client libraries and servers. For server-side CVEs, check if the server mode exists and is accessible. + +| Package | Client Usage | Server Mode | Check in Filesystem | +|---------|-------------|-------------|---------------------| +| `mlflow` | `mlflow.log_*()` | `mlflow server` | `glob_find("**/bin/mlflow", ...)` | +| `jupyter` | Client connections | `jupyter notebook/lab` | `glob_find("**/bin/jupyter*", ...)` | +| `tensorboard` | N/A | `tensorboard --logdir=...` | `glob_find("**/bin/tensorboard", ...)` | +| `flask` | N/A | `flask run` | Check site-packages/flask/ | +| `fastapi`/`uvicorn` | N/A | `uvicorn app:app` | `glob_find("**/bin/uvicorn", ...)` | +| `ray` | `ray.get()` | `ray start --head` | `glob_find("**/bin/ray", ...)` | + +--- + +## 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?] | + +### 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] | Yes / No | [context] | + +#### 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] | +| Vulnerable code reachable | YES / NO | [explanation] | +| Unreachability type (if not reachable) | Architectural barrier / Not called / N/A | [explanation — e.g., alternative backend, internal dispatch, compile-time option] | +| 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}**." + ) + + parts.append( + "## Pre-Triage Verification\n\n" + "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.\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 pre_triage.early_exit: + triage_status = f"TRIAGE: {pre_triage.early_exit_verdict} (pre-triage could not confirm package — 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 pre_triage.early_exit and pre_triage.early_exit_reason: + lines.append(f"- Pre-Triage Note: {pre_triage.early_exit_reason}") + 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}") + + 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:", + f"1. {vr.package_raw} must be installed at a vulnerable version (confirmed: {primary.version if primary else 'N/A'})", + 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/vex_categorizer.py b/src/vuln_analysis/prompts/vex_categorizer.py new file mode 100644 index 000000000..73c88d73b --- /dev/null +++ b/src/vuln_analysis/prompts/vex_categorizer.py @@ -0,0 +1,444 @@ +"""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:** The exploitability report verifies package presence via the container filesystem (the authoritative source). There is no separate SBOM check. + +### 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`) + +### 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 → **`requires_configuration`** (status: `not_affected`) +- Required component missing → **`requires_dependency`** (status: `not_affected`) +- Environmental conditions unavailable → **`requires_environment`** (status: `not_affected`) + +### 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 ✅/❌ 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 (any container type) | `affected` | `vulnerable` | **Always `vulnerable`.** CONDITIONALLY EXPLOITABLE means the conditions CAN be met by an attacker. This applies to SDK, Runtime, and Infrastructure containers equally. See CRITICAL NOTE 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 | + +**CRITICAL — CONDITIONALLY EXPLOITABLE always maps to `vulnerable`:** + +If Stage 2's verdict is **CONDITIONALLY EXPLOITABLE** (regardless of container type), the VEX +justification MUST be **`vulnerable`**. Do NOT use `requires_configuration`. + +- **`requires_configuration`** means the PRODUCT ITSELF must be put in a non-default configuration + to expose the vulnerability (e.g., a disabled feature flag must be turned on, a debug mode must + be enabled, a listening port must be opened that is closed by default). +- **`requires_configuration` is WRONG** when: the product is already in its default deployed state + AND the only barrier is that the ATTACKER needs special positioning (MITM, supply chain access, + social engineering). Attacker capabilities and attack complexity are CVSS metrics — not VEX + justification criteria. +- When the product's outbound network requests are active by default (e.g., model downloads at + startup), the container IS in the exposure state by default. The fact that those requests go to + trusted endpoints by default 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 APPLICABLE has two sub-cases** — distinguish them: +- "Version is Vulnerable: No" or "Distro Backport Check: Patched" → `false_positive` +- "Package in Filesystem: No" → `code_not_present` + +**MITIGATED** requires reading Steps 3-4 to determine the specific mitigation type. + +**NOT REACHABLE** can occasionally map to `requires_configuration` or `requires_dependency` +instead of `code_not_reachable` — check the Verdict Justification text. + +--- + +## 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", "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. **STEP NUMBERING IS DIFFERENT.** The report's "Step 1/2/3/4" does NOT equal the VEX + flowchart's "Step 1/2/3/4/5/6". Use the Step Mapping table above to read the correct + report sections for each VEX decision step. + +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 type is SDK/Development and the + exploitability verdict is CONDITIONALLY EXPLOITABLE or NOT REACHABLE, the VEX + justification MUST be `vulnerable` (status: `affected`) UNLESS one of the still-valid + exceptions applies (false_positive, code_not_present, requires_dependency, + requires_environment, *_protected). + +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] |\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..13ad5f014 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -1,24 +1,17 @@ -# 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 — registers the CVE pipeline workflow and all components. +Replaces the old vuln_analysis workflow with the cve-agent-v3 pipeline. +""" + +import asyncio +import json import logging -from datetime import datetime +import time +import traceback +from collections.abc import Awaitable, Callable from io import TextIOWrapper -from typing import Literal +from pathlib import Path -# 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 +24,359 @@ 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 +from vuln_analysis.data_models.input import PipelineInput +from vuln_analysis.data_models.output import PipelineResult, StageResult +from vuln_analysis.data_models.state import PipelineState +from vuln_analysis.data_models.cve_researcher import CVEResearcherInput, CVEResearcherResult +from vuln_analysis.data_models.container_analyzer import ( + ContainerAnalyzerInput, + ContainerAnalyzerResult, + derive_report_filename, +) +from vuln_analysis.data_models.exploitability_analyzer import ( + ExploitabilityAnalyzerInput, + ExploitabilityAnalyzerResult, + extract_container_name_from_path, + extract_cve_id_from_path, +) +from vuln_analysis.data_models.vex_categorizer import ( + VEXCategorizerInput, + VEXCategorizerResult, + parse_report_filename, +) +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +# Import to trigger @register_function decorators +from vuln_analysis.functions import cve_researcher # noqa: F401 +from vuln_analysis.functions import container_analyzer # noqa: F401 +from vuln_analysis.functions import exploitability_analyzer # noqa: F401 +from vuln_analysis.functions import vex_categorizer # noqa: F401 +from vuln_analysis.tools import web_tools # noqa: F401 +from vuln_analysis.tools import osv_tools # noqa: F401 +from vuln_analysis.tools import filesystem_tools # noqa: F401 +from vuln_analysis.tools import binary_tools # noqa: F401 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)") - 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) +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) + 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): + + from langgraph.graph import END, START, StateGraph + + cve_researcher_fn = await builder.get_function(name=config.cve_researcher_name) + container_analyzer_fn = await builder.get_function(name=config.container_analyzer_name) + exploitability_fn = await builder.get_function(name=config.exploitability_analyzer_name) + vex_fn = await builder.get_function(name=config.vex_categorizer_name) + + async def _run_with_retry( + agent_name: str, + coro_factory: Callable[[], Awaitable[StageResult]], + ) -> StageResult: + max_attempts = 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=config.stage_timeout) + except asyncio.TimeoutError: + result = StageResult(agent_name=agent_name, success=False, + error=f"Stage timed out after {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 result + + def _result_or_failure(result, agent_name: str) -> StageResult: + if isinstance(result, BaseException): + return StageResult(agent_name=agent_name, success=False, + error=f"Exception: {result}") + return result + + async def _run_cve_researcher(state: PipelineState) -> StageResult: + inp_data = state["input"] + agent_inp = CVEResearcherInput( + cve_id=inp_data.get("cve_id"), + ghsa_id=inp_data.get("ghsa_id"), + package_hint=inp_data.get("package_hint"), + version_hint=inp_data.get("version_hint"), + ) + res = await cve_researcher_fn.ainvoke(agent_inp) + if not isinstance(res, CVEResearcherResult): + res = CVEResearcherResult(**res) if isinstance(res, dict) else CVEResearcherResult(identifier=agent_inp.identifier, success=False, error="Unexpected result type") + + report_path = res.report_path + if res.success and res.report_content and not report_path: + root = Path(state["input"].get("output_dir", config.output_dir)).resolve() + vuln_dir = root / "vulnerability_reports" + vuln_dir.mkdir(parents=True, exist_ok=True) + report_path = str(vuln_dir / f"{res.identifier}.md") + Path(report_path).write_text(res.report_content, encoding="utf-8") + + 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, + ) + + async def _run_container_analyzer(state: PipelineState) -> StageResult: + inp_data = state["input"] + agent_inp = ContainerAnalyzerInput( + image_name=inp_data["image_name"], + 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 = await container_analyzer_fn.ainvoke(agent_inp) + if not isinstance(res, ContainerAnalyzerResult): + res = ContainerAnalyzerResult(**res) if isinstance(res, dict) else ContainerAnalyzerResult(image_name=agent_inp.image_name, success=False, error="Unexpected result type") + + report_path = res.report_path + if res.success and res.report_content and not report_path: + root = Path(state["input"].get("output_dir", config.output_dir)).resolve() + container_dir = root / "container_reports" + container_dir.mkdir(parents=True, exist_ok=True) + filename = derive_report_filename(inp_data["image_name"]) + report_path = str(container_dir / filename) + Path(report_path).write_text(res.report_content, encoding="utf-8") + + 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}, + ) + + async def stage1_parallel(state: PipelineState) -> dict: + results = await asyncio.gather( + _run_with_retry("CVE Researcher", lambda: _run_cve_researcher(state)), + _run_with_retry("Container Analyzer", lambda: _run_container_analyzer(state)), + return_exceptions=True, + ) + cve_result = _result_or_failure(results[0], "cve_researcher") + container_result = _result_or_failure(results[1], "container_analyzer") + return {"cve_report": cve_result, "container_report": container_result} + + def check_stage1(state: PipelineState) -> str: + cve = state.get("cve_report") + container = state.get("container_report") + if cve and container and cve.success and container.success: + return "continue" + return "fail" + + async def exploitability_node(state: PipelineState) -> dict: + cve_report = state["cve_report"] + container_report = state["container_report"] + inp_data = state["input"] + + agent_inp = ExploitabilityAnalyzerInput( + vulnerability_report_path=cve_report.report_path, + container_report_path=container_report.report_path, + filesystem_path=inp_data["filesystem_path"], + ) + res = await exploitability_fn.ainvoke(agent_inp) + if not isinstance(res, ExploitabilityAnalyzerResult): + res = ExploitabilityAnalyzerResult(**res) if isinstance(res, dict) else ExploitabilityAnalyzerResult(cve_id="unknown", container_name="unknown", success=False, error="Unexpected result type") + + report_path = res.report_path + if res.success and res.report_content and not report_path: + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + exploit_dir = root / "exploitability_reports" + exploit_dir.mkdir(parents=True, exist_ok=True) + container_name = extract_container_name_from_path(container_report.report_path) + from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as exploit_derive + filename = exploit_derive(res.cve_id, container_name) + report_path = str(exploit_dir / filename) + Path(report_path).write_text(res.report_content, encoding="utf-8") + + stage_result = 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}, + ) + + result = await _run_with_retry("Exploitability Analyzer", lambda: _make_awaitable(stage_result)) + return {"exploit_report": result} + + async def _make_awaitable(val): + return val + + def check_stage2(state: PipelineState) -> str: + exploit = state.get("exploit_report") + if exploit and exploit.success: + return "continue" + return "fail" + + async def vex_node(state: PipelineState) -> dict: + exploit_report = state["exploit_report"] + container_report = state["container_report"] + inp_data = state["input"] + + agent_inp = VEXCategorizerInput( + exploitability_report_path=exploit_report.report_path, + container_report_path=container_report.report_path, + ) + res = await vex_fn.ainvoke(agent_inp) + if not isinstance(res, VEXCategorizerResult): + res = VEXCategorizerResult(**res) if isinstance(res, dict) else VEXCategorizerResult(cve_id="unknown", container_name="unknown", success=False, error="Unexpected result type") + + summary_path = res.summary_report_path + json_path = res.json_path + + if res.success and res.report_content and not summary_path: + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + vex_dir = root / "vex_results" + vex_dir.mkdir(parents=True, exist_ok=True) + stem = f"{res.cve_id}_{res.container_name}" + summary_path = str(vex_dir / f"{stem}.md") + json_path = str(vex_dir / f"{stem}.json") + Path(summary_path).write_text(res.report_content, encoding="utf-8") + _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, + ) + + stage_result = 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 { + "vex_result": stage_result, + "vex_status": res.status, + "vex_justification": res.justification, + } + + async def fail_safe_node(state: PipelineState) -> dict: + logger.warning("Fail-safe: defaulting to unknown/uncertain") + return { + "vex_status": "unknown", + "vex_justification": "uncertain", + } + + async def output_results_node(state: PipelineState) -> dict: + success = bool( + state.get("vex_result") + and state["vex_result"].success + ) + return {"success": success} + + graph_builder = StateGraph(PipelineState) + graph_builder.add_node("stage1_parallel", stage1_parallel) + graph_builder.add_node("exploitability", exploitability_node) + graph_builder.add_node("vex_categorizer", vex_node) + graph_builder.add_node("fail_safe", fail_safe_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(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) graph = graph_builder.compile() - def convert_str_to_agent_morpheus_input(input_str: str) -> AgentMorpheusInput: - logger.debug("Converting input to AgentMorpheusInput: %s", input_str) + def str_to_pipeline_input(input_str: str) -> PipelineInput: try: - return AgentMorpheusInput.model_validate_json(input_str) + return PipelineInput.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 + logger.error("Failed to convert input: %s", e) + raise - def convert_textio_to_agent_morpheus_input(input_file: TextIOWrapper) -> AgentMorpheusInput: - logger.debug("Converting input to AgentMorpheusInput: %s", input_file) + def textio_to_pipeline_input(input_file: TextIOWrapper) -> PipelineInput: try: data = input_file.read() - return AgentMorpheusInput.model_validate_json(data) + return PipelineInput.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.") + logger.error("Failed to convert input: %s", e) + raise + + def pipeline_result_to_str(output: PipelineResult) -> str: + return output.model_dump_json() + + async def _response_fn(inp: PipelineInput) -> PipelineResult: + t0 = time.monotonic() + initial_state: PipelineState = { + "input": inp.model_dump(), + "cve_report": None, + "container_report": None, + "exploit_report": None, + "vex_result": None, + "vex_status": None, + "vex_justification": None, + "success": False, + } + result_state = await graph.ainvoke(initial_state) + elapsed = time.monotonic() - t0 + + return PipelineResult( + cve_id=inp.identifier, + image_name=inp.image_name, + success=result_state.get("success", False), + cve_researcher=result_state.get("cve_report"), + container_analyzer=result_state.get("container_report"), + exploitability_analyzer=result_state.get("exploit_report"), + vex_categorizer=result_state.get("vex_result"), + vex_status=result_state.get("vex_status"), + vex_justification=result_state.get("vex_justification"), + elapsed_seconds=elapsed, + ) + + yield FunctionInfo.from_fn( + _response_fn, + input_schema=PipelineInput, + description=config.description, + converters=[str_to_pipeline_input, textio_to_pipeline_input, pipeline_result_to_str], + ) + + +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: + data: dict = { + "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 + + p = Path(json_path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(data, indent=2), encoding="utf-8") + logger.info("Wrote VEX JSON to %s", json_path) 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..ef59938aa --- /dev/null +++ b/src/vuln_analysis/tools/binary_tools.py @@ -0,0 +1,236 @@ +"""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(["strings", str(path)], capture_output=True, text=True, timeout=timeout) + if "go1." in result.stdout[:50000]: + is_go = True + 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") diff --git a/src/vuln_analysis/tools/filesystem_tools.py b/src/vuln_analysis/tools/filesystem_tools.py new file mode 100644 index 000000000..7d5a4edc3 --- /dev/null +++ b/src/vuln_analysis/tools/filesystem_tools.py @@ -0,0 +1,348 @@ +"""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_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_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 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") + 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): + matches.append(f"{fpath}:{lineno}:{line}") + 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_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_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 fc9982972..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_classic.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/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..7963b91e4 --- /dev/null +++ b/src/vuln_analysis/tools/web_tools.py @@ -0,0 +1,129 @@ +"""Web fetch tool for NAT registration.""" + +import logging +import re +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__) + +_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) > 1000: + 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 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: + from tavily import TavilyClient + import os + api_key = os.getenv("TAVILY_API_KEY") + if api_key: + client = TavilyClient(api_key=api_key) + 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 = _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..0ce790564 --- /dev/null +++ b/src/vuln_analysis/utils/agentic_loop.py @@ -0,0 +1,264 @@ +"""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 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], +) -> 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: + 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 = "", +) -> 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) + 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/__init__.py b/src/vuln_analysis/utils/clients/__init__.py deleted file mode 100644 index a1744724e..000000000 --- a/src/vuln_analysis/utils/clients/__init__.py +++ /dev/null @@ -1,14 +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. 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/document_embedding.py b/src/vuln_analysis/utils/document_embedding.py deleted file mode 100644 index 301f10f58..000000000 --- a/src/vuln_analysis/utils/document_embedding.py +++ /dev/null @@ -1,522 +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 re -import sys -import time -import typing -from hashlib import sha512 -from pathlib import Path -from pathlib import PurePath - -from langchain_classic.docstore.document import Document -from langchain_classic.text_splitter import Language -from langchain_classic.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.concurrency import file_lock -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. - - Each combination of git_repo and ref gets its own directory to prevent - conflicts when multiple refs of the same repository are being processed - concurrently. - - Parameters - ---------- - source_info : SourceDocumentsInfo - The source document info to get the git repository path. - - Returns - ------- - Path - Returns the path to the git repository. - """ - # Include the ref in the path to avoid conflicts when multiple refs - # of the same repo are processed concurrently - base_path = self._git_directory / PurePath(source_info.git_repo) - - # Sanitize ref name for use in filesystem path (remove special chars) - safe_ref = re.sub(r'[^\w\-.]', '_', source_info.ref) - - return base_path / safe_ref - - 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)) - - # Use file lock to prevent race conditions when multiple processes check/create/load the same VDB - with file_lock(vdb_output_dir, timeout=1800): - # Re-check existence after acquiring lock in case another process created it - 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 f5c3d2d0b..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_classic.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/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/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..645477110 --- /dev/null +++ b/src/vuln_analysis/utils/pre_triage.py @@ -0,0 +1,319 @@ +"""Deterministic pre-triage for the Exploitability Analyzer. + +Performs Steps 0-1 of the exploitability analysis deterministically: + 1. Parse vulnerability and container reports + 2. Discover the container filesystem layout + 3. Verify the vulnerable package on the filesystem + 4. Compare installed version against the vulnerable range + 5. Check distro backports + 6. Decide: early exit (NOT APPLICABLE / ENVIRONMENT ISOLATED) or CONTINUE +""" + +from __future__ import annotations + +import json +import logging +import os +import re +from pathlib import Path + +from packaging.version import InvalidVersion, Version + +from vuln_analysis.data_models.exploitability_analyzer import ( + AffectedRange, + ExploitabilityAnalyzerInput, + FilesystemLayout, + PackageInstallation, + PackageVerification, + ParsedContainerReport, + ParsedVulnReport, + PreTriageResult, +) +from vuln_analysis.utils.report_parser import extract_markdown_table_field + +logger = logging.getLogger(__name__) + +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_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", +}) + + +def _raw_listdir(directory: str) -> list[str]: + try: + return sorted(os.listdir(directory)) + except (OSError, PermissionError): + return [] + + +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 deterministic pre-triage verification. + + Returns a PreTriageResult with either an early-exit verdict or + enriched context for Phase 2 deep analysis. + """ + vuln_report = _parse_vulnerability_report(inp.vulnerability_report_path) + container_report = _parse_container_report(inp.container_report_path) + fs_layout = _discover_filesystem(inp.filesystem_path) + pkg_verification = _verify_package( + vuln_report, container_report, inp.filesystem_path, fs_layout, + ) + + early_exit = False + early_exit_verdict = None + early_exit_reason = None + + if not pkg_verification.found: + early_exit = True + early_exit_verdict = "NOT APPLICABLE" + early_exit_reason = ( + f"Package '{vuln_report.package_raw}' not found on the container filesystem. " + f"Searched names: {', '.join(pkg_verification.searched_names) if pkg_verification.searched_names else vuln_report.package_name}." + ) + elif pkg_verification.is_vulnerable is False: + early_exit = True + early_exit_verdict = "NOT APPLICABLE" + if pkg_verification.backport_status == "patched": + early_exit_reason = ( + f"Package '{vuln_report.package_raw}' is present but the distro has " + f"backported a fix. {pkg_verification.backport_evidence or ''}" + ) + else: + inst = pkg_verification.active_env_installation or ( + pkg_verification.installations[0] if pkg_verification.installations else None + ) + early_exit_reason = ( + f"Package '{vuln_report.package_raw}' is present at version " + f"{inst.version if inst else 'unknown'} which is NOT in the vulnerable " + f"range ({vuln_report.vulnerable_versions}). Fixed in: {vuln_report.fixed_in}." + ) + elif ( + pkg_verification.found + and pkg_verification.active_env_installation is None + and pkg_verification.installations + ): + early_exit = True + early_exit_verdict = "ENVIRONMENT ISOLATED" + envs = [inst.environment or "unknown" for inst in pkg_verification.installations] + early_exit_reason = ( + f"Package '{vuln_report.package_raw}' found at a vulnerable version but " + f"only in non-default environment(s): {', '.join(envs)}. " + f"The default environment is '{container_report.default_environment}'." + ) + + return PreTriageResult( + vuln_report=vuln_report, + container_report=container_report, + filesystem_layout=fs_layout, + package_verification=pkg_verification, + early_exit=early_exit, + early_exit_verdict=early_exit_verdict, + early_exit_reason=early_exit_reason, + ) + + +def _parse_vulnerability_report(path: str) -> ParsedVulnReport: + """Parse key fields from a vulnerability report.""" + try: + content = Path(path).read_text(encoding="utf-8") + except Exception: + content = "" + + cve_id = extract_markdown_table_field(content, "CVE ID") or Path(path).stem + package = extract_markdown_table_field(content, "Affected Package") or "unknown" + vulnerable_versions = extract_markdown_table_field(content, "Vulnerable Versions") or "unknown" + fixed_in = extract_markdown_table_field(content, "Fixed In") or "unknown" + cvss_raw = extract_markdown_table_field(content, "CVSS v3.1") or "" + cvss_vector = extract_markdown_table_field(content, "CVSS Vector") + cwe = extract_markdown_table_field(content, "CWE") + + cvss_score = None + cvss_severity = None + if cvss_raw: + parts = cvss_raw.split() + try: + cvss_score = float(parts[0]) + except (ValueError, IndexError): + pass + if len(parts) > 1: + cvss_severity = parts[1] + + vulnerable_function = None + func_match = re.search(r"\*\*Function:\*\*\s*`?([^`\n]+)`?", content) + if func_match: + vulnerable_function = func_match.group(1).strip() + + package_name = re.sub(r"[^a-zA-Z0-9_.-]", "", package).lower().replace("-", "_") + + return ParsedVulnReport( + cve_id=cve_id, + package_name=package_name, + package_raw=package, + ecosystem_hint=None, + 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, + ) + + +def _parse_container_report(path: str) -> ParsedContainerReport: + """Parse key fields from a container analysis report.""" + try: + content = Path(path).read_text(encoding="utf-8") + except Exception: + content = "" + + image_name = extract_markdown_table_field(content, "Image Name") or Path(path).stem + container_type = extract_markdown_table_field(content, "Container Type") or "Runtime/Microservice" + primary_language = extract_markdown_table_field(content, "Primary Language") or "unknown" + base_os = extract_markdown_table_field(content, "Base Image") or extract_markdown_table_field(content, "Base OS") or "unknown" + + default_environment = None + env_match = re.search(r"activated.*?environment.*?[`\"'](\w+)[`\"']", content, re.IGNORECASE) + if env_match: + default_environment = env_match.group(1) + if not default_environment: + env_field = extract_markdown_table_field(content, "Default Environment") + if env_field: + default_environment = env_field + + runtime_user = extract_markdown_table_field(content, "USER directive") or "root" + + return ParsedContainerReport( + image_name=image_name, + container_type=container_type, + primary_language=primary_language, + base_os=base_os, + default_environment=default_environment, + runtime_user=runtime_user, + ) + + +def _discover_filesystem(filesystem_path: str) -> FilesystemLayout: + """Discover the container's filesystem layout.""" + fs = Path(filesystem_path) + layout = FilesystemLayout() + + conda_envs_dir = fs / "opt" / "conda" / "envs" + if conda_envs_dir.is_dir(): + layout.conda_envs = [d.name for d in 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"]: + for sp in fs.glob(probe): + if sp.is_dir(): + layout.site_packages_paths.append(str(sp)) + + for app_dir_name in ["workspace", "app", "opt", "srv"]: + candidate = fs / app_dir_name + if candidate.is_dir(): + layout.app_code_dirs.append(str(candidate)) + + for pattern in ["**/entrypoint*.sh", "**/docker-entrypoint*.sh", "**/start*.sh"]: + for ep in fs.glob(pattern): + if ep.is_file(): + layout.entrypoint_scripts.append(str(ep)) + + return layout + + +def _verify_package( + vuln_report: ParsedVulnReport, + container_report: ParsedContainerReport, + filesystem_path: str, + layout: FilesystemLayout, +) -> PackageVerification: + """Verify the vulnerable package on the container filesystem.""" + search_names = [vuln_report.package_name] + aliases = PACKAGE_ALIASES.get(vuln_report.package_name, []) + search_names.extend(aliases) + search_names = list(dict.fromkeys(search_names)) + + installations: list[PackageInstallation] = [] + + for sp_path in layout.site_packages_paths: + sp = Path(sp_path) + for name in search_names: + for dist_info in sp.glob(f"{name}*.dist-info"): + metadata_file = dist_info / "METADATA" + if metadata_file.is_file(): + try: + metadata = metadata_file.read_text(encoding="utf-8", errors="ignore") + version_match = re.search(r"^Version:\s*(.+)$", metadata, re.MULTILINE) + version = version_match.group(1).strip() if version_match else "unknown" + except Exception: + version = "unknown" + + env = None + if "conda/envs/" in sp_path: + env_match = re.search(r"conda/envs/([^/]+)/", sp_path) + if env_match: + env = env_match.group(1) + + installations.append(PackageInstallation( + version=version, + location=str(dist_info), + source="pip", + environment=env, + )) + + if not installations: + return PackageVerification(found=False, searched_names=search_names) + + active_env = container_report.default_environment + active_inst = None + if active_env: + for inst in installations: + if inst.environment == active_env: + active_inst = inst + break + if not active_inst and installations: + for inst in installations: + if inst.environment is None: + active_inst = inst + break + + is_vulnerable = None + check_inst = active_inst or installations[0] + try: + installed_ver = Version(check_inst.version) + if vuln_report.fixed_in and vuln_report.fixed_in != "unknown": + try: + fixed_ver = Version(vuln_report.fixed_in) + is_vulnerable = installed_ver < fixed_ver + except InvalidVersion: + pass + except InvalidVersion: + pass + + return PackageVerification( + found=True, + installations=installations, + active_env_installation=active_inst, + is_vulnerable=is_vulnerable, + searched_names=search_names, + ) 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..ee3ec0405 --- /dev/null +++ b/src/vuln_analysis/utils/report_generator.py @@ -0,0 +1,88 @@ +"""Template-based report generation for early-exit exploitability verdicts. + +When pre-triage determines NOT APPLICABLE or ENVIRONMENT ISOLATED, +this generates the full markdown report with zero LLM calls. +""" + +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.""" + vr = result.vuln_report + cr = result.container_report + pkg = result.package_verification + verdict = result.early_exit_verdict or "NOT APPLICABLE" + reason = result.early_exit_reason or "" + + 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" + else: + fs_version = "N/A" + fs_location = "not found" + pkg_in_fs = "No" + version_vulnerable = "N/A" + + report = f"""# Container Exploitability Analysis + +## Summary + +| Field | Value | +|-------|-------| +| **CVE ID** | {vr.cve_id} | +| **Container** | {cr.image_name} | +| **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 | + +## Analysis + +### Step 1: Vulnerable Component Presence + +| Check | Result | Evidence | +|-------|--------|----------| +| **Package in Filesystem** | {pkg_in_fs} | {fs_location} | +| **Installed Version** | {fs_version} | Filesystem verification | +| **Version is Vulnerable** | {version_vulnerable} | Version comparison | + +### Step 2: Code Reachability + +Skipped — {reason} + +### Step 3: Data Flow and Trigger Conditions + +Skipped — {reason} + +### Step 4: Security Controls and Impact + +Skipped — {reason} + +### Verdict: {verdict} + +**Confidence:** High + +**Justification:** +{reason} + +## References + +- Container Report: {inp.container_report_path} +- Vulnerability Report: {inp.vulnerability_report_path} +- Filesystem: {inp.filesystem_path} +""" + return report.strip() + "\n" 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 9431cf1e9..000000000 --- a/src/vuln_analysis/utils/serp_api_wrapper.py +++ /dev/null @@ -1,103 +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_classic.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 c54fc975c..000000000 --- a/src/vuln_analysis/utils/source_code_git_loader.py +++ /dev/null @@ -1,208 +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 - -from vuln_analysis.utils.concurrency import file_lock - -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. - - Uses file locking to prevent concurrent access issues when multiple processes - try to clone, fetch, or checkout the same repository simultaneously. - - 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") - - # Use file lock to prevent concurrent git operations on the same repository - with file_lock(Path(self.repo_path), timeout=600): - if 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) - - # Check if "origin" remote exists before attempting fetch - has_origin_remote = any(remote.name == "origin" for remote in repo.remotes) - - if has_origin_remote: - # Reliable way to check out the ref using a shallow clone - repo.git.fetch("origin", self.ref, depth=1) - repo.git.checkout("FETCH_HEAD") - else: - # For local-only repos without origin remote, directly checkout the ref - logger.debug("No 'origin' remote found, checking out ref directly: %s", self.ref) - repo.git.checkout(self.ref) - - 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..9d4a5cfd4 --- /dev/null +++ b/src/vuln_analysis/utils/web_researcher.py @@ -0,0 +1,59 @@ +"""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 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, +) -> 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", + ) + 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 From 236700252b0c178a5b951ea97003416d26846d56 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Wed, 11 Mar 2026 15:18:07 -0400 Subject: [PATCH 04/48] Add MIGRATION_PLAN.md Made-with: Cursor --- MIGRATION_PLAN.md | 635 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 MIGRATION_PLAN.md diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md new file mode 100644 index 000000000..71884911e --- /dev/null +++ b/MIGRATION_PLAN.md @@ -0,0 +1,635 @@ +# Migrate cve-agent-v3 to NeMo Agent Toolkit + +## Target Repository + +All changes go into the **vulnerability-analysis** repo on the `cve-agent-v3` branch. The cve-agent-v3 pipeline **replaces** the existing `vuln_analysis` workflow in `src/vuln_analysis/`. Source code to port comes from the cve-agent-v3 repo's `cve_agent/` directory. + +## Architecture Overview + +```mermaid +graph TD + subgraph natWorkflow [NAT Workflow: cve_pipeline] + Input["PipelineInput"] --> Stage1["stage1_parallel node"] + Stage1 -->|"asyncio.gather"| CVERes["cve_researcher fn"] + Stage1 -->|"asyncio.gather"| ContAn["container_analyzer fn"] + Stage1 -->|check success| Cond1{"Stage 1 OK?"} + Cond1 -->|no| FailSafe["fail_safe node"] + Cond1 -->|yes| Stage2["exploitability_analyzer fn"] + Stage2 -->|check success| Cond2{"Stage 2 OK?"} + Cond2 -->|no| FailSafe + Cond2 -->|yes| Stage3["vex_categorizer fn"] + Stage3 --> OutputNode["output_results node"] + FailSafe --> OutputNode + OutputNode --> Result["PipelineResult"] + end + + subgraph natComponents [NAT Components] + LLM["llms: nim LLM config"] + Tools["tools: web, filesystem, binary, osv"] + Builder["Builder wires LLMs + tools into functions"] + end + + Builder --> CVERes + Builder --> ContAn + Builder --> Stage2 + Builder --> Stage3 +``` + + + +## 0. Removals -- old vuln_analysis code that no longer applies + +The following files/directories inside `src/vuln_analysis/` are **deleted** because they implement the old workflow and are replaced by the cve-agent-v3 pipeline: + +**Entire directories to delete:** + +- `functions/` -- all 10 `cve_*.py` files (cve_agent, cve_generate_vdbs, cve_fetch_intel, cve_process_sbom, cve_check_vuln_deps, cve_checklist, cve_summarize, cve_justify, cve_file_output, cve_http_output) +- `tools/` -- local_vdb.py, serp.py, lexical_full_search.py (replaced by new tool set) +- `test_time_compute/` -- execute_then_select_function.py, majority_voting_selector.py (TTC tied to old output schema) +- `eval/` -- parse_eval_input.py, evaluators/accuracy.py, evaluators/consistency.py, visualizations/ (evaluators tied to old AgentMorpheusOutput) +- `configs/` -- all old YAML files (config.yml, config-eval.yml, config-ttc.yml, config-tracing.yml) and openapi/ (replaced by new configs) +- `data/` -- input_messages/, sboms/, profiler_datasets/, eval_datasets/ (old sample data) +- `data_models/` -- all old models (common.py, input.py, output.py, state.py, cve_intel.py, dependencies.py, info.py, vdb_type.py, eval_input.py) -- replaced by new cve-agent-v3 schemas + +**Individual files in `utils/` to delete:** + +- `prompting.py` -- old agent/summary/checklist prompts +- `checklist_prompt_generator.py` -- checklist generation logic +- `justification_parser.py` -- label/status parsing for old workflow +- `llm_engine_utils.py` -- skip reasons, preprocess/postprocess tied to old state +- `output_formatter.py` -- markdown report tied to AgentMorpheusOutput +- `intel_retriever.py` -- NVD/GHSA/RHSA/Ubuntu/EPSS orchestrator (replaced by cve_researcher agent) +- `vulnerable_dependency_checker.py` -- deps.dev checker (replaced by cve_researcher) +- `document_embedding.py` -- FAISS VDB builder (not used in new pipeline) +- `full_text_search.py` -- Tantivy indexer (not used in new pipeline) +- `source_code_git_loader.py` -- git blob loader (not used in new pipeline) +- `js_extended_parser.py` -- JS parser for VDB (not used in new pipeline) +- `serp_api_wrapper.py` -- SerpAPI wrapper (replaced by Tavily) +- `intel_utils.py` -- NVD/CVE version parsing (replaced by cve_researcher) +- `clients/` -- nvd_client.py, ghsa_client.py, rhsa_client.py, ubuntu_client.py, first_client.py, intel_client.py (replaced by cve_researcher agent) + +**Old `register.py`** -- deleted and replaced with new pipeline registration. + +**Kept utils** (generic, reusable across workflows): + +- `string_utils.py` -- CVE/GHSA ID validation, list parsing +- `concurrency.py` -- rate limiting, file locking +- `error_utils.py` -- auth error detection +- `nat_logging_patch.py` -- NAT logging enhancement +- `http_utils.py` -- sync HTTP with retries +- `async_http_utils.py` -- async HTTP with retries +- `url_utils.py` -- URL joining +- `data_utils.py` -- serialization helpers +- `git_utils.py` -- GitPython repo helper + +## 1. Project Restructuring + +Replace the contents of `src/vuln_analysis/` with the new cve-agent-v3 pipeline. The package name and NAT entry point stay the same (`vuln_analysis`). + +**New layout of `src/vuln_analysis/` after migration:** + +``` +src/vuln_analysis/ + __init__.py # (kept, unchanged) + register.py # NAT entry point (replaced) + configs/ + config.yml # full pipeline + config-cve-researcher.yml # individual agent + config-container-analyzer.yml + config-exploitability-analyzer.yml + config-vex-categorizer.yml + data_models/ + __init__.py + input.py # PipelineInput (Pydantic) + output.py # PipelineResult, StageResult (Pydantic) + state.py # PipelineState (TypedDict for LangGraph) + cve_researcher.py # CVEResearcherInput, CVEResearcherResult + container_analyzer.py # ContainerAnalyzerInput, ContainerAnalyzerResult + exploitability_analyzer.py # ExploitabilityAnalyzerInput, ExploitabilityAnalyzerResult + vex_categorizer.py # VEXCategorizerInput, VEXCategorizerResult + functions/ + __init__.py + cve_researcher.py # @register_function + container_analyzer.py + exploitability_analyzer.py + vex_categorizer.py + tools/ + __init__.py + web_tools.py # web_fetch (custom), web_search uses NAT tavily + osv_tools.py # osv_lookup + filesystem_tools.py # read_file, list_directory, grep_search, glob_find + binary_tools.py # check_binary_dependencies, search_binary_content, check_distro_patches + prompts/ + __init__.py + cve_researcher.py # system prompts, user message builders + container_analyzer.py + exploitability_analyzer.py + vex_categorizer.py + utils/ + __init__.py + # --- kept from old vuln_analysis --- + string_utils.py + concurrency.py + error_utils.py + nat_logging_patch.py + http_utils.py + async_http_utils.py + url_utils.py + data_utils.py + git_utils.py + # --- new, ported from cve-agent-v3 --- + agentic_loop.py # Adapted BaseAgent loop logic + pre_triage.py # Deterministic pre-triage (from exploitability_analyzer) + report_parser.py # extract_markdown_table_field + report_generator.py # Early-exit report templates + helpers.py # estimate_message_tokens, truncate, extract_final_response + web_researcher.py # WebResearchSubAgent (Phase 1 of container analyzer) +``` + +**pyproject.toml changes** (modify in place): + +1. **Keep the existing entry point** (no new entry point needed): + +```toml +[project.entry-points.'nat.components'] +vuln_analysis = "vuln_analysis.register" +``` + +2. **Update `nvidia-nat` version** from `~=1.4.0` to `==1.5.0rc5`: + +```toml +"nvidia-nat[async_endpoints,langchain,phoenix,profiling]==1.5.0rc5", +``` + +3. **Remove `langchain-classic`** from dependencies (keep `langchain` and `langchain-core`). + +4. **Update Python version** from `>=3.11,<3.13` to `>=3.13,<3.14`: + +```toml +requires-python = ">=3.13,<3.14" +``` + +5. **Add new dependencies** not already present: + +```toml +"langgraph>=1.0.5,<2.0.0", +"httpx>=0.27", +"tavily-python>=0.5.0", +"packaging>=24.0", +``` + +6. **Remove old dependencies** no longer needed (verify each before removing): + - `tantivy` (Tantivy full-text search -- replaced) + - `faiss-cpu` / `faiss-gpu` (FAISS VDB -- replaced) + - `google-search-results` (SerpAPI -- replaced by Tavily) + - Any other deps exclusive to the old workflow + +Port source code from the cve-agent-v3 repo's `cve_agent/` into `src/vuln_analysis/`. + +--- + +## 2. Data Models + +Port existing dataclass schemas to Pydantic models. The LangGraph state uses `TypedDict`. + +**`data_models/state.py`** -- LangGraph pipeline state: + +```python +from typing import TypedDict +from vuln_analysis.data_models.output import StageResult + +class PipelineState(TypedDict): + input: dict # serialized PipelineInput + cve_report: StageResult | None + container_report: StageResult | None + exploit_report: StageResult | None + vex_result: StageResult | None + vex_status: str | None + vex_justification: str | None + success: bool +``` + +**`data_models/input.py`** -- port `PipelineInput` from `orchestrator/schemas.py` to Pydantic `BaseModel`. Same for per-agent input schemas. + +**`data_models/output.py`** -- port `StageResult` and `PipelineResult` to Pydantic. Add `model_dump_json()` converter for NAT's `FunctionInfo.from_fn` output. + +--- + +## 3. Shared Agentic Loop + +Port `BaseAgent._run_agentic_loop` from `shared/base_agent.py` into a standalone async utility in `utils/agentic_loop.py`. Instead of using `AsyncOpenAI` directly, it takes a LangChain `BaseChatModel` (obtained from `builder.get_llm()`) and uses `model.bind_tools(tool_defs).ainvoke(messages)`. + +**`utils/agentic_loop.py`** -- key function: + +```python +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 = "", +) -> tuple[str, int, int]: + """Run the tool-calling loop. Returns (final_content, tool_calls, llm_calls).""" +``` + +Preserves from `BaseAgent`: + +- **Context compaction** (`_compact_context`) -- shrinks old tool/assistant messages when nearing token limit +- **Deadline nudge** (`_deadline_nudge`) -- wrap-up message at `wrap_up_fraction` of max iterations +- **Loop detection** -- skip repeated identical tool calls after threshold +- **LLM retry** -- exponential backoff on 429/5xx (handled by LangChain's retry or custom wrapper) +- **Empty response nudge** -- re-prompts on empty assistant content + +Adaptation: convert between LangChain `BaseMessage` types and the loop logic. LangChain's `AIMessage.tool_calls` provides `[{"name", "args", "id"}]` -- same information as OpenAI's format. + +### NAT intermediate steps for observability + +The agentic loop emits NAT intermediate steps so that `nat serve` can stream per-iteration progress and tracing backends (Phoenix) capture individual tool calls. Uses NAT's `IntermediateStepManager` via `SyncBuilder.current().context` to record each iteration: + +```python +from nat.builder import SyncBuilder + +async def run_agentic_loop(...) -> tuple[str, int, int]: + ctx = SyncBuilder.current().context + step_mgr = ctx.intermediate_step_manager + + for iteration in range(max_iterations): + # ... LLM call ... + step_mgr.add_step( + agent_action=f"[{phase_label}] LLM call #{iteration + 1}", + observation=_summarize_response(ai_message), + ) + + for tool_call in ai_message.tool_calls: + result = await _execute_tool(tool_call, tools) + step_mgr.add_step( + agent_action=f"[{phase_label}] tool:{tool_call['name']}", + observation=_truncate(result, max_len=500), + ) +``` + +--- + +## 4. Tool Registration + +Each tool is registered as its own `@register_function` with its own config class (see [9.2](#92-one-register_function-per-tool----do-not-yield-multiple-functioninfo-objects)). Default values come from the cve-agent-v3 config files. + +**`tools/web_tools.py`** -- web_fetch tool: + +```python +class WebFetchToolConfig(FunctionBaseConfig, name="web_fetch"): + timeout: int = Field(default=30) # FETCH_TIMEOUT + max_content_length: int = Field(default=50000) # MAX_CONTENT_LENGTH + +@register_function(config_type=WebFetchToolConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def web_fetch_tool(config: WebFetchToolConfig, builder: Builder): + async def _arun(url: str) -> str: + # Port logic from cve_agent/shared/tools.py web_fetch() + ... + yield FunctionInfo.from_fn(_arun, description="Fetch and extract content from a URL") +``` + +For **web_search**, use NAT's built-in `tavily_internet_search` tool in config.yml. Override defaults: `search_depth: advanced`, `max_results: 5`. + +**`tools/osv_tools.py`** -- osv_lookup: port from `shared/tools.py`. + +**`tools/filesystem_tools.py`** -- read_file, list_directory, grep_search, glob_find: port from `shared/filesystem_tools.py`. Each tool gets its own `@register_function` with a shared base config class. Includes `contextvars.ContextVar`-based path sandboxing via `configure_allowed_paths()` (see [9.4](#94-filesystem-tool-path-sandboxing-requires-contextvarscontextvar)): + +```python +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_glob_results: int = Field(default=200) + +class ReadFileConfig(_FsBaseConfig, name="read_file"): pass +class ListDirectoryConfig(_FsBaseConfig, name="list_directory"): pass +class GrepSearchConfig(_FsBaseConfig, name="grep_search"): pass +class GlobFindConfig(_FsBaseConfig, name="glob_find"): pass +``` + +**`tools/binary_tools.py`** -- port from `exploitability_analyzer/tools.py`. Each tool gets its own `@register_function`: + +```python +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 +class SearchBinaryContentConfig(_BinaryBaseConfig, name="search_binary_content"): pass +class CheckDistroPatchesConfig(_BinaryBaseConfig, name="check_distro_patches"): pass +``` + +--- + +## 5. Agent Functions + +Each agent is a `@register_function` that: + +1. Gets an LLM from `builder.get_llm(wrapper_type=LLMFrameworkEnum.LANGCHAIN)` +2. Gets tools from `builder.get_tools(wrapper_type=LLMFrameworkEnum.LANGCHAIN)` +3. Runs the agentic loop from `utils/agentic_loop.py` +4. Returns a typed result +5. Provides `converters` for standalone `nat run` (see [9.7](#97-agent-functions-need-converters-for-standalone-nat-run)) + +All default values come directly from the corresponding `config.py` in cve-agent-v3. + +**`functions/cve_researcher.py`**: + +```python +class CVEResearcherConfig(FunctionBaseConfig, name="cve_researcher"): + llm_name: str = Field(description="LLM for CVE research") + tool_names: list[FunctionRef] = Field(description="Tools: web_search, web_fetch, osv_lookup") + max_iterations: int = Field(default=30) + context_window_tokens: int = Field(default=262144) + wrap_up_fraction: float = Field(default=0.8) + loop_detection_threshold: int = Field(default=2) + output_subdir: str = Field(default="vulnerability_reports") + description: str = Field(default="Researches a CVE and produces a vulnerability report") + +@register_function(config_type=CVEResearcherConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def cve_researcher(config: CVEResearcherConfig, 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) + + async def _arun(inp: CVEResearcherInput) -> CVEResearcherResult: + messages = [SystemMessage(content=SYSTEM_PROMPT), HumanMessage(content=build_user_message(inp))] + content, tool_calls, llm_calls = await run_agentic_loop( + llm=llm, messages=messages, tools=tools, + max_iterations=config.max_iterations, + context_window_tokens=config.context_window_tokens, + wrap_up_fraction=config.wrap_up_fraction, + loop_threshold=config.loop_detection_threshold, + phase_label="CVE Researcher", + ) + return CVEResearcherResult(identifier=inp.identifier, report_content=content, success=True, + tool_call_count=tool_calls, llm_call_count=llm_calls) + + # Converters required for standalone `nat run` (see 9.7) + def _str_to_input(s: str) -> CVEResearcherInput: + return CVEResearcherInput.model_validate_json(s) + def _result_to_str(r: CVEResearcherResult) -> str: + return r.model_dump_json() + + yield FunctionInfo.from_fn(_arun, input_schema=CVEResearcherInput, description=config.description, + converters=[_str_to_input, _result_to_str]) +``` + +**`functions/container_analyzer.py`** -- two-phase approach: + +- Phase 1 (web research): runs only for images matching `public_registry_prefixes`; uses `phase1_llm_name` and `web_tool_names` +- Phase 2 (filesystem analysis): always runs; uses `phase2_llm_name` and `fs_tool_names` +- Calls `configure_allowed_paths()` before Phase 2 (see [9.4](#94-filesystem-tool-path-sandboxing-requires-contextvarscontextvar)) +- Port `WebResearchSubAgent` into `utils/web_researcher.py` + +**`functions/exploitability_analyzer.py`** -- two-phase approach: + +- Phase 1: Deterministic pre-triage (no LLM). Uses `discovery_probe_paths` for filesystem probing. +- If pre-triage doesn't early-exit, Phase 2 runs LLM agentic loop with filesystem + binary tools +- Calls `configure_allowed_paths()` before Phase 2 +- Early-exit generates template report via `report_generator.py` + +**`functions/vex_categorizer.py`** -- LLM-only agent using `reason` tool (scratchpad). Reads exploitability and container reports, produces VEX status/justification. + +--- + +## 6. Workflow Function (Orchestrator) + +The main workflow uses `@register_function` with LangGraph `StateGraph` inline. + +**`register.py`:** + +```python +class CVEPipelineWorkflowConfig(FunctionBaseConfig, name="cve_pipeline"): + cve_researcher_name: FunctionRef + container_analyzer_name: FunctionRef + exploitability_analyzer_name: FunctionRef + vex_categorizer_name: FunctionRef + stage_timeout: int = Field(default=600) + stage_max_retries: int = Field(default=1) + 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): + cve_researcher_fn = await builder.get_function(name=config.cve_researcher_name) + container_analyzer_fn = await builder.get_function(name=config.container_analyzer_name) + exploitability_fn = await builder.get_function(name=config.exploitability_analyzer_name) + vex_fn = await builder.get_function(name=config.vex_categorizer_name) + + # --- LangGraph node functions --- + async def stage1_parallel(state: PipelineState) -> dict: + results = await asyncio.gather( + _run_with_retry(cve_researcher_fn, make_cve_input(state), config), + _run_with_retry(container_analyzer_fn, make_container_input(state), config), + return_exceptions=True, + ) + cve_result = _result_or_failure(results[0], "cve_researcher") + container_result = _result_or_failure(results[1], "container_analyzer") + return {"cve_report": cve_result, "container_report": container_result} + + # ... exploitability_node, vex_node, fail_safe_node, output_results_node ... + + # --- Build StateGraph --- + graph_builder = StateGraph(PipelineState) + graph_builder.add_node("stage1_parallel", stage1_parallel) + graph_builder.add_node("exploitability", exploitability_node) + graph_builder.add_node("vex_categorizer", vex_node) + 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) + graph = graph_builder.compile() + + # --- Response function (bare yield, see 9.5) --- + async def _response_fn(inp: PipelineInput) -> PipelineResult: + initial_state = build_initial_state(inp) + result = await graph.ainvoke(initial_state) + return PipelineResult(**result) + + yield FunctionInfo.from_fn( + _response_fn, input_schema=PipelineInput, description=config.description, + converters=[str_to_pipeline_input, pipeline_result_to_str] + ) +``` + +**`register.py` imports** (triggers `@register_function` decorators): + +```python +from vuln_analysis.functions import cve_researcher, container_analyzer, exploitability_analyzer, vex_categorizer +from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_tools +``` + +--- + +## 7. Configuration Files + +All default values are sourced from the cve-agent-v3 config files. + +See the actual YAML files in `src/vuln_analysis/configs/` for the full configuration. Key points: + +- 5 separate LLM configs (different temperature/max_tokens per agent) +- Each tool listed individually under `functions:` -- no YAML anchors (see [9.3](#93-do-not-use-yaml-anchors-under-functions)) +- Individual agent configs for standalone `nat run` (workflow section references the agent directly) + +```bash +# Full pipeline +nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json + +# Individual agent +nat run --config_file=configs/config-cve-researcher.yml --input='{"cve_id":"CVE-2025-47273"}' + +# Serve as HTTP API +nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 8000 +``` + +--- + +## Migration Sequence + +Implement in this order so each step can be validated independently: + +1. **Remove old code** -- delete old workflow-specific files from `src/vuln_analysis/` +2. **Scaffold** -- update pyproject.toml, create new directory structure, write new register.py skeleton +3. **Data models** -- port all schemas to Pydantic under `src/vuln_analysis/data_models/` +4. **Utils** -- keep reusable utils, add agentic_loop.py, helpers.py, report_parser.py, pre_triage.py, report_generator.py, web_researcher.py +5. **Tools** -- register all tool functions under `src/vuln_analysis/tools/` +6. **Agent: cve_researcher** -- simplest agent (web tools only), validate with `nat run` +7. **Agent: container_analyzer** -- two-phase, validate individually +8. **Agent: exploitability_analyzer** -- pre-triage + LLM, validate individually +9. **Agent: vex_categorizer** -- LLM-only, validate individually +10. **Workflow** -- LangGraph orchestrator with parallel Stage 1, validate full pipeline +11. **Serve** -- test `nat serve` with HTTP API + +> **Important:** Read [Section 9 (NAT Integration Pitfalls)](#9-nat-integration-pitfalls-lessons-learned) before starting. These issues are critical for correct operation and should be incorporated inline rather than fixed after the fact. + +--- + +## 9. NAT Integration Pitfalls (Lessons Learned) + +The following issues were discovered during runtime validation and are **required** for the pipeline to function correctly. Incorporate these inline when following this plan. + +### 9.1 Do NOT use `from __future__ import annotations` in files that register NAT functions + +**Problem:** NAT's `FunctionInfo.from_fn` calls `typing.get_type_hints()` on the inner `_arun` function to discover the input/output schemas. With `from __future__ import annotations`, all type annotations become lazy strings. When `get_type_hints()` tries to resolve them for nested (closure) functions, it can't find the types in the function's namespace and raises `NameError`. + +**Fix:** Remove `from __future__ import annotations` from all files that call `FunctionInfo.from_fn` or `@register_function`. Since we target Python 3.13, the `X | Y` union syntax works natively without the future import. Affected files: + +- `functions/cve_researcher.py` +- `functions/container_analyzer.py` +- `functions/exploitability_analyzer.py` +- `functions/vex_categorizer.py` +- `tools/web_tools.py` +- `tools/osv_tools.py` +- `tools/filesystem_tools.py` +- `tools/binary_tools.py` + +Files that DON'T register NAT functions (data_models, prompts, utils, register.py) can safely use `from __future__ import annotations`. + +### 9.2 One `@register_function` per tool -- do NOT yield multiple FunctionInfo objects + +**Problem:** Bundling multiple tools under a single `@register_function` (e.g., `filesystem_tools` yielding 4 `FunctionInfo` objects) does not work. NAT only uses the **first** yielded `FunctionInfo` per registration and silently ignores the rest. The LLM then can only call the first tool and has no access to the others. + +**Fix:** Register each tool as its own `@register_function` with its own config class. Use a shared base config class in Python to avoid duplicating field definitions. Each tool gets its own `@register_function` decorator and yields exactly one `FunctionInfo`. + +In YAML configs, list each tool individually under `functions:` and in agent `tool_names`. + +### 9.3 Do NOT use YAML anchors under `functions:` + +**Problem:** YAML anchors (`_fs_defaults: &fs_defaults`) placed under the `functions:` section are treated by NAT as function definitions. NAT tries to find a `_type` discriminator in each entry and fails with `Unable to extract tag using discriminator`. + +**Fix:** Omit config values and let the Python defaults apply, or repeat values explicitly. Do not use YAML anchors or merge keys under `functions:`. + +### 9.4 Filesystem tool path sandboxing requires `contextvars.ContextVar` + +**Problem:** In cve-agent-v3, each agent owned its own `FilesystemToolkit` instance and called `set_allowed_paths()` directly. In NAT, tools are registered globally and shared across all functions. The agent function can't reach the toolkit instance inside the tool's registration closure. + +**Fix:** Added a `contextvars.ContextVar` bridge in `filesystem_tools.py`: + +1. **`configure_allowed_paths(roots, files, filesystem_root)`** -- module-level function that stores the allowed-paths config in a `ContextVar`. Agent functions call this before running their agentic loop. +2. **`get_filesystem_root()`** -- module-level function that reads the filesystem root from the context var. Used by `binary_tools.py` for `check_distro_patches`. +3. **`FilesystemToolkit._validate_path()`** -- falls through to the `ContextVar` when the instance-level `set_allowed_paths()` hasn't been called. + +```python +# container_analyzer.py -- before Phase 2 +from vuln_analysis.tools.filesystem_tools import configure_allowed_paths +configure_allowed_paths(roots=[inp.filesystem_path]) + +# exploitability_analyzer.py -- before Phase 2 +configure_allowed_paths( + roots=[inp.filesystem_path], + files=[inp.vulnerability_report_path, inp.container_report_path], + filesystem_root=inp.filesystem_path, +) +``` + +### 9.5 Do NOT wrap the workflow yield in `try/except GeneratorExit` or `try/finally` + +**Problem:** Wrapping `yield FunctionInfo.from_fn(...)` in `try/except GeneratorExit/finally` violates the generator protocol when NAT closes the async generator, causing `RuntimeError: generator didn't stop`. + +**Fix:** Use a bare `yield` without any exception handling: + +```python +# DON'T do this: +try: + yield FunctionInfo.from_fn(...) +except GeneratorExit: + logger.info("Exited!") +finally: + logger.info("Cleanup") + +# DO this: +yield FunctionInfo.from_fn(...) +``` + +### 9.6 Sample input data + +Add `data/examples/` with sample JSON inputs for validation and onboarding: + +- `pipeline_input.json` -- full pipeline input +- `cve_researcher_input.json` -- standalone CVE researcher +- `container_analyzer_input.json` -- standalone container analyzer +- `exploitability_analyzer_input.json` -- standalone exploitability analyzer +- `vex_categorizer_input.json` -- standalone VEX categorizer + +### 9.7 Agent functions need `converters` for standalone `nat run` + +**Problem:** When an agent function is used as the `workflow:` entry point (standalone `nat run`), NAT passes the raw CLI `--input` string to `ainvoke`. Without converters, NAT can't convert the JSON string into the Pydantic model the `_arun` function expects, causing `TypeError`. + +**Fix:** Add `str → Model` and `Model → str` converters to every agent function's `FunctionInfo.from_fn` call: + +```python +def _str_to_input(s: str) -> CVEResearcherInput: + return CVEResearcherInput.model_validate_json(s) + +def _result_to_str(r: CVEResearcherResult) -> str: + return r.model_dump_json() + +yield FunctionInfo.from_fn( + _arun, input_schema=CVEResearcherInput, description=config.description, + converters=[_str_to_input, _result_to_str], +) +``` + +Apply to all four agent functions and the pipeline workflow in `register.py`. + +### 9.8 Clear `__pycache__` after making fixes + +After applying fixes to tool/function registration files, stale `.pyc` bytecode can cause old code to execute even though the source files have changed: + +```bash +find src/vuln_analysis -type d -name __pycache__ -exec rm -rf {} + +``` From 11132d903419a2b6f45728eeb430553d4cc247b5 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Wed, 11 Mar 2026 16:01:36 -0400 Subject: [PATCH 05/48] Use generic paths in pipeline_input.json example Made-with: Cursor --- src/vuln_analysis/data/examples/pipeline_input.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vuln_analysis/data/examples/pipeline_input.json b/src/vuln_analysis/data/examples/pipeline_input.json index 28f691e8e..7e424c945 100644 --- a/src/vuln_analysis/data/examples/pipeline_input.json +++ b/src/vuln_analysis/data/examples/pipeline_input.json @@ -1,8 +1,8 @@ { "cve_id": "CVE-2025-47273", "image_name": "nvcr.io/nvidia/morpheus/morpheus:25.06-runtime", - "filesystem_path": "/Users/efajardo/containers/morpheus/filesystem", - "image_config_path": "/Users/efajardo/containers/morpheus/image_config.json", + "filesystem_path": "/path/to/filesystem", + "image_config_path": "/path/to/image_config.json", "output_dir": "outputs", "cache": true } From 608e982cbca421532403550577335b84762b66bd Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Wed, 11 Mar 2026 17:41:43 -0400 Subject: [PATCH 06/48] Add tools missed in migration Made-with: Cursor --- MIGRATION_PLAN.md | 243 +++--- .../config-exploitability-analyzer.yml | 53 +- src/vuln_analysis/configs/config.yml | 53 +- src/vuln_analysis/register.py | 1 + src/vuln_analysis/tools/binary_tools.py | 48 ++ src/vuln_analysis/tools/package_tools.py | 731 ++++++++++++++++++ 6 files changed, 978 insertions(+), 151 deletions(-) create mode 100644 src/vuln_analysis/tools/package_tools.py diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index 71884911e..bc08afde7 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -98,6 +98,8 @@ src/vuln_analysis/ config-container-analyzer.yml config-exploitability-analyzer.yml config-vex-categorizer.yml + data/ + examples/ # sample inputs for validation data_models/ __init__.py input.py # PipelineInput (Pydantic) @@ -115,10 +117,11 @@ src/vuln_analysis/ vex_categorizer.py tools/ __init__.py - web_tools.py # web_fetch (custom), web_search uses NAT tavily + web_tools.py # web_fetch osv_tools.py # osv_lookup filesystem_tools.py # read_file, list_directory, grep_search, glob_find - binary_tools.py # check_binary_dependencies, search_binary_content, check_distro_patches + binary_tools.py # check_binary_dependencies, search_binary_content, check_distro_patches, get_binary_linked_libraries + package_tools.py # 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 prompts/ __init__.py cve_researcher.py # system prompts, user message builders @@ -276,19 +279,24 @@ async def run_agentic_loop(...) -> tuple[str, int, int]: ## 4. Tool Registration -Each tool is registered as its own `@register_function` with its own config class (see [9.2](#92-one-register_function-per-tool----do-not-yield-multiple-functioninfo-objects)). Default values come from the cve-agent-v3 config files. +Each tool is registered as its own `@register_function` with its own config class, yielding exactly **one** `FunctionInfo` per registration. Default values come from the cve-agent-v3 config files. + +> **CRITICAL -- One tool per `@register_function`:** NAT only uses the first yielded `FunctionInfo` per registration and silently ignores the rest. Do NOT bundle multiple tools under one `@register_function`. Use a shared base config class to avoid duplicating field definitions. + +> **CRITICAL -- No `from __future__ import annotations`:** NAT's `FunctionInfo.from_fn` calls `typing.get_type_hints()` on inner functions. With `from __future__ import annotations`, annotations become lazy strings that can't be resolved in nested closures, causing `NameError`. Since we target Python 3.13, the `X | Y` syntax works natively. This applies to all files under `tools/` and `functions/`. + +> **CRITICAL -- At least one parameter per tool:** NAT's `FunctionInfo.from_fn` requires every tool function to have at least one parameter. For tools that logically take no arguments, add a dummy parameter with a default: `scope: str = "all"`. **`tools/web_tools.py`** -- web_fetch tool: ```python class WebFetchToolConfig(FunctionBaseConfig, name="web_fetch"): - timeout: int = Field(default=30) # FETCH_TIMEOUT - max_content_length: int = Field(default=50000) # MAX_CONTENT_LENGTH + 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): async def _arun(url: str) -> str: - # Port logic from cve_agent/shared/tools.py web_fetch() ... yield FunctionInfo.from_fn(_arun, description="Fetch and extract content from a URL") ``` @@ -297,7 +305,7 @@ For **web_search**, use NAT's built-in `tavily_internet_search` tool in config.y **`tools/osv_tools.py`** -- osv_lookup: port from `shared/tools.py`. -**`tools/filesystem_tools.py`** -- read_file, list_directory, grep_search, glob_find: port from `shared/filesystem_tools.py`. Each tool gets its own `@register_function` with a shared base config class. Includes `contextvars.ContextVar`-based path sandboxing via `configure_allowed_paths()` (see [9.4](#94-filesystem-tool-path-sandboxing-requires-contextvarscontextvar)): +**`tools/filesystem_tools.py`** -- read_file, list_directory, grep_search, glob_find: port from `shared/filesystem_tools.py`. Each tool gets its own `@register_function` with a shared base config class. Includes `contextvars.ContextVar`-based path sandboxing (see below): ```python class _FsBaseConfig(FunctionBaseConfig): @@ -312,7 +320,15 @@ class GrepSearchConfig(_FsBaseConfig, name="grep_search"): pass class GlobFindConfig(_FsBaseConfig, name="glob_find"): pass ``` -**`tools/binary_tools.py`** -- port from `exploitability_analyzer/tools.py`. Each tool gets its own `@register_function`: +### Filesystem path sandboxing with `contextvars.ContextVar` + +In cve-agent-v3, each agent owned its own `FilesystemToolkit` instance and called `set_allowed_paths()` directly. In NAT, tools are registered globally and shared. The solution is a `contextvars.ContextVar` bridge in `filesystem_tools.py`: + +1. **`configure_allowed_paths(roots, files, filesystem_root)`** -- module-level function that stores the config in a `ContextVar`. Agent functions call this before their agentic loop. +2. **`get_filesystem_root()`** -- module-level function that reads the filesystem root. Used by `binary_tools.py` and `package_tools.py`. +3. **`FilesystemToolkit._validate_path()`** -- falls through to the `ContextVar` when instance-level config hasn't been set. + +**`tools/binary_tools.py`** -- port from `exploitability_analyzer/tools.py`: ```python class _BinaryBaseConfig(FunctionBaseConfig): @@ -322,8 +338,53 @@ class _BinaryBaseConfig(FunctionBaseConfig): class CheckBinaryDepsConfig(_BinaryBaseConfig, name="check_binary_dependencies"): pass class SearchBinaryContentConfig(_BinaryBaseConfig, name="search_binary_content"): pass class CheckDistroPatchesConfig(_BinaryBaseConfig, name="check_distro_patches"): pass +class GetBinaryLinkedLibsConfig(_BinaryBaseConfig, name="get_binary_linked_libraries"): pass +``` + +**`tools/package_tools.py`** -- port from `exploitability_analyzer/tools.py`. Package database, library discovery, and security control tools. Each uses `get_filesystem_root()`: + +| Tool Name | Purpose | +|-----------|---------| +| `query_dpkg_database` | Search dpkg/status for Debian/Ubuntu packages | +| `search_dpkg_file_lists` | Find which package owns a file | +| `query_rpm_database` | Search RPM database for RHEL/Fedora/SUSE packages | +| `list_python_packages` | Enumerate pip packages across all environments | +| `find_shared_libraries` | Locate .so files by name pattern | +| `find_go_binaries` | Find Go binaries and list embedded modules | +| `check_security_controls` | Detect AppArmor, seccomp, non-root users | +| `query_npm_packages` | Find Node.js/npm packages | +| `find_java_packages` | Locate JARs and extract manifest/maven metadata | +| `list_installed_packages` | Aggregate summary of all package ecosystems | + +Import all tool modules in `register.py` to trigger `@register_function` decorators: + +```python +from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_tools, package_tools ``` +### Complete tool inventory + +All 21 tools across 5 files, plus 1 NAT built-in: + +| File | Tools | +|------|-------| +| NAT built-in | `tavily_internet_search` (configured as `web_search` in YAML) | +| `tools/web_tools.py` | `web_fetch` | +| `tools/osv_tools.py` | `osv_lookup` | +| `tools/filesystem_tools.py` | `read_file`, `list_directory`, `grep_search`, `glob_find` | +| `tools/binary_tools.py` | `check_binary_dependencies`, `search_binary_content`, `check_distro_patches`, `get_binary_linked_libraries` | +| `tools/package_tools.py` | `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` | + +Agent tool assignments in YAML configs: + +| Agent | Tools | +|-------|-------| +| `cve_researcher` | `web_search`, `web_fetch`, `osv_lookup` (3 tools) | +| `container_analyzer` (Phase 1) | `web_search`, `web_fetch` (2 tools) | +| `container_analyzer` (Phase 2) | `read_file`, `list_directory`, `grep_search`, `glob_find` (4 tools) | +| `exploitability_analyzer` | All 18 filesystem/binary/package tools | +| `vex_categorizer` | `reason` (LangChain `@tool`, not a NAT registration) | + --- ## 5. Agent Functions @@ -334,7 +395,9 @@ Each agent is a `@register_function` that: 2. Gets tools from `builder.get_tools(wrapper_type=LLMFrameworkEnum.LANGCHAIN)` 3. Runs the agentic loop from `utils/agentic_loop.py` 4. Returns a typed result -5. Provides `converters` for standalone `nat run` (see [9.7](#97-agent-functions-need-converters-for-standalone-nat-run)) +5. Provides `converters` for standalone `nat run` (string-to-model and model-to-string) + +> **CRITICAL -- Converters are required:** When an agent is used as the standalone `workflow:` entry point (`nat run --config_file=config-cve-researcher.yml --input='...'`), NAT passes the raw CLI `--input` string to `ainvoke`. Without converters, NAT can't convert the JSON string into the Pydantic model, causing `TypeError`. Add `str → Model` and `Model → str` converters to every `FunctionInfo.from_fn` call. All default values come directly from the corresponding `config.py` in cve-agent-v3. @@ -369,7 +432,6 @@ async def cve_researcher(config: CVEResearcherConfig, builder: Builder): return CVEResearcherResult(identifier=inp.identifier, report_content=content, success=True, tool_call_count=tool_calls, llm_call_count=llm_calls) - # Converters required for standalone `nat run` (see 9.7) def _str_to_input(s: str) -> CVEResearcherInput: return CVEResearcherInput.model_validate_json(s) def _result_to_str(r: CVEResearcherResult) -> str: @@ -383,14 +445,14 @@ async def cve_researcher(config: CVEResearcherConfig, builder: Builder): - Phase 1 (web research): runs only for images matching `public_registry_prefixes`; uses `phase1_llm_name` and `web_tool_names` - Phase 2 (filesystem analysis): always runs; uses `phase2_llm_name` and `fs_tool_names` -- Calls `configure_allowed_paths()` before Phase 2 (see [9.4](#94-filesystem-tool-path-sandboxing-requires-contextvarscontextvar)) +- Calls `configure_allowed_paths(roots=[inp.filesystem_path])` before Phase 2 - Port `WebResearchSubAgent` into `utils/web_researcher.py` **`functions/exploitability_analyzer.py`** -- two-phase approach: - Phase 1: Deterministic pre-triage (no LLM). Uses `discovery_probe_paths` for filesystem probing. -- If pre-triage doesn't early-exit, Phase 2 runs LLM agentic loop with filesystem + binary tools -- Calls `configure_allowed_paths()` before Phase 2 +- If pre-triage doesn't early-exit, Phase 2 runs LLM agentic loop with all 18 filesystem/binary/package tools +- Calls `configure_allowed_paths(roots=[...], files=[...], filesystem_root=...)` before Phase 2 - Early-exit generates template report via `report_generator.py` **`functions/vex_categorizer.py`** -- LLM-only agent using `reason` tool (scratchpad). Reads exploitability and container reports, produces VEX status/justification. @@ -401,6 +463,8 @@ async def cve_researcher(config: CVEResearcherConfig, builder: Builder): The main workflow uses `@register_function` with LangGraph `StateGraph` inline. +> **CRITICAL -- Bare yield only:** Do NOT wrap the `yield FunctionInfo.from_fn(...)` in `try/except GeneratorExit` or `try/finally`. This violates the async generator protocol and causes `RuntimeError: generator didn't stop`. + **`register.py`:** ```python @@ -436,30 +500,15 @@ async def cve_pipeline_workflow(config: CVEPipelineWorkflowConfig, builder: Buil # --- Build StateGraph --- graph_builder = StateGraph(PipelineState) - graph_builder.add_node("stage1_parallel", stage1_parallel) - graph_builder.add_node("exploitability", exploitability_node) - graph_builder.add_node("vex_categorizer", vex_node) - 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) + # ... add nodes and edges ... graph = graph_builder.compile() - # --- Response function (bare yield, see 9.5) --- async def _response_fn(inp: PipelineInput) -> PipelineResult: initial_state = build_initial_state(inp) result = await graph.ainvoke(initial_state) return PipelineResult(**result) + # Bare yield -- no try/finally yield FunctionInfo.from_fn( _response_fn, input_schema=PipelineInput, description=config.description, converters=[str_to_pipeline_input, pipeline_result_to_str] @@ -470,7 +519,7 @@ async def cve_pipeline_workflow(config: CVEPipelineWorkflowConfig, builder: Buil ```python from vuln_analysis.functions import cve_researcher, container_analyzer, exploitability_analyzer, vex_categorizer -from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_tools +from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_tools, package_tools ``` --- @@ -479,12 +528,22 @@ from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_t All default values are sourced from the cve-agent-v3 config files. +> **CRITICAL -- No YAML anchors under `functions:`:** NAT treats every key under `functions:` as a function definition and requires a `_type` discriminator. YAML anchors placed there become phantom entries that NAT can't parse. Omit config values and let Pydantic defaults apply, or repeat values explicitly. + See the actual YAML files in `src/vuln_analysis/configs/` for the full configuration. Key points: - 5 separate LLM configs (different temperature/max_tokens per agent) -- Each tool listed individually under `functions:` -- no YAML anchors (see [9.3](#93-do-not-use-yaml-anchors-under-functions)) +- Each tool listed individually under `functions:` with just its `_type` - Individual agent configs for standalone `nat run` (workflow section references the agent directly) +Add `data/examples/` with sample JSON inputs for validation and onboarding: + +- `pipeline_input.json` -- full pipeline input +- `cve_researcher_input.json` -- standalone CVE researcher +- `container_analyzer_input.json` -- standalone container analyzer +- `exploitability_analyzer_input.json` -- standalone exploitability analyzer +- `vex_categorizer_input.json` -- standalone VEX categorizer + ```bash # Full pipeline nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json @@ -506,129 +565,15 @@ Implement in this order so each step can be validated independently: 2. **Scaffold** -- update pyproject.toml, create new directory structure, write new register.py skeleton 3. **Data models** -- port all schemas to Pydantic under `src/vuln_analysis/data_models/` 4. **Utils** -- keep reusable utils, add agentic_loop.py, helpers.py, report_parser.py, pre_triage.py, report_generator.py, web_researcher.py -5. **Tools** -- register all tool functions under `src/vuln_analysis/tools/` +5. **Tools** -- register all 21 tool functions under `src/vuln_analysis/tools/` (one `@register_function` per tool) 6. **Agent: cve_researcher** -- simplest agent (web tools only), validate with `nat run` 7. **Agent: container_analyzer** -- two-phase, validate individually -8. **Agent: exploitability_analyzer** -- pre-triage + LLM, validate individually +8. **Agent: exploitability_analyzer** -- pre-triage + LLM with all 18 filesystem/binary/package tools, validate individually 9. **Agent: vex_categorizer** -- LLM-only, validate individually 10. **Workflow** -- LangGraph orchestrator with parallel Stage 1, validate full pipeline 11. **Serve** -- test `nat serve` with HTTP API -> **Important:** Read [Section 9 (NAT Integration Pitfalls)](#9-nat-integration-pitfalls-lessons-learned) before starting. These issues are critical for correct operation and should be incorporated inline rather than fixed after the fact. - ---- - -## 9. NAT Integration Pitfalls (Lessons Learned) - -The following issues were discovered during runtime validation and are **required** for the pipeline to function correctly. Incorporate these inline when following this plan. - -### 9.1 Do NOT use `from __future__ import annotations` in files that register NAT functions - -**Problem:** NAT's `FunctionInfo.from_fn` calls `typing.get_type_hints()` on the inner `_arun` function to discover the input/output schemas. With `from __future__ import annotations`, all type annotations become lazy strings. When `get_type_hints()` tries to resolve them for nested (closure) functions, it can't find the types in the function's namespace and raises `NameError`. - -**Fix:** Remove `from __future__ import annotations` from all files that call `FunctionInfo.from_fn` or `@register_function`. Since we target Python 3.13, the `X | Y` union syntax works natively without the future import. Affected files: - -- `functions/cve_researcher.py` -- `functions/container_analyzer.py` -- `functions/exploitability_analyzer.py` -- `functions/vex_categorizer.py` -- `tools/web_tools.py` -- `tools/osv_tools.py` -- `tools/filesystem_tools.py` -- `tools/binary_tools.py` - -Files that DON'T register NAT functions (data_models, prompts, utils, register.py) can safely use `from __future__ import annotations`. - -### 9.2 One `@register_function` per tool -- do NOT yield multiple FunctionInfo objects - -**Problem:** Bundling multiple tools under a single `@register_function` (e.g., `filesystem_tools` yielding 4 `FunctionInfo` objects) does not work. NAT only uses the **first** yielded `FunctionInfo` per registration and silently ignores the rest. The LLM then can only call the first tool and has no access to the others. - -**Fix:** Register each tool as its own `@register_function` with its own config class. Use a shared base config class in Python to avoid duplicating field definitions. Each tool gets its own `@register_function` decorator and yields exactly one `FunctionInfo`. - -In YAML configs, list each tool individually under `functions:` and in agent `tool_names`. - -### 9.3 Do NOT use YAML anchors under `functions:` - -**Problem:** YAML anchors (`_fs_defaults: &fs_defaults`) placed under the `functions:` section are treated by NAT as function definitions. NAT tries to find a `_type` discriminator in each entry and fails with `Unable to extract tag using discriminator`. - -**Fix:** Omit config values and let the Python defaults apply, or repeat values explicitly. Do not use YAML anchors or merge keys under `functions:`. - -### 9.4 Filesystem tool path sandboxing requires `contextvars.ContextVar` - -**Problem:** In cve-agent-v3, each agent owned its own `FilesystemToolkit` instance and called `set_allowed_paths()` directly. In NAT, tools are registered globally and shared across all functions. The agent function can't reach the toolkit instance inside the tool's registration closure. - -**Fix:** Added a `contextvars.ContextVar` bridge in `filesystem_tools.py`: - -1. **`configure_allowed_paths(roots, files, filesystem_root)`** -- module-level function that stores the allowed-paths config in a `ContextVar`. Agent functions call this before running their agentic loop. -2. **`get_filesystem_root()`** -- module-level function that reads the filesystem root from the context var. Used by `binary_tools.py` for `check_distro_patches`. -3. **`FilesystemToolkit._validate_path()`** -- falls through to the `ContextVar` when the instance-level `set_allowed_paths()` hasn't been called. - -```python -# container_analyzer.py -- before Phase 2 -from vuln_analysis.tools.filesystem_tools import configure_allowed_paths -configure_allowed_paths(roots=[inp.filesystem_path]) - -# exploitability_analyzer.py -- before Phase 2 -configure_allowed_paths( - roots=[inp.filesystem_path], - files=[inp.vulnerability_report_path, inp.container_report_path], - filesystem_root=inp.filesystem_path, -) -``` - -### 9.5 Do NOT wrap the workflow yield in `try/except GeneratorExit` or `try/finally` - -**Problem:** Wrapping `yield FunctionInfo.from_fn(...)` in `try/except GeneratorExit/finally` violates the generator protocol when NAT closes the async generator, causing `RuntimeError: generator didn't stop`. - -**Fix:** Use a bare `yield` without any exception handling: - -```python -# DON'T do this: -try: - yield FunctionInfo.from_fn(...) -except GeneratorExit: - logger.info("Exited!") -finally: - logger.info("Cleanup") - -# DO this: -yield FunctionInfo.from_fn(...) -``` - -### 9.6 Sample input data - -Add `data/examples/` with sample JSON inputs for validation and onboarding: - -- `pipeline_input.json` -- full pipeline input -- `cve_researcher_input.json` -- standalone CVE researcher -- `container_analyzer_input.json` -- standalone container analyzer -- `exploitability_analyzer_input.json` -- standalone exploitability analyzer -- `vex_categorizer_input.json` -- standalone VEX categorizer - -### 9.7 Agent functions need `converters` for standalone `nat run` - -**Problem:** When an agent function is used as the `workflow:` entry point (standalone `nat run`), NAT passes the raw CLI `--input` string to `ainvoke`. Without converters, NAT can't convert the JSON string into the Pydantic model the `_arun` function expects, causing `TypeError`. - -**Fix:** Add `str → Model` and `Model → str` converters to every agent function's `FunctionInfo.from_fn` call: - -```python -def _str_to_input(s: str) -> CVEResearcherInput: - return CVEResearcherInput.model_validate_json(s) - -def _result_to_str(r: CVEResearcherResult) -> str: - return r.model_dump_json() - -yield FunctionInfo.from_fn( - _arun, input_schema=CVEResearcherInput, description=config.description, - converters=[_str_to_input, _result_to_str], -) -``` - -Apply to all four agent functions and the pipeline workflow in `register.py`. - -### 9.8 Clear `__pycache__` after making fixes - -After applying fixes to tool/function registration files, stale `.pyc` bytecode can cause old code to execute even though the source files have changed: +After any fix to tool/function files, clear stale bytecode: ```bash find src/vuln_analysis -type d -name __pycache__ -exec rm -rf {} + diff --git a/src/vuln_analysis/configs/config-exploitability-analyzer.yml b/src/vuln_analysis/configs/config-exploitability-analyzer.yml index 63fa85f6c..911497129 100644 --- a/src/vuln_analysis/configs/config-exploitability-analyzer.yml +++ b/src/vuln_analysis/configs/config-exploitability-analyzer.yml @@ -31,10 +31,61 @@ functions: 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] + 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 diff --git a/src/vuln_analysis/configs/config.yml b/src/vuln_analysis/configs/config.yml index 0c85b46d6..efa94be84 100644 --- a/src/vuln_analysis/configs/config.yml +++ b/src/vuln_analysis/configs/config.yml @@ -72,6 +72,39 @@ functions: 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 @@ -104,7 +137,25 @@ functions: 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] + 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 diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index 13ad5f014..97f15f72a 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -55,6 +55,7 @@ from vuln_analysis.tools import osv_tools # noqa: F401 from vuln_analysis.tools import filesystem_tools # noqa: F401 from vuln_analysis.tools import binary_tools # noqa: F401 +from vuln_analysis.tools import package_tools # noqa: F401 logger = logging.getLogger(__name__) diff --git a/src/vuln_analysis/tools/binary_tools.py b/src/vuln_analysis/tools/binary_tools.py index ef59938aa..354441453 100644 --- a/src/vuln_analysis/tools/binary_tools.py +++ b/src/vuln_analysis/tools/binary_tools.py @@ -234,3 +234,51 @@ async def check_distro_patches(package_name: str, cve_id: str) -> str: 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/package_tools.py b/src/vuln_analysis/tools/package_tools.py new file mode 100644 index 000000000..395836987 --- /dev/null +++ b/src/vuln_analysis/tools/package_tools.py @@ -0,0 +1,731 @@ +"""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") + + +# --- 6. find_go_binaries --- + + +class FindGoBinariesConfig(FunctionBaseConfig, name="find_go_binaries"): + binary_analysis_timeout: int = Field(default=30) + + +@register_function(config_type=FindGoBinariesConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def find_go_binaries_tool(config: FindGoBinariesConfig, builder: Builder): + timeout = config.binary_analysis_timeout + + 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 + MAX_FILES = 5000 + try: + for candidate in resolved.rglob("*"): + if files_checked >= MAX_FILES: + 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}" if files_checked >= MAX_FILES 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[:10]: + 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("(go version -m failed)") + except (FileNotFoundError, subprocess.TimeoutExpired): + lines.append("(go command not available or timed out)") + else: + lines.append("('go' command not found)") + if len(go_binaries) > 10: + lines.append(f"\n[... {len(go_binaries) - 10} 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") From c9106649c87668f75ae78c445ea2da716d1882be Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Thu, 12 Mar 2026 11:53:05 -0400 Subject: [PATCH 07/48] Migrate v3 benchmarks to NAT eval --- .tmp/.gitkeep | 0 .../CVE-2023-36632.md | 394 ------------------ pyproject.toml | 2 +- src/vuln_analysis/configs/config-eval.yml | 234 +++++++++++ .../eval_datasets/morpheus_benchmark_2506.csv | 30 ++ .../eval_datasets/pb_eval_exploitable_new.csv | 9 + .../data/eval_datasets/pb_eval_highcrit.csv | 30 ++ .../eval_datasets/pb_eval_highcrit_15.csv | 16 + .../data/eval_datasets/pb_eval_holoscan.csv | 2 + .../data/eval_datasets/pb_eval_test.csv | 15 + .../data/eval_datasets/redhat_benchmark.csv | 51 +++ src/vuln_analysis/eval/__init__.py | 14 + src/vuln_analysis/eval/evaluators/__init__.py | 14 + src/vuln_analysis/eval/evaluators/accuracy.py | 297 +++++++++++++ src/vuln_analysis/eval/parse_eval_input.py | 111 +++++ src/vuln_analysis/eval/sample_pb_eval.py | 327 +++++++++++++++ src/vuln_analysis/register.py | 3 +- 17 files changed, 1153 insertions(+), 396 deletions(-) delete mode 100644 .tmp/.gitkeep delete mode 100644 .tmp/vulnerability_markdown_reports/vulnerability_reports_morpheus_23_11-runtime/CVE-2023-36632.md create mode 100644 src/vuln_analysis/configs/config-eval.yml create mode 100644 src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_exploitable_new.csv create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_highcrit.csv create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_highcrit_15.csv create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_holoscan.csv create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_test.csv create mode 100644 src/vuln_analysis/data/eval_datasets/redhat_benchmark.csv create mode 100644 src/vuln_analysis/eval/__init__.py create mode 100644 src/vuln_analysis/eval/evaluators/__init__.py create mode 100644 src/vuln_analysis/eval/evaluators/accuracy.py create mode 100644 src/vuln_analysis/eval/parse_eval_input.py create mode 100644 src/vuln_analysis/eval/sample_pb_eval.py diff --git a/.tmp/.gitkeep b/.tmp/.gitkeep deleted file mode 100644 index e69de29bb..000000000 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/pyproject.toml b/pyproject.toml index 5346e99a5..4a21fd691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "langchain-core", "langgraph>=1.0.5,<2.0.0", "filelock", - "nvidia-nat[async_endpoints,langchain,phoenix,profiling]==1.5.0rc5", + "nvidia-nat[async_endpoints,langchain,phoenix,profiling,eval]==1.5.0rc5", "openinference-instrumentation-langchain", "packaging>=24.0", "pydpkg==1.9.4", diff --git a/src/vuln_analysis/configs/config-eval.yml b/src/vuln_analysis/configs/config-eval.yml new file mode 100644 index 000000000..1e7d57aa6 --- /dev/null +++ b/src/vuln_analysis/configs/config-eval.yml @@ -0,0 +1,234 @@ +# 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. + +# 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 + +llms: + cve_researcher_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.15 + max_tokens: 16384 + + container_phase1_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.1 + max_tokens: 4096 + + container_phase2_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.15 + max_tokens: 16384 + + exploitability_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.1 + max_tokens: 16384 + + vex_llm: + _type: nim + model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} + temperature: 0.0 + max_tokens: 4096 + +functions: + web_search: + _type: tavily_internet_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 + context_window_tokens: 262144 + wrap_up_fraction: 0.8 + loop_detection_threshold: 2 + 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_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: outputs + +eval: + general: + output_dir: ./.tmp/evaluators + max_concurrency: 2 + dataset: + _type: custom + file_path: data/eval_datasets/pb_eval_highcrit_15.csv + function: vuln_analysis.eval.parse_eval_input.parse_input + + evaluators: + accuracy_exclude: + _type: accuracy + uncertain_mode: exclude + accuracy_wrong: + _type: accuracy + uncertain_mode: wrong + accuracy_vulnerable: + _type: accuracy + uncertain_mode: vulnerable 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..df0db57b5 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv @@ -0,0 +1,30 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +GHSA-53q9-r3pm-6pq6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +CVE-2025-4517,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2025-13836,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-wf7f-8fxf-xfxc,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-pgqp-8h46-6x4j,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-gm62-xv2j-4w53,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-7f5h-v6xp-fcq8,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-6mq8-rvhq-8wgg,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-63vm-454h-vhhq,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-5rjg-fvgr-3xxf,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +GHSA-58pv-8j8x-9vj2,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-38jv-5279-wg99,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-2xpw-w6gg-jr37,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-2qfp-q593-8484,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_present +GHSA-ph84-rcj2-fxxm,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2024-9287,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2025-4138,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2025-4330,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2025-4435,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +CVE-2025-8194,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +CVE-2025-9230,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +CVE-2025-48384,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable +CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable +GHSA-58pv-8j8x-9vj2,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable +GHSA-5rjg-fvgr-3xxf,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable +GHSA-rqc4-2hc7-8c8v,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_exploitable_new.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_exploitable_new.csv new file mode 100644 index 000000000..9c39e9dbd --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_exploitable_new.csv @@ -0,0 +1,9 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2025-30204,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/riva-translate-4b-instruct-v1.1:latest,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/image_config.json,vulnerable +CVE-2024-11041,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,vulnerable diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_highcrit.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_highcrit.csv new file mode 100644 index 000000000..570182cc7 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_highcrit.csv @@ -0,0 +1,30 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2023-48022,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +GHSA-5vqr-wprc-cpp7,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2025-62372,nvcr.io/nvidia/tritonserver-pb25h2:25.08.03-vllm-python-py3-stig-fips-x86,/raid/hsinc/nspect_pb_container_data/tritonserver-pb25h2_25.08.03-vllm-python-py3-stig-fips-x86/filesystem,/raid/hsinc/nspect_pb_container_data/tritonserver-pb25h2_25.08.03-vllm-python-py3-stig-fips-x86/image_config.json,code_not_reachable +CVE-2024-11392,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2025-66418,nvcr.io/nim/nvidia/riva-translate-4b-instruct-v1.1:latest,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/image_config.json,vulnerable +CVE-2025-30204,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,vulnerable +CVE-2025-57809,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-62164,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2024-48921,nvcr.io/nvidia/nemo-microservices/dgxc-admission-controller:1.801.0,/raid/hsinc/nspect_pb_container_data/dgxc-admission-controller_1.801.0/filesystem,/raid/hsinc/nspect_pb_container_data/dgxc-admission-controller_1.801.0/image_config.json,vulnerable +CVE-2025-47287,nvcr.io/nvidia/rapids-pb24h2:24.06.06-runtime,/raid/hsinc/nspect_pb_container_data/rapids-pb24h2_24.06.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/rapids-pb24h2_24.06.06-runtime/image_config.json,vulnerable +CVE-2024-11041,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2025-9900,nvcr.io/nvidia/holoscan-ltsb2:23.10.08-lws2.3.1-dgpu,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/filesystem,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/image_config.json,vulnerable +CVE-2025-57809,nvcr.io/nim/meta/llama-3.1-70b-instruct:1.13.1,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2024-9053,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2025-29783,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_present +CVE-2024-2961,nvcr.io/nvidia/gpu-operator:v24.3.0,/raid/hsinc/nspect_pb_container_data/gpu-operator_v24.3.0/filesystem,/raid/hsinc/nspect_pb_container_data/gpu-operator_v24.3.0/image_config.json,requires_configuration +CVE-2024-56171,nvcr.io/nvidia/vgpu/vgpu-guest-driver-6:570.124.06-rhel8.8,/raid/hsinc/nspect_pb_container_data/vgpu-guest-driver-6_570.124.06-rhel8.8/filesystem,/raid/hsinc/nspect_pb_container_data/vgpu-guest-driver-6_570.124.06-rhel8.8/image_config.json,code_not_reachable +CVE-2024-5187,nvcr.io/nim/defog/llama-3-sqlcoder-8b:latest,/raid/hsinc/nspect_pb_container_data/llama-3-sqlcoder-8b_latest/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3-sqlcoder-8b_latest/image_config.json,code_not_reachable +CVE-2024-6387,nvcr.io/nim/meta/llama-2-70b-chat:1,/raid/hsinc/nspect_pb_container_data/llama-2-70b-chat_1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-2-70b-chat_1/image_config.json,code_not_reachable +CVE-2025-30165,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,code_not_reachable +CVE-2024-12376,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2025-52881,nvcr.io/nvidia/mellanox/plugins:network-operator-v25.10.0,/raid/hsinc/nspect_pb_container_data/plugins_network-operator-v25.10.0/filesystem,/raid/hsinc/nspect_pb_container_data/plugins_network-operator-v25.10.0/image_config.json,code_not_reachable +CVE-2024-37059,nvcr.io/nvidia/nemo-microservices/customizer:25.10,/raid/hsinc/nspect_pb_container_data/customizer_25.10/filesystem,/raid/hsinc/nspect_pb_container_data/customizer_25.10/image_config.json,code_not_reachable +CVE-2025-22869,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_reachable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.2-3b-instruct:1.8.3,/raid/hsinc/nspect_pb_container_data/llama-3.2-3b-instruct_1.8.3/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.2-3b-instruct_1.8.3/image_config.json,runtime_protected +CVE-2024-9052,nvcr.io/nim/nvidia/llama-3.3-nemotron-super-49b-v1:1.8.3,/raid/hsinc/nspect_pb_container_data/llama-3.3-nemotron-super-49b-v1_1.8.3/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.3-nemotron-super-49b-v1_1.8.3/image_config.json,runtime_protected +CVE-2024-9053,nvcr.io/nvidia/tritonserver-pb24h2:24.08.07-vllm-python-py3,/raid/hsinc/nspect_pb_container_data/tritonserver-pb24h2_24.08.07-vllm-python-py3/filesystem,/raid/hsinc/nspect_pb_container_data/tritonserver-pb24h2_24.08.07-vllm-python-py3/image_config.json,runtime_protected diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_highcrit_15.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_highcrit_15.csv new file mode 100644 index 000000000..db693d2a4 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_highcrit_15.csv @@ -0,0 +1,16 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2024-6387,nvcr.io/nim/meta/llama-2-70b-chat:1,/raid/hsinc/nspect_pb_container_data/llama-2-70b-chat_1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-2-70b-chat_1/image_config.json,code_not_reachable +CVE-2024-11392,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2023-48022,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +CVE-2025-52881,nvcr.io/nvidia/mellanox/plugins:network-operator-v25.10.0,/raid/hsinc/nspect_pb_container_data/plugins_network-operator-v25.10.0/filesystem,/raid/hsinc/nspect_pb_container_data/plugins_network-operator-v25.10.0/image_config.json,code_not_reachable +CVE-2024-48921,nvcr.io/nvidia/nemo-microservices/dgxc-admission-controller:1.801.0,/raid/hsinc/nspect_pb_container_data/dgxc-admission-controller_1.801.0/filesystem,/raid/hsinc/nspect_pb_container_data/dgxc-admission-controller_1.801.0/image_config.json,vulnerable +CVE-2025-62164,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-22869,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_reachable +CVE-2025-66418,nvcr.io/nim/nvidia/riva-translate-4b-instruct-v1.1:latest,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/nvidia/llama-3.3-nemotron-super-49b-v1:1.8.3,/raid/hsinc/nspect_pb_container_data/llama-3.3-nemotron-super-49b-v1_1.8.3/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.3-nemotron-super-49b-v1_1.8.3/image_config.json,runtime_protected +CVE-2024-2961,nvcr.io/nvidia/gpu-operator:v24.3.0,/raid/hsinc/nspect_pb_container_data/gpu-operator_v24.3.0/filesystem,/raid/hsinc/nspect_pb_container_data/gpu-operator_v24.3.0/image_config.json,requires_configuration +CVE-2025-62372,nvcr.io/nvidia/tritonserver-pb25h2:25.08.03-vllm-python-py3-stig-fips-x86,/raid/hsinc/nspect_pb_container_data/tritonserver-pb25h2_25.08.03-vllm-python-py3-stig-fips-x86/filesystem,/raid/hsinc/nspect_pb_container_data/tritonserver-pb25h2_25.08.03-vllm-python-py3-stig-fips-x86/image_config.json,code_not_reachable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,vulnerable +GHSA-5vqr-wprc-cpp7,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.2-3b-instruct:1.8.3,/raid/hsinc/nspect_pb_container_data/llama-3.2-3b-instruct_1.8.3/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.2-3b-instruct_1.8.3/image_config.json,runtime_protected +CVE-2025-29783,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_present diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_holoscan.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_holoscan.csv new file mode 100644 index 000000000..c21241a88 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_holoscan.csv @@ -0,0 +1,2 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2025-9900,nvcr.io/nvidia/holoscan-ltsb2:23.10.08-lws2.3.1-dgpu,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/filesystem,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/image_config.json,vulnerable diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_test.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_test.csv new file mode 100644 index 000000000..d28422fc9 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_test.csv @@ -0,0 +1,15 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2025-30204,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,vulnerable +CVE-2025-57809,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-47287,nvcr.io/nvidia/rapids-pb24h2:24.06.06-runtime,/raid/hsinc/nspect_pb_container_data/rapids-pb24h2_24.06.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/rapids-pb24h2_24.06.06-runtime/image_config.json,vulnerable +CVE-2024-11041,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2025-9900,nvcr.io/nvidia/holoscan-ltsb2:23.10.08-lws2.3.1-dgpu,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/filesystem,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/image_config.json,vulnerable +CVE-2025-57809,nvcr.io/nim/meta/llama-3.1-70b-instruct:1.13.1,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2024-9053,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2024-56171,nvcr.io/nvidia/vgpu/vgpu-guest-driver-6:570.124.06-rhel8.8,/raid/hsinc/nspect_pb_container_data/vgpu-guest-driver-6_570.124.06-rhel8.8/filesystem,/raid/hsinc/nspect_pb_container_data/vgpu-guest-driver-6_570.124.06-rhel8.8/image_config.json,code_not_reachable +CVE-2024-5187,nvcr.io/nim/defog/llama-3-sqlcoder-8b:latest,/raid/hsinc/nspect_pb_container_data/llama-3-sqlcoder-8b_latest/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3-sqlcoder-8b_latest/image_config.json,code_not_reachable +CVE-2025-30165,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,code_not_reachable +CVE-2024-12376,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2024-37059,nvcr.io/nvidia/nemo-microservices/customizer:25.10,/raid/hsinc/nspect_pb_container_data/customizer_25.10/filesystem,/raid/hsinc/nspect_pb_container_data/customizer_25.10/image_config.json,code_not_reachable +CVE-2024-9053,nvcr.io/nvidia/tritonserver-pb24h2:24.08.07-vllm-python-py3,/raid/hsinc/nspect_pb_container_data/tritonserver-pb24h2_24.08.07-vllm-python-py3/filesystem,/raid/hsinc/nspect_pb_container_data/tritonserver-pb24h2_24.08.07-vllm-python-py3/image_config.json,runtime_protected diff --git a/src/vuln_analysis/data/eval_datasets/redhat_benchmark.csv b/src/vuln_analysis/data/eval_datasets/redhat_benchmark.csv new file mode 100644 index 000000000..e54fb0901 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/redhat_benchmark.csv @@ -0,0 +1,51 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2023-49569,registry.redhat.io/rhacm2/acm-governance-policy-addon-controller-rhel8:v2.8.4-3,/raid/efajardo/redhat_containers/acm-governance-policy-addon-controller-rhel8_v2.8.4-3/filesystem,/raid/efajardo/redhat_containers/acm-governance-policy-addon-controller-rhel8_v2.8.4-3/image_config.json,code_not_reachable +CVE-2024-45339,registry.redhat.io/openshift4/cloud-event-proxy-rhel8:v4.12.0-202506062300.p0.gca2ebf6.assembly.stream.el8,/raid/efajardo/redhat_containers/cloud-event-proxy-rhel8_v4.12.0-202506062300.p0.gca2ebf6.assembly.stream.el8/filesystem,/raid/efajardo/redhat_containers/cloud-event-proxy-rhel8_v4.12.0-202506062300.p0.gca2ebf6.assembly.stream.el8/image_config.json,code_not_reachable +CVE-2024-45339,registry.redhat.io/openshift4/cnf-tests-rhel8:v4.12-1751364430,/raid/efajardo/redhat_containers/cnf-tests-rhel8_v4.12-1751364430/filesystem,/raid/efajardo/redhat_containers/cnf-tests-rhel8_v4.12-1751364430/image_config.json,code_not_reachable +CVE-2022-3064,registry.redhat.io/openshift4/egress-router-cni-rhel8@sha256:024243c121ec4fa25b48c2db3b254763d33c23c8d03b6f75855a525993a953d7,/raid/efajardo/redhat_containers/egress-router-cni-rhel8@sha256_024243c121ec4fa25b48c2db3b254763d33c23c8d03b6f75855a525993a953d7/filesystem,/raid/efajardo/redhat_containers/egress-router-cni-rhel8@sha256_024243c121ec4fa25b48c2db3b254763d33c23c8d03b6f75855a525993a953d7/image_config.json,code_not_reachable +CVE-2021-3121,registry.redhat.io/openshift4/kubernetes-nmstate-rhel8-operator@sha256:a5ecbc406c33e464b5b0920513678be0f4aeab3757322ee3e384753098ab5c55,/raid/efajardo/redhat_containers/kubernetes-nmstate-rhel8-operator@sha256_a5ecbc406c33e464b5b0920513678be0f4aeab3757322ee3e384753098ab5c55/filesystem,/raid/efajardo/redhat_containers/kubernetes-nmstate-rhel8-operator@sha256_a5ecbc406c33e464b5b0920513678be0f4aeab3757322ee3e384753098ab5c55/image_config.json,code_not_reachable +CVE-2024-45339,registry.redhat.io/openshift4/metallb-rhel9-operator:v4.19.0-202507221938.p0.gfa92126.assembly.stream.el9,/raid/efajardo/redhat_containers/metallb-rhel9-operator_v4.19.0-202507221938.p0.gfa92126.assembly.stream.el9/filesystem,/raid/efajardo/redhat_containers/metallb-rhel9-operator_v4.19.0-202507221938.p0.gfa92126.assembly.stream.el9/image_config.json,code_not_reachable +CVE-2022-41717,registry.redhat.io/mta/mta-admin-addon-rhel8:6.0.0-10,/raid/efajardo/redhat_containers/mta-admin-addon-rhel8_6.0.0-10/filesystem,/raid/efajardo/redhat_containers/mta-admin-addon-rhel8_6.0.0-10/image_config.json,code_not_reachable +CVE-2022-41717,registry.redhat.io/oadp/oadp-registry-rhel8:1.0.6-3,/raid/efajardo/redhat_containers/oadp-registry-rhel8_1.0.6-3/filesystem,/raid/efajardo/redhat_containers/oadp-registry-rhel8_1.0.6-3/image_config.json,code_not_reachable +CVE-2024-34156,registry.redhat.io/oadp/oadp-velero-rhel9:1.3.0-50,/raid/efajardo/redhat_containers/oadp-velero-rhel9_1.3.0-50/filesystem,/raid/efajardo/redhat_containers/oadp-velero-rhel9_1.3.0-50/image_config.json,code_not_reachable +CVE-2022-23524,registry.redhat.io/openshift4/oc-mirror-plugin-rhel8@sha256:0dcece2c8608f83c3451dbdee310d274b0235db0eeaf10a2f90c698b0ae56ab7,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel8@sha256_0dcece2c8608f83c3451dbdee310d274b0235db0eeaf10a2f90c698b0ae56ab7/filesystem,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel8@sha256_0dcece2c8608f83c3451dbdee310d274b0235db0eeaf10a2f90c698b0ae56ab7/image_config.json,code_not_reachable +CVE-2023-25165,registry.redhat.io/openshift4/oc-mirror-plugin-rhel8@sha256:6b30150437b3eae0125dae15ce9767c93a0edc643ea653375203c9d0e448e848,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel8@sha256_6b30150437b3eae0125dae15ce9767c93a0edc643ea653375203c9d0e448e848/filesystem,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel8@sha256_6b30150437b3eae0125dae15ce9767c93a0edc643ea653375203c9d0e448e848/image_config.json,code_not_reachable +CVE-2023-25173,registry.redhat.io/openshift4/oc-mirror-plugin-rhel8@sha256:6b30150437b3eae0125dae15ce9767c93a0edc643ea653375203c9d0e448e848,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel8@sha256_6b30150437b3eae0125dae15ce9767c93a0edc643ea653375203c9d0e448e848/filesystem,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel8@sha256_6b30150437b3eae0125dae15ce9767c93a0edc643ea653375203c9d0e448e848/image_config.json,code_not_reachable +CVE-2025-32387,registry.redhat.io/openshift4/oc-mirror-plugin-rhel9@sha256:1cc713dbb4c830039d12934c58a62f79bbbff319225735fce2b3cb6cb18a7ad9,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel9@sha256_1cc713dbb4c830039d12934c58a62f79bbbff319225735fce2b3cb6cb18a7ad9/filesystem,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel9@sha256_1cc713dbb4c830039d12934c58a62f79bbbff319225735fce2b3cb6cb18a7ad9/image_config.json,code_not_reachable +CVE-2024-28180,registry.redhat.io/openshift4/oc-mirror-plugin-rhel9@sha256:2dda2094ba7c5569155405cf165a90d9d4448b0329c17fd47e768cfa1cb6bffb,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel9@sha256_2dda2094ba7c5569155405cf165a90d9d4448b0329c17fd47e768cfa1cb6bffb/filesystem,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel9@sha256_2dda2094ba7c5569155405cf165a90d9d4448b0329c17fd47e768cfa1cb6bffb/image_config.json,code_not_reachable +CVE-2025-21613,registry.redhat.io/openshift4/oc-mirror-plugin-rhel9@sha256:fc8df4b0ec38f21ef8e7b76c50417c8bff0c6d8ae39b4bbca77542cf0da25dc0,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel9@sha256_fc8df4b0ec38f21ef8e7b76c50417c8bff0c6d8ae39b4bbca77542cf0da25dc0/filesystem,/raid/efajardo/redhat_containers/oc-mirror-plugin-rhel9@sha256_fc8df4b0ec38f21ef8e7b76c50417c8bff0c6d8ae39b4bbca77542cf0da25dc0/image_config.json,code_not_reachable +CVE-2023-24538,registry.redhat.io/openshift4/openshift-route-controller-manager-rhel8:v4.13.0-202304190216.p0.gd7a8e22.assembly.stream,/raid/efajardo/redhat_containers/openshift-route-controller-manager-rhel8_v4.13.0-202304190216.p0.gd7a8e22.assembly.stream/filesystem,/raid/efajardo/redhat_containers/openshift-route-controller-manager-rhel8_v4.13.0-202304190216.p0.gd7a8e22.assembly.stream/image_config.json,code_not_reachable +CVE-2024-36620,registry.redhat.io/openshift4/ose-agent-installer-api-server-rhel9@sha256:c3c2b347c2f6ac5b5914e9518fe7ce417711211acf815e7e3026a8992be44f8a,/raid/efajardo/redhat_containers/ose-agent-installer-api-server-rhel9@sha256_c3c2b347c2f6ac5b5914e9518fe7ce417711211acf815e7e3026a8992be44f8a/filesystem,/raid/efajardo/redhat_containers/ose-agent-installer-api-server-rhel9@sha256_c3c2b347c2f6ac5b5914e9518fe7ce417711211acf815e7e3026a8992be44f8a/image_config.json,code_not_reachable +CVE-2024-51744,registry.redhat.io/openshift4/ose-agent-installer-orchestrator-rhel9@sha256:1a5abb6cd5e80caa55de0cf687f97c72321bb0d8d2debb4cf8356a629acc0add,/raid/efajardo/redhat_containers/ose-agent-installer-orchestrator-rhel9@sha256_1a5abb6cd5e80caa55de0cf687f97c72321bb0d8d2debb4cf8356a629acc0add/filesystem,/raid/efajardo/redhat_containers/ose-agent-installer-orchestrator-rhel9@sha256_1a5abb6cd5e80caa55de0cf687f97c72321bb0d8d2debb4cf8356a629acc0add/image_config.json,vulnerable +CVE-2024-56201,registry.redhat.io/openshift4/ose-ansible-rhel9-operator@sha256:1fb1ce58f088b64b1717dec9c131e64a42460417e31c26861ec17c8ba56abee0,/raid/efajardo/redhat_containers/ose-ansible-rhel9-operator@sha256_1fb1ce58f088b64b1717dec9c131e64a42460417e31c26861ec17c8ba56abee0/filesystem,/raid/efajardo/redhat_containers/ose-ansible-rhel9-operator@sha256_1fb1ce58f088b64b1717dec9c131e64a42460417e31c26861ec17c8ba56abee0/image_config.json,code_not_reachable +CVE-2022-2385,registry.redhat.io/openshift4/ose-aws-cluster-api-controllers-rhel8@sha256:389655fc58cbb7f8c6bc42fdd0a2c866e1548150c5d27bdd6dcbac8cd356a3ae,/raid/efajardo/redhat_containers/ose-aws-cluster-api-controllers-rhel8@sha256_389655fc58cbb7f8c6bc42fdd0a2c866e1548150c5d27bdd6dcbac8cd356a3ae/filesystem,/raid/efajardo/redhat_containers/ose-aws-cluster-api-controllers-rhel8@sha256_389655fc58cbb7f8c6bc42fdd0a2c866e1548150c5d27bdd6dcbac8cd356a3ae/image_config.json,code_not_reachable +CVE-2022-46174,registry.redhat.io/openshift4/ose-aws-efs-csi-driver-container-rhel8@sha256:bb07426f0ce5ac593739b94fe521831c1b61fed883faaaeb115fb40ad9ca3378,/raid/efajardo/redhat_containers/ose-aws-efs-csi-driver-container-rhel8@sha256_bb07426f0ce5ac593739b94fe521831c1b61fed883faaaeb115fb40ad9ca3378/filesystem,/raid/efajardo/redhat_containers/ose-aws-efs-csi-driver-container-rhel8@sha256_bb07426f0ce5ac593739b94fe521831c1b61fed883faaaeb115fb40ad9ca3378/image_config.json,code_not_reachable +CVE-2022-30321,registry.redhat.io/openshift4/ose-baremetal-installer-rhel8@sha256:5ea1eea9cc4ae2f0ada5780bdfa75733595e2c7f17d53589825f3f5864e7dada,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_5ea1eea9cc4ae2f0ada5780bdfa75733595e2c7f17d53589825f3f5864e7dada/filesystem,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_5ea1eea9cc4ae2f0ada5780bdfa75733595e2c7f17d53589825f3f5864e7dada/image_config.json,code_not_reachable +CVE-2022-29810,registry.redhat.io/openshift4/ose-baremetal-installer-rhel8@sha256:ac2a83fcfdc2576add63fe053921a4c1e93e1a35618a16dbde8fd98a5a72f0f3,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_ac2a83fcfdc2576add63fe053921a4c1e93e1a35618a16dbde8fd98a5a72f0f3/filesystem,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_ac2a83fcfdc2576add63fe053921a4c1e93e1a35618a16dbde8fd98a5a72f0f3/image_config.json,code_not_reachable +CVE-2022-26945,registry.redhat.io/openshift4/ose-baremetal-installer-rhel8@sha256:cf6c12f26a4bc83448d7631a0c952fc3e05c38402c784624b1c486e05e9e72ec,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_cf6c12f26a4bc83448d7631a0c952fc3e05c38402c784624b1c486e05e9e72ec/filesystem,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_cf6c12f26a4bc83448d7631a0c952fc3e05c38402c784624b1c486e05e9e72ec/image_config.json,code_not_reachable +CVE-2022-30323,registry.redhat.io/openshift4/ose-baremetal-installer-rhel8@sha256:f088aa1d58ecbb83b684af7105107ee1379f70f79244958211c3a52dbf29b833,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_f088aa1d58ecbb83b684af7105107ee1379f70f79244958211c3a52dbf29b833/filesystem,/raid/efajardo/redhat_containers/ose-baremetal-installer-rhel8@sha256_f088aa1d58ecbb83b684af7105107ee1379f70f79244958211c3a52dbf29b833/image_config.json,code_not_reachable +CVE-2022-24769,registry.redhat.io/openshift4/ose-cli-artifacts@sha256:c16a726b257db961d025f3827e03a0490c616593e10156a164bfb5f4154e72b7,/raid/efajardo/redhat_containers/ose-cli-artifacts@sha256_c16a726b257db961d025f3827e03a0490c616593e10156a164bfb5f4154e72b7/filesystem,/raid/efajardo/redhat_containers/ose-cli-artifacts@sha256_c16a726b257db961d025f3827e03a0490c616593e10156a164bfb5f4154e72b7/image_config.json,code_not_reachable +CVE-2024-23653,registry.redhat.io/openshift4/ose-cli@sha256:d9206fd23cb0aa3a479aff3d8eaa305ad3dccf32527f94229a66d9e8c4d8260c,/raid/efajardo/redhat_containers/ose-cli@sha256_d9206fd23cb0aa3a479aff3d8eaa305ad3dccf32527f94229a66d9e8c4d8260c/filesystem,/raid/efajardo/redhat_containers/ose-cli@sha256_d9206fd23cb0aa3a479aff3d8eaa305ad3dccf32527f94229a66d9e8c4d8260c/image_config.json,code_not_reachable +CVE-2025-1767,registry.redhat.io/openshift4/ose-cluster-capacity-rhel9@sha256:d9b179018c799c122545e47aaa0e0c86550febc68303f5da99824096dd9b5700,/raid/efajardo/redhat_containers/ose-cluster-capacity-rhel9@sha256_d9b179018c799c122545e47aaa0e0c86550febc68303f5da99824096dd9b5700/filesystem,/raid/efajardo/redhat_containers/ose-cluster-capacity-rhel9@sha256_d9b179018c799c122545e47aaa0e0c86550febc68303f5da99824096dd9b5700/image_config.json,code_not_reachable +CVE-2023-47108,registry.redhat.io/openshift4/ose-cluster-image-registry-operator@sha256:4772ea55683df352a024c76d2411f750342238778955b900ea4670e0c810ee2f,/raid/efajardo/redhat_containers/ose-cluster-image-registry-operator@sha256_4772ea55683df352a024c76d2411f750342238778955b900ea4670e0c810ee2f/filesystem,/raid/efajardo/redhat_containers/ose-cluster-image-registry-operator@sha256_4772ea55683df352a024c76d2411f750342238778955b900ea4670e0c810ee2f/image_config.json,code_not_reachable +CVE-2022-3172,registry.redhat.io/openshift4/ose-cluster-kube-apiserver-operator@sha256:df64b3607e0f09866a1473f8dc0ed115b93163db31ce7a507dc5325be77f9634,/raid/efajardo/redhat_containers/ose-cluster-kube-apiserver-operator@sha256_df64b3607e0f09866a1473f8dc0ed115b93163db31ce7a507dc5325be77f9634/filesystem,/raid/efajardo/redhat_containers/ose-cluster-kube-apiserver-operator@sha256_df64b3607e0f09866a1473f8dc0ed115b93163db31ce7a507dc5325be77f9634/image_config.json,code_not_reachable +CVE-2024-26147,registry.redhat.io/openshift4/ose-console-rhel9@sha256:a56e37e15948de51960049369f9b96b1816742dc82b601f72fee105cd05845ef,/raid/efajardo/redhat_containers/ose-console-rhel9@sha256_a56e37e15948de51960049369f9b96b1816742dc82b601f72fee105cd05845ef/filesystem,/raid/efajardo/redhat_containers/ose-console-rhel9@sha256_a56e37e15948de51960049369f9b96b1816742dc82b601f72fee105cd05845ef/image_config.json,code_not_reachable +CVE-2022-23526,registry.redhat.io/openshift4/ose-console@sha256:a79e8ff787cb47de81a9bacb7cd3f1de6eb69d8258e533dc731b4d7a289dc978,/raid/efajardo/redhat_containers/ose-console@sha256_a79e8ff787cb47de81a9bacb7cd3f1de6eb69d8258e533dc731b4d7a289dc978/filesystem,/raid/efajardo/redhat_containers/ose-console@sha256_a79e8ff787cb47de81a9bacb7cd3f1de6eb69d8258e533dc731b4d7a289dc978/image_config.json,code_not_reachable +CVE-2022-36109,registry.redhat.io/openshift4/ose-console@sha256:a79e8ff787cb47de81a9bacb7cd3f1de6eb69d8258e533dc731b4d7a289dc978,/raid/efajardo/redhat_containers/ose-console@sha256_a79e8ff787cb47de81a9bacb7cd3f1de6eb69d8258e533dc731b4d7a289dc978/filesystem,/raid/efajardo/redhat_containers/ose-console@sha256_a79e8ff787cb47de81a9bacb7cd3f1de6eb69d8258e533dc731b4d7a289dc978/image_config.json,code_not_reachable +CVE-2024-53259,registry.redhat.io/openshift4/ose-coredns-rhel9@sha256:28ec0469bc4be6b39114aeba125e24ecf66057cc7c3b4dd581d52aa66ef6647b,/raid/efajardo/redhat_containers/ose-coredns-rhel9@sha256_28ec0469bc4be6b39114aeba125e24ecf66057cc7c3b4dd581d52aa66ef6647b/filesystem,/raid/efajardo/redhat_containers/ose-coredns-rhel9@sha256_28ec0469bc4be6b39114aeba125e24ecf66057cc7c3b4dd581d52aa66ef6647b/image_config.json,code_not_reachable +CVE-2024-24786,registry.redhat.io/openshift4/ose-coredns-rhel9@sha256:81502dac120d49300f0f34fb10ac69a5c0ab0cffc05fdbc49a2241ec1f6ac206,/raid/efajardo/redhat_containers/ose-coredns-rhel9@sha256_81502dac120d49300f0f34fb10ac69a5c0ab0cffc05fdbc49a2241ec1f6ac206/filesystem,/raid/efajardo/redhat_containers/ose-coredns-rhel9@sha256_81502dac120d49300f0f34fb10ac69a5c0ab0cffc05fdbc49a2241ec1f6ac206/image_config.json,code_not_reachable +CVE-2023-49295,registry.redhat.io/openshift4/ose-coredns-rhel9@sha256:b85a06f5710986a3ac5c3592268e0e27de9640af55e8ba2490dec0de82a6f624,/raid/efajardo/redhat_containers/ose-coredns-rhel9@sha256_b85a06f5710986a3ac5c3592268e0e27de9640af55e8ba2490dec0de82a6f624/filesystem,/raid/efajardo/redhat_containers/ose-coredns-rhel9@sha256_b85a06f5710986a3ac5c3592268e0e27de9640af55e8ba2490dec0de82a6f624/image_config.json,code_not_reachable +CVE-2021-4235,registry.redhat.io/openshift4/ose-coredns@sha256:2088dceb5ccc67423abb8ab4c90d7fbf3b36c8060a685c6ca99b7dc8c5a708e4,/raid/efajardo/redhat_containers/ose-coredns@sha256_2088dceb5ccc67423abb8ab4c90d7fbf3b36c8060a685c6ca99b7dc8c5a708e4/filesystem,/raid/efajardo/redhat_containers/ose-coredns@sha256_2088dceb5ccc67423abb8ab4c90d7fbf3b36c8060a685c6ca99b7dc8c5a708e4/image_config.json,code_not_reachable +CVE-2022-23525,registry.redhat.io/openshift4/ose-helm-operator@sha256:dbc5b4057a26f25e883f0b571974f1531aa8812715aa2090688a5eccd99f51a4,/raid/efajardo/redhat_containers/ose-helm-operator@sha256_dbc5b4057a26f25e883f0b571974f1531aa8812715aa2090688a5eccd99f51a4/filesystem,/raid/efajardo/redhat_containers/ose-helm-operator@sha256_dbc5b4057a26f25e883f0b571974f1531aa8812715aa2090688a5eccd99f51a4/image_config.json,code_not_reachable +CVE-2021-32690,registry.redhat.io/openshift4/ose-helm-operator@sha256:f5354940379944ab09189d6237cb6cdcbf46ff47c76d95592b038c44c8aea86f,/raid/efajardo/redhat_containers/ose-helm-operator@sha256_f5354940379944ab09189d6237cb6cdcbf46ff47c76d95592b038c44c8aea86f/filesystem,/raid/efajardo/redhat_containers/ose-helm-operator@sha256_f5354940379944ab09189d6237cb6cdcbf46ff47c76d95592b038c44c8aea86f/image_config.json,code_not_reachable +CVE-2025-32386,registry.redhat.io/openshift4/ose-helm-rhel9-operator@sha256:bd5f2914e0435b969778edac11ca455a9a7ca4af6cd863679f44bbf1edfb86d7,/raid/efajardo/redhat_containers/ose-helm-rhel9-operator@sha256_bd5f2914e0435b969778edac11ca455a9a7ca4af6cd863679f44bbf1edfb86d7/filesystem,/raid/efajardo/redhat_containers/ose-helm-rhel9-operator@sha256_bd5f2914e0435b969778edac11ca455a9a7ca4af6cd863679f44bbf1edfb86d7/image_config.json,code_not_reachable +CVE-2025-30204,registry.redhat.io/openshift4/ose-hypershift-rhel9@sha256:4b522848b679798e9a5d309089555b27d0e997398ae3efe91050916964bd6c55,/raid/efajardo/redhat_containers/ose-hypershift-rhel9@sha256_4b522848b679798e9a5d309089555b27d0e997398ae3efe91050916964bd6c55/filesystem,/raid/efajardo/redhat_containers/ose-hypershift-rhel9@sha256_4b522848b679798e9a5d309089555b27d0e997398ae3efe91050916964bd6c55/image_config.json,code_not_reachable +CVE-2024-6104,registry.redhat.io/openshift4/ose-ibm-vpc-node-label-updater-rhel9@sha256:e8902ee6ab6a84cdce5fed8191d6f6d65550dddbb834a4629ac9c81c99574640,/raid/efajardo/redhat_containers/ose-ibm-vpc-node-label-updater-rhel9@sha256_e8902ee6ab6a84cdce5fed8191d6f6d65550dddbb834a4629ac9c81c99574640/filesystem,/raid/efajardo/redhat_containers/ose-ibm-vpc-node-label-updater-rhel9@sha256_e8902ee6ab6a84cdce5fed8191d6f6d65550dddbb834a4629ac9c81c99574640/image_config.json,code_not_reachable +CVE-2022-30322,registry.redhat.io/openshift4/ose-installer-artifacts@sha256:b8591bfb6ba7e12641fd7cb4fc3e0725f30870571ddb440514c8b0e7da504eb9,/raid/efajardo/redhat_containers/ose-installer-artifacts@sha256_b8591bfb6ba7e12641fd7cb4fc3e0725f30870571ddb440514c8b0e7da504eb9/filesystem,/raid/efajardo/redhat_containers/ose-installer-artifacts@sha256_b8591bfb6ba7e12641fd7cb4fc3e0725f30870571ddb440514c8b0e7da504eb9/image_config.json,code_not_reachable +CVE-2021-4238,registry.redhat.io/openshift4/ose-kubernetes-nmstate-handler-rhel8@sha256:a6418d07403c0326809532079b1a43ce54583cc3ef5eb781829949591d27a05f,/raid/efajardo/redhat_containers/ose-kubernetes-nmstate-handler-rhel8@sha256_a6418d07403c0326809532079b1a43ce54583cc3ef5eb781829949591d27a05f/filesystem,/raid/efajardo/redhat_containers/ose-kubernetes-nmstate-handler-rhel8@sha256_a6418d07403c0326809532079b1a43ce54583cc3ef5eb781829949591d27a05f/image_config.json,code_not_reachable +CVE-2023-45142,registry.redhat.io/openshift4/ose-oauth-proxy@sha256:377ddb0f05462a2a6f2ec4a13667b43a152aa47be9ae5e5411fb9e74861594a6,/raid/efajardo/redhat_containers/ose-oauth-proxy@sha256_377ddb0f05462a2a6f2ec4a13667b43a152aa47be9ae5e5411fb9e74861594a6/filesystem,/raid/efajardo/redhat_containers/ose-oauth-proxy@sha256_377ddb0f05462a2a6f2ec4a13667b43a152aa47be9ae5e5411fb9e74861594a6/image_config.json,code_not_reachable +CVE-2021-20329,registry.redhat.io/openshift4/ose-openshift-controller-manager-rhel9@sha256:0bd01ad4b62ba7290bd2b7992bf75c6c15440b66f321f9f7983968fa12834e9e,/raid/efajardo/redhat_containers/ose-openshift-controller-manager-rhel9@sha256_0bd01ad4b62ba7290bd2b7992bf75c6c15440b66f321f9f7983968fa12834e9e/filesystem,/raid/efajardo/redhat_containers/ose-openshift-controller-manager-rhel9@sha256_0bd01ad4b62ba7290bd2b7992bf75c6c15440b66f321f9f7983968fa12834e9e/image_config.json,code_not_reachable +CVE-2023-49568,registry.redhat.io/openshift4/ose-operator-registry-rhel9@sha256:2af1a3c2e6cb263db8fc6b1df2153562bb172a0aa147e28ec22ec89ce662bf70,/raid/efajardo/redhat_containers/ose-operator-registry-rhel9@sha256_2af1a3c2e6cb263db8fc6b1df2153562bb172a0aa147e28ec22ec89ce662bf70/filesystem,/raid/efajardo/redhat_containers/ose-operator-registry-rhel9@sha256_2af1a3c2e6cb263db8fc6b1df2153562bb172a0aa147e28ec22ec89ce662bf70/image_config.json,code_not_reachable +CVE-2022-21698,registry.redhat.io/openshift4/ose-prometheus-alertmanager@sha256:b85ddb2f725bf324f6e210aaec9f3bdfe3e81ccecf3dcf58d1f073d9eb1ec6e9,/raid/efajardo/redhat_containers/ose-prometheus-alertmanager@sha256_b85ddb2f725bf324f6e210aaec9f3bdfe3e81ccecf3dcf58d1f073d9eb1ec6e9/filesystem,/raid/efajardo/redhat_containers/ose-prometheus-alertmanager@sha256_b85ddb2f725bf324f6e210aaec9f3bdfe3e81ccecf3dcf58d1f073d9eb1ec6e9/image_config.json,code_not_reachable +CVE-2025-24976,registry.redhat.io/openshift4/ose-tests-rhel9@sha256:d31e49481cd172775f4284952f9ae61aa74cc74cbf4bfca0f7a2fae129640419,/raid/efajardo/redhat_containers/ose-tests-rhel9@sha256_d31e49481cd172775f4284952f9ae61aa74cc74cbf4bfca0f7a2fae129640419/filesystem,/raid/efajardo/redhat_containers/ose-tests-rhel9@sha256_d31e49481cd172775f4284952f9ae61aa74cc74cbf4bfca0f7a2fae129640419/image_config.json,code_not_reachable +CVE-2023-49569,registry.redhat.io/openshift4/ose-tools-rhel8@sha256:e27a043c2bd94ab1e4f2e6e5ee22b1651eae534b6b08348bb33f2c7c5ea2fb8b,/raid/efajardo/redhat_containers/ose-tools-rhel8@sha256_e27a043c2bd94ab1e4f2e6e5ee22b1651eae534b6b08348bb33f2c7c5ea2fb8b/filesystem,/raid/efajardo/redhat_containers/ose-tools-rhel8@sha256_e27a043c2bd94ab1e4f2e6e5ee22b1651eae534b6b08348bb33f2c7c5ea2fb8b/image_config.json,code_not_reachable diff --git a/src/vuln_analysis/eval/__init__.py b/src/vuln_analysis/eval/__init__.py new file mode 100644 index 000000000..cf7c586a5 --- /dev/null +++ b/src/vuln_analysis/eval/__init__.py @@ -0,0 +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. diff --git a/src/vuln_analysis/eval/evaluators/__init__.py b/src/vuln_analysis/eval/evaluators/__init__.py new file mode 100644 index 000000000..cf7c586a5 --- /dev/null +++ b/src/vuln_analysis/eval/evaluators/__init__.py @@ -0,0 +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. diff --git a/src/vuln_analysis/eval/evaluators/accuracy.py b/src/vuln_analysis/eval/evaluators/accuracy.py new file mode 100644 index 000000000..472897dd0 --- /dev/null +++ b/src/vuln_analysis/eval/evaluators/accuracy.py @@ -0,0 +1,297 @@ +# 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 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 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.data_models.evaluator import EvalInput +from nat.data_models.evaluator import EvalInputItem +from nat.data_models.evaluator import EvalOutput +from nat.data_models.evaluator import EvalOutputItem +from pydantic import Field +from tqdm import tqdm + +try: + from nat.eval.utils.tqdm_position_registry import TqdmPositionRegistry +except ImportError: + TqdmPositionRegistry = None # type: ignore[assignment,misc] + + +# --------------------------------------------------------------------------- +# 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 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 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: + """Evaluator that computes label accuracy, binary classification metrics, + and confusion matrix for the CVE pipeline benchmark.""" + + def __init__(self, max_concurrency: int, uncertain_mode: str = "exclude"): + self.max_concurrency = max_concurrency + self.semaphore = asyncio.Semaphore(max_concurrency) + self.uncertain_mode = uncertain_mode + + async def evaluate(self, eval_input: EvalInput) -> EvalOutput: + items = eval_input.eval_input_items + predictions: list[dict] = [] + + tqdm_position = None + if TqdmPositionRegistry is not None: + tqdm_position = TqdmPositionRegistry.claim() + + 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() + if TqdmPositionRegistry is not None and tqdm_position is not None: + TqdmPositionRegistry.release(tqdm_position) + + 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=p["item_id"], + score=1.0 if p.get("label_match") else 0.0, + reasoning={ + "cve_id": p.get("cve_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=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", "") + cve_id_raw = item.full_dataset_entry.get("cve_id", "") + container = item.full_dataset_entry.get("image_name", "") + 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) + pred_just = output.get("vex_justification") + pred_status = output.get("vex_status") + success = output.get("success", False) + 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, + "cve_id": cve_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/parse_eval_input.py b/src/vuln_analysis/eval/parse_eval_input.py new file mode 100644 index 000000000..da767761f --- /dev/null +++ b/src/vuln_analysis/eval/parse_eval_input.py @@ -0,0 +1,111 @@ +# 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 benchmark CSVs. + +Reads a CSV with columns: + cve_id, image_name, filesystem_path, image_config_path, ground_truth_label + +and produces an EvalInput where each row becomes one EvalInputItem whose +``input_obj`` is a JSON-serialised PipelineInput dict consumable by the +cve_pipeline workflow. + +NAT config usage:: + + eval: + general: + dataset: + _type: custom + file_path: data/eval_datasets/pb_eval_highcrit_15.csv + function: vuln_analysis.eval.parse_eval_input.parse_input +""" + +from __future__ import annotations + +import csv +import json +from pathlib import Path + +from nat.data_models.evaluator import EvalInput, EvalInputItem + + +_REQUIRED_COLUMNS = frozenset({ + "cve_id", + "image_name", + "filesystem_path", + "image_config_path", + "ground_truth_label", +}) + + +def parse_input(file_path: Path, **kwargs) -> EvalInput: + """Parse a benchmark CSV into NAT ``EvalInput``. + + Each CSV row maps to one ``EvalInputItem``: + + * **id** — ``"{cve_id}__{image_name}"`` (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: + cve_id_raw = row["cve_id"] + is_ghsa = cve_id_raw.startswith("GHSA-") + + pipeline_input = { + "cve_id": None if is_ghsa else cve_id_raw, + "ghsa_id": cve_id_raw if is_ghsa else None, + "image_name": row["image_name"], + "filesystem_path": row["filesystem_path"], + "image_config_path": row["image_config_path"] or None, + } + + item_id = f"{cve_id_raw}__{row['image_name']}" + 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, + "item_id": item_id, + }, + ) + ) + + return EvalInput(eval_input_items=eval_items) + + +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() for k, v in row.items()}) + 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..399583f6b --- /dev/null +++ b/src/vuln_analysis/eval/sample_pb_eval.py @@ -0,0 +1,327 @@ +#!/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 random +import sys +from pathlib import Path + +_DEFAULT_SHEET = "Full dataset (Copy)" +_DEFAULT_DATA_ROOT = "data/pb_container_data" + +_CSV_COLUMNS = [ + "cve_id", + "image_name", + "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(image_url: str) -> str: + """Derive the nspect_pb_container_data dir name from a full image URL. + + e.g. nvcr.io/nvidia/tritonserver-pb25h2:25.08.03-... -> tritonserver-pb25h2_25.08.03-... + """ + name_tag = image_url.strip().split("/")[-1] + if ":" in name_tag: + name, tag = name_tag.split(":", 1) + return f"{name}_{tag}" + return name_tag + + +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))} + + img = d.get("Image URL") + cve = d.get("CVE ID") + if not img or not cve: + continue + img = str(img).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(img) + 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, img) + if key in seen: + continue + seen.add(key) + + cases.append({ + "cve_id": cve, + "image_name": img, + "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.", + ) + # Full PB csv file can be found here: https://docs.google.com/spreadsheets/d/1AZrKWJ04a3XKLvvxs1x7lnEE1rwg8kfrpcXpttOL-GA/edit?pli=1&gid=841845469#gid=841845469 + 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=_DEFAULT_DATA_ROOT, + help=f"Root dir with extracted container filesystems (default: {_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['cve_id']:<30} {c['image_name']:<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/register.py b/src/vuln_analysis/register.py index 97f15f72a..ce2ffde4a 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -46,7 +46,7 @@ ) from vuln_analysis.utils.report_parser import extract_markdown_table_field -# Import to trigger @register_function decorators +# Import to trigger @register_function / @register_evaluator decorators from vuln_analysis.functions import cve_researcher # noqa: F401 from vuln_analysis.functions import container_analyzer # noqa: F401 from vuln_analysis.functions import exploitability_analyzer # noqa: F401 @@ -56,6 +56,7 @@ from vuln_analysis.tools import filesystem_tools # noqa: F401 from vuln_analysis.tools import binary_tools # noqa: F401 from vuln_analysis.tools import package_tools # noqa: F401 +from vuln_analysis.eval.evaluators import accuracy # noqa: F401 logger = logging.getLogger(__name__) From 3dff0aa03cceba55cee7183bfe053f54dc1b2a42 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Thu, 12 Mar 2026 12:18:00 -0400 Subject: [PATCH 08/48] Add v3 caching behavior --- src/vuln_analysis/register.py | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index ce2ffde4a..3015c93bb 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -111,6 +111,19 @@ def _result_or_failure(result, agent_name: str) -> StageResult: async def _run_cve_researcher(state: PipelineState) -> StageResult: inp_data = state["input"] + use_cache = inp_data.get("cache", True) + identifier = inp_data.get("cve_id") or inp_data.get("ghsa_id") + + if use_cache: + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + cached_path = root / "vulnerability_reports" / f"{identifier}.md" + if cached_path.exists(): + logger.info("Cache hit — skipping CVE Researcher, report at %s", cached_path) + return StageResult( + agent_name="CVE Researcher", success=True, + report_path=str(cached_path), + ) + agent_inp = CVEResearcherInput( cve_id=inp_data.get("cve_id"), ghsa_id=inp_data.get("ghsa_id"), @@ -138,6 +151,20 @@ async def _run_cve_researcher(state: PipelineState) -> StageResult: async def _run_container_analyzer(state: PipelineState) -> StageResult: inp_data = state["input"] + use_cache = inp_data.get("cache", True) + + if use_cache: + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + filename = derive_report_filename(inp_data["image_name"]) + cached_path = root / "container_reports" / filename + if cached_path.exists(): + logger.info("Cache hit — skipping Container Analyzer, report at %s", cached_path) + return StageResult( + agent_name="Container Analyzer", success=True, + report_path=str(cached_path), + extra={"phase1_skipped": True}, + ) + agent_inp = ContainerAnalyzerInput( image_name=inp_data["image_name"], filesystem_path=inp_data["filesystem_path"], @@ -186,6 +213,25 @@ async def exploitability_node(state: PipelineState) -> dict: cve_report = state["cve_report"] container_report = state["container_report"] inp_data = state["input"] + use_cache = inp_data.get("cache", True) + + if use_cache: + container_name = extract_container_name_from_path(container_report.report_path) + cve_id = extract_cve_id_from_path(cve_report.report_path) + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as exploit_derive + cached_path = root / "exploitability_reports" / exploit_derive(cve_id, container_name) + if cached_path.exists(): + logger.info("Cache hit — skipping Exploitability Analyzer, report at %s", cached_path) + content = cached_path.read_text(encoding="utf-8") + verdict = extract_markdown_table_field(content, "Verdict") + confidence = extract_markdown_table_field(content, "Confidence") + stage_result = StageResult( + agent_name="Exploitability Analyzer", success=True, + report_path=str(cached_path), + extra={"verdict": verdict, "confidence": confidence}, + ) + return {"exploit_report": stage_result} agent_inp = ExploitabilityAnalyzerInput( vulnerability_report_path=cve_report.report_path, @@ -231,6 +277,32 @@ async def vex_node(state: PipelineState) -> dict: exploit_report = state["exploit_report"] container_report = state["container_report"] inp_data = state["input"] + use_cache = inp_data.get("cache", True) + + if use_cache: + cve_id, container_name = parse_report_filename(exploit_report.report_path) + stem = f"{cve_id}_{container_name}" + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + cached_md = root / "vex_results" / f"{stem}.md" + cached_json = root / "vex_results" / f"{stem}.json" + if cached_md.exists(): + logger.info("Cache hit — skipping VEX Categorizer, report at %s", cached_md) + content = cached_md.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") + stage_result = StageResult( + agent_name="VEX Categorizer", success=True, + report_path=str(cached_md), + extra={"status": status, "justification": justification, + "reasoning": reasoning, + "json_path": str(cached_json) if cached_json.exists() else None}, + ) + return { + "vex_result": stage_result, + "vex_status": status, + "vex_justification": justification, + } agent_inp = VEXCategorizerInput( exploitability_report_path=exploit_report.report_path, From bbd5a9a5b48a4296ed4c9514f0ca9b73d89397ea Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Thu, 12 Mar 2026 13:43:56 -0400 Subject: [PATCH 09/48] Fix caching for GHSA IDs --- src/vuln_analysis/register.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index 3015c93bb..c6dac8744 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -6,6 +6,7 @@ import asyncio import json import logging +import re import time import traceback from collections.abc import Awaitable, Callable @@ -37,7 +38,6 @@ ExploitabilityAnalyzerInput, ExploitabilityAnalyzerResult, extract_container_name_from_path, - extract_cve_id_from_path, ) from vuln_analysis.data_models.vex_categorizer import ( VEXCategorizerInput, @@ -217,7 +217,7 @@ async def exploitability_node(state: PipelineState) -> dict: if use_cache: container_name = extract_container_name_from_path(container_report.report_path) - cve_id = extract_cve_id_from_path(cve_report.report_path) + cve_id = _resolve_cve_id(cve_report.report_path) root = Path(inp_data.get("output_dir", config.output_dir)).resolve() from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as exploit_derive cached_path = root / "exploitability_reports" / exploit_derive(cve_id, container_name) @@ -431,6 +431,22 @@ async def _response_fn(inp: PipelineInput) -> PipelineResult: ) +def _resolve_cve_id(vulnerability_report_path: str) -> str: + """Extract CVE ID from a vulnerability report path, falling back to file content for GHSA reports.""" + 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 _write_vex_json( json_path: str, *, cve_id: str, container_name: str, status: str | None, justification: str | None, reasoning: str | None, From 4117efc839d03fec95d3c598465219575244380f Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Thu, 12 Mar 2026 19:08:31 -0400 Subject: [PATCH 10/48] multi-package CVE support, pre-triage enhancements, early-exit reports --- .../eval_datasets/morpheus_benchmark_2506.csv | 6 +- .../data_models/exploitability_analyzer.py | 24 + src/vuln_analysis/data_models/input.py | 26 + src/vuln_analysis/eval/sample_pb_eval.py | 1 - .../functions/exploitability_analyzer.py | 32 +- src/vuln_analysis/prompts/cve_researcher.py | 45 +- .../prompts/exploitability_analyzer.py | 325 +-- src/vuln_analysis/prompts/vex_categorizer.py | 80 +- src/vuln_analysis/register.py | 12 +- src/vuln_analysis/tools/binary_tools.py | 8 +- src/vuln_analysis/tools/filesystem_tools.py | 42 +- src/vuln_analysis/tools/web_tools.py | 2 +- src/vuln_analysis/utils/pre_triage.py | 1958 +++++++++++++++-- src/vuln_analysis/utils/report_generator.py | 295 ++- 14 files changed, 2466 insertions(+), 390 deletions(-) diff --git a/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv index df0db57b5..23a2c6790 100644 --- a/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv +++ b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv @@ -2,11 +2,11 @@ cve_id,image_name,filesystem_path,image_config_path,ground_truth_label GHSA-53q9-r3pm-6pq6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-4517,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-13836,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable -GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable GHSA-wf7f-8fxf-xfxc,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-pgqp-8h46-6x4j,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-gm62-xv2j-4w53,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable -GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable GHSA-7f5h-v6xp-fcq8,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-6mq8-rvhq-8wgg,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-63vm-454h-vhhq,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable @@ -20,7 +20,7 @@ CVE-2024-9287,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_ CVE-2025-4138,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-4330,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-4435,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable -CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable +CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-8194,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-9230,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-48384,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable diff --git a/src/vuln_analysis/data_models/exploitability_analyzer.py b/src/vuln_analysis/data_models/exploitability_analyzer.py index 424a2dec1..d97262657 100644 --- a/src/vuln_analysis/data_models/exploitability_analyzer.py +++ b/src/vuln_analysis/data_models/exploitability_analyzer.py @@ -40,6 +40,18 @@ class AffectedRange(BaseModel): 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 @@ -53,6 +65,7 @@ class ParsedVulnReport(BaseModel): 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): @@ -92,6 +105,12 @@ class PackageVerification(BaseModel): 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 @@ -100,6 +119,11 @@ class PreTriageResult(BaseModel): 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 diff --git a/src/vuln_analysis/data_models/input.py b/src/vuln_analysis/data_models/input.py index 1ef0fbb4d..295464dcd 100644 --- a/src/vuln_analysis/data_models/input.py +++ b/src/vuln_analysis/data_models/input.py @@ -16,6 +16,11 @@ class PipelineInput(BaseModel): model: str | None = None cache: bool = True + 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_ids(self): if not self.cve_id and not self.ghsa_id: @@ -26,6 +31,27 @@ def _check_ids(self): raise ValueError("filesystem_path must be 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.cve_id or self.ghsa_id diff --git a/src/vuln_analysis/eval/sample_pb_eval.py b/src/vuln_analysis/eval/sample_pb_eval.py index 399583f6b..4f9871a7d 100644 --- a/src/vuln_analysis/eval/sample_pb_eval.py +++ b/src/vuln_analysis/eval/sample_pb_eval.py @@ -227,7 +227,6 @@ def main() -> int: parser = argparse.ArgumentParser( description="Sample from the PB eval dataset (Excel) and emit a benchmark CSV.", ) - # Full PB csv file can be found here: https://docs.google.com/spreadsheets/d/1AZrKWJ04a3XKLvvxs1x7lnEE1rwg8kfrpcXpttOL-GA/edit?pli=1&gid=841845469#gid=841845469 parser.add_argument( "--xlsx", required=True, diff --git a/src/vuln_analysis/functions/exploitability_analyzer.py b/src/vuln_analysis/functions/exploitability_analyzer.py index caa3062d9..4a363c408 100644 --- a/src/vuln_analysis/functions/exploitability_analyzer.py +++ b/src/vuln_analysis/functions/exploitability_analyzer.py @@ -25,6 +25,7 @@ ) from vuln_analysis.tools.filesystem_tools import configure_allowed_paths from vuln_analysis.utils.agentic_loop import run_agentic_loop +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__) @@ -83,7 +84,36 @@ async def _arun(inp: ExploitabilityAnalyzerInput) -> ExploitabilityAnalyzerResul success=False, error=f"Pre-triage failed: {exc}", ) - logger.info("Pre-triage complete (early_exit=%s)", pre_triage_result.early_exit) + 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], diff --git a/src/vuln_analysis/prompts/cve_researcher.py b/src/vuln_analysis/prompts/cve_researcher.py index a4693983c..7a0c5764a 100644 --- a/src/vuln_analysis/prompts/cve_researcher.py +++ b/src/vuln_analysis/prompts/cve_researcher.py @@ -1,9 +1,18 @@ -"""System prompt and user message builder for the CVE Researcher.""" +"""System prompt construction for the CVE Researcher Agent. + +The system prompt encodes the full research workflow from the cursor rule +(cursor_rules/cve-analysis.mdc) so the LLM can autonomously research CVEs. +""" from __future__ import annotations from vuln_analysis.data_models.cve_researcher import CVEResearcherInput + +# --------------------------------------------------------------------------- +# System prompt (the agent's "behavioral specification") +# --------------------------------------------------------------------------- + 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: @@ -97,6 +106,15 @@ - 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` @@ -205,6 +223,24 @@ 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] @@ -312,9 +348,13 @@ def build_user_message(inp: CVEResearcherInput) -> str: - """Construct the initial user message for the LLM.""" + """Construct the initial user message for the LLM. + + Includes the CVE/GHSA to research plus any scanner-provided hints. + """ parts: list[str] = [] + # Primary identifier if inp.cve_id and inp.ghsa_id: parts.append( f"Research vulnerability **{inp.cve_id}** (also known as **{inp.ghsa_id}**)." @@ -327,6 +367,7 @@ def build_user_message(inp: CVEResearcherInput) -> str: "This is a GitHub Security Advisory ID – start by finding the associated CVE ID." ) + # Package/version hints from the scanner if inp.package_hint or inp.version_hint: hint_parts: list[str] = [] if inp.package_hint: diff --git a/src/vuln_analysis/prompts/exploitability_analyzer.py b/src/vuln_analysis/prompts/exploitability_analyzer.py index bc44db250..0c4b7ca63 100644 --- a/src/vuln_analysis/prompts/exploitability_analyzer.py +++ b/src/vuln_analysis/prompts/exploitability_analyzer.py @@ -34,7 +34,7 @@ **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 fifteen tools: +You have eighteen tools: **Filesystem:** read_file, list_directory, grep_search, glob_find @@ -54,22 +54,6 @@ 3. Container analysis report (from the Container Analyzer agent) — you can re-read this for container details 4. Extracted container filesystem (the ground truth) -## CRITICAL INSTRUCTIONS - -- **PRE-TRIAGE IS A STARTING POINT, NOT FINAL:** Pre-triage runs deterministic package lookups (pip dist-info, conda, Go SBOM, npm). It 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 the pre-triage says "package not found", treat this as a starting point — **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 at a vulnerable version, trust that finding and proceed directly to reachability. - -- **CONTAINER TYPE DRIVES VERDICT LOGIC:** - * Runtime + function not called → NOT REACHABLE - * SDK + function not called but accessible via normal import/API → CONDITIONALLY EXPLOITABLE (users can write code that calls it) - * SDK + code behind architectural barrier (alternative implementation backend selected by compiled native extension, internal runtime dispatch not exposed through user API, compile-time feature flag) → NOT REACHABLE (reaching the vulnerable path requires modifying internal implementation details of a dependency, not writing normal SDK code) - * Infrastructure + function not called → NOT REACHABLE (no user code), but with elevated risk level due to blast radius - -- **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. - -- **ASSIGN HONEST CONFIDENCE:** Rate verdict confidence as High/Medium/Low based on evidence quality. Reduce confidence when: CVE lacks specific function, code is compiled without source, cross-architecture limitations apply. - -- **CONTEXT EFFICIENCY:** Your context window is limited. Prefer `grep_search` over `read_file` for large structured files (package databases, dependency lists, log files). The pre-triage has already verified package presence, version, and environment — do not re-read package metadata files to re-confirm what the pre-triage summary already reports. - --- ## ANALYSIS WORKFLOW @@ -95,11 +79,17 @@ grep_search for the vulnerable function in discovered app code directories ``` -b. Trace transitive call chains within libraries: +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 @@ -127,7 +117,7 @@ - **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:** -When answering "can the attacker control the input?", read the CVE's attack vector first. The CVE defines what capabilities the attacker has — answer the question from that position, not from how the system behaves under normal, non-adversarial conditions. +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 10. **3.3 Assess input validation:** Search for validation, sanitization, or security middleware that might prevent exploitation. @@ -150,6 +140,7 @@ 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 @@ -168,7 +159,7 @@ | Verdict | Meaning | |---------|---------| | **EXPLOITABLE** | All conditions met — vulnerability can be triggered by an attacker | -| **CONDITIONALLY EXPLOITABLE** | Requires specific user action, configuration, or custom code (common for SDK containers) | +| **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 | @@ -206,100 +197,111 @@ ## DECISION FLOW -``` -Start (package confirmed present + vulnerable + in active env) - │ - ├─ Select reachability strategy based on CVE specificity: - │ ├─ Specific function known ──→ Function-level analysis (2.2) - │ └─ No specific function ────→ Module-level analysis (2.3, reduced confidence) - │ │ - ├─ Reachable? ─────────────── No ──┬── SDK container? ─── Yes ──┬── Accessible via ── Yes ──→ CONDITIONALLY - │ │ │ │ normal import/API? EXPLOITABLE - │ │ │ └── Behind architectural - │ │ │ barrier? ─── Yes ──→ NOT REACHABLE - │ │ ├── Runtime? ──────── Yes ──→ NOT REACHABLE - │ │ └── Infrastructure? ─ Yes ──→ NOT REACHABLE - │ Yes (elevated risk) - │ │ - ├─ Trigger conditions met? ── No ──→ NOT REACHABLE - │ (env vars, config, (or CONDITIONALLY EXPLOITABLE - │ network exposure) if user can change them) - │ │ - │ Yes - │ │ - ├─ Attacker controls ──────── No ──→ NOT REACHABLE - │ input? - │ │ - │ Yes - │ │ - ├─ Blocked by input ──────── Yes ──→ MITIGATED - │ validation? - │ │ - │ No - │ │ - ├─ Blocked by security ───── Yes ──→ MITIGATED - │ controls? (only CVE-specific controls count) - │ │ - │ No - │ │ - ├─ Assign confidence ─────── High / Medium / Low - │ (based on evidence quality) - │ │ - ├─ Assess post-exploitation impact (does NOT change verdict) - │ │ - └─────────────────────────────────→ EXPLOITABLE -``` +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** + 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** + 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 10). 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). --- -## BEHAVIORAL RULES +## 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." -1. **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 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. -2. **Handle Compiled/Bytecode Code:** Use `check_binary_dependencies` to extract dependency/module info and `search_binary_content` to search for symbols in compiled binaries instead of grep. These tools auto-detect the binary format. Note cross-architecture limitations. +### Reachability Analysis -2a. **C System Libraries Require Package Database Queries:** Python dist-info and glob for .so files will NOT find packages like libtiff5, libssl3, or libxml2. For C/C++ native library CVEs on Debian/Ubuntu containers, call `query_dpkg_database` first to find installed C libraries. Use `find_shared_libraries` to confirm .so presence. Use `search_dpkg_file_lists` to find which package owns a specific .so file. +**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). -2b. **Go Package Verification Requires Binary Analysis:** SBOM scanners may misreport Go dependencies (e.g., confusing nats-io/jwt with golang-jwt). Use `find_go_binaries` to enumerate ALL Go binaries in the container, then verify the actual module list with `go version -m`. Only conclude a Go package is absent after checking all binaries. +**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. -3. **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 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. -3c. **Guards Must Be Verified by Call Site, Not by Existence:** When you identify a guard, flag check, validation function, or security middleware that appears to block the vulnerable code path, you MUST verify it is actually invoked on the hot path — not just that it exists. Use `grep_search` to find all call sites of the guard function/method across the codebase. Confirm at least one call site lies on the path between the entry point and the vulnerable function. A guard that is defined but never called from the actual request-handling path provides zero protection. Common failure modes: - - A validation method exists on a class but is never called by the request handler that processes user input - - Security middleware is defined but not registered in the application's middleware stack - - A subclass overrides a parent method and calls `super()` in a way that bypasses the parent's guard - - A flag check exists in one code path (e.g., a CLI argument parser) but the hot path goes through a different route that skips it +**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. - **Do not conclude NOT REACHABLE or REQUIRES CONFIGURATION based on a guard unless you can show a call site of that guard on the actual entry point → vulnerable function chain.** +**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. -3d. **Derive the Attacker's Capabilities From the CVE, Not Your Own Assumptions:** Before reasoning about whether an attacker can trigger the vulnerable code, read the CVE's attack vector and description to understand what the attacker controls. Then ask: given those capabilities, can they reach the vulnerable code? Do not substitute your own threat model. The CVE defines the attacker's position — your job is to determine whether that position is sufficient to trigger the code in this container. +**Rule 9: 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. -3f. **`code_not_reachable` Means the Code Does Not Run — Not That Exploitation Is Difficult:** If the vulnerable code executes during container operation, the verdict cannot be `code_not_reachable`, regardless of how difficult the exploitation conditions are. Exploitation difficulty (e.g., requires specific attacker positioning, hard-to-achieve preconditions) affects confidence level and risk rating — not reachability. Use `code_not_reachable` only when the vulnerable code path does not execute at all. +### Verdict Determination - **The trustworthiness of external systems is not a reachability argument.** Whether a remote server, upstream service, or external dependency is "trusted" or "controlled by NVIDIA/the operator" describes the intended deployment — not the attack scenario. If the CVE is triggered by malicious data from an external system, the reachability question is whether the vulnerable code executes when that system behaves maliciously, not whether it is expected to behave correctly. +**Rule 10: 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. -3e. **Proving Absence Requires Searching All Callers, Not Just the Ones You Checked:** When you search for usage of a vulnerable function/API and don't find it in one part of the codebase, you have not proven absence — you have proven absence in that search scope. The vulnerable package may be used by other installed packages, utility scripts, or third-party libraries that also run in the container. Before concluding NOT REACHABLE based on "this API is not called," use `grep_search` broadly across the full filesystem to find ALL call sites, not just the application directories you already inspected. +**Rule 11: 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. -4. **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. +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 -5. **Filesystem Is Ground Truth:** Always verify claims against the actual filesystem contents. +**Rule 12: 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. -6. **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 13: 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. -7. **Environment Isolation Can Be Bypassed in SDK Containers:** For SDK containers, ENVIRONMENT ISOLATED is still valid (default environment is unaffected), but document that users can deliberately switch environments. +**Rule 14: 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. -8. **Confidence Communicates Uncertainty:** Medium and Low confidence verdicts signal that further investigation could change the conclusion. +### General -9. **Model-Serving Platforms Have Deployment-Time Code Paths:** Generic model-serving containers (e.g., NVIDIA Triton Inference Server, Ray Serve, BentoML, KServe) are blank inference platforms — they ship *code that can handle many model types* but contain NO model by default. A code path is only "in the default execution path" if it executes when the container starts with no model deployed and no user-provided model configuration. +**Rule 15: Filesystem is ground truth.** Always verify claims against the actual filesystem contents. - Ask explicitly: **"Would this code path be exercised in the container's out-of-the-box state — with no model loaded, no user-supplied model config, and no custom runtime args?"** If the answer is No, this code path is NOT in the default execution path and the verdict is **NOT REACHABLE**. +**Rule 16: 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. - Common traps: - - Multimodal processing code in a Triton container → only activates when a multimodal model is deployed; NOT in the default path if no model is baked into the image - - Streaming/chunked response handlers → only activate with models that use them - - Backend-specific code (vLLM backend, TensorRT backend) → only instantiated when a model using that backend is deployed +**Rule 17: 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. - To determine if a model is baked into the image: check for model weight files (`.pt`, `.onnx`, `.engine`, `.bin`, `.safetensors`) or model config files in the filesystem. If none are present, no model is deployed by default. +**Rule 18: 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 19: 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. --- @@ -314,14 +316,6 @@ | 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 | -### Node.js - -| Pattern | What to Search | Exploitation Requires | -|---------|---------------|----------------------| -| Prototype Pollution | `grep_search("merge\|extend\|assign\|defaultsDeep", ...)` include="*.js" | User-controlled object reaching merge | -| Path Traversal | `grep_search("path.join\|path.resolve\|readFile\|createReadStream", ...)` include="*.js" | User-controlled path component | -| Command Injection | `grep_search("child_process\|exec(\|execSync\|spawn", ...)` include="*.js" | User input in commands | - ### Go | Pattern | What to Search | Exploitation Requires | @@ -330,39 +324,16 @@ | 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 | -### Java - -| Pattern | What to Search | Exploitation Requires | -|---------|---------------|----------------------| -| Library presence | `glob_find("**/*.jar", ...)` | JAR in classpath | -| Deserialization | `search_binary_content` in JARs for "ObjectInputStream" | Untrusted data reaching deserializer | -| JNDI Injection | `search_binary_content` for "lookup" or "JndiLookup" | User-controlled JNDI name | - ### Native Libraries (C/C++) | Pattern | What to Search | Exploitation Requires | |---------|---------------|----------------------| -| Library presence | `glob_find("**/lib*.so*", ...)` | Library installed | +| 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 | --- -## QUICK REFERENCE: Dual-Mode Packages - -Some packages function as both client libraries and servers. For server-side CVEs, check if the server mode exists and is accessible. - -| Package | Client Usage | Server Mode | Check in Filesystem | -|---------|-------------|-------------|---------------------| -| `mlflow` | `mlflow.log_*()` | `mlflow server` | `glob_find("**/bin/mlflow", ...)` | -| `jupyter` | Client connections | `jupyter notebook/lab` | `glob_find("**/bin/jupyter*", ...)` | -| `tensorboard` | N/A | `tensorboard --logdir=...` | `glob_find("**/bin/tensorboard", ...)` | -| `flask` | N/A | `flask run` | Check site-packages/flask/ | -| `fastapi`/`uvicorn` | N/A | `uvicorn app:app` | `glob_find("**/bin/uvicorn", ...)` | -| `ray` | `ray.get()` | `ray start --head` | `glob_find("**/bin/ray", ...)` | - ---- - ## REPORT TEMPLATE You MUST produce the final report using this template. The downstream VEX Categorizer agent reads this format. @@ -556,11 +527,22 @@ def build_phase2_user_message( 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" - "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.\n\n" + f"{triage_preamble}\n\n" f"{triage_context}" ) @@ -609,8 +591,8 @@ def _format_triage_context(pre_triage: PreTriageResult) -> str: entry_points_str = ", ".join(cr.entry_points) if cr.entry_points else "N/A" - if pre_triage.early_exit: - triage_status = f"TRIAGE: {pre_triage.early_exit_verdict} (pre-triage could not confirm package — YOU MUST INDEPENDENTLY VERIFY)" + if not pkg.found: + triage_status = "TRIAGE: PACKAGE NOT CONFIRMED — YOU MUST INDEPENDENTLY VERIFY" else: triage_status = "TRIAGE: CONTINUE" @@ -643,8 +625,15 @@ def _format_triage_context(pre_triage: PreTriageResult) -> str: f"- Package Source: {primary.source if primary else 'N/A'}", ] - if pre_triage.early_exit and pre_triage.early_exit_reason: - lines.append(f"- Pre-Triage Note: {pre_triage.early_exit_reason}") + 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, " @@ -680,6 +669,49 @@ def _format_triage_context(pre_triage: PreTriageResult) -> str: 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", @@ -697,7 +729,38 @@ def _format_triage_context(pre_triage: PreTriageResult) -> str: "", "## Exploitation Hypothesis", f"For {vr.cve_id} to be exploitable in this container:", - f"1. {vr.package_raw} must be installed at a vulnerable version (confirmed: {primary.version if primary else 'N/A'})", + ]) + + 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", diff --git a/src/vuln_analysis/prompts/vex_categorizer.py b/src/vuln_analysis/prompts/vex_categorizer.py index 73c88d73b..1fcf76f78 100644 --- a/src/vuln_analysis/prompts/vex_categorizer.py +++ b/src/vuln_analysis/prompts/vex_categorizer.py @@ -102,7 +102,13 @@ **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:** The exploitability report verifies package presence via the container filesystem (the authoritative source). There is no separate SBOM check. +**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) @@ -151,6 +157,11 @@ 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 @@ -158,8 +169,12 @@ If attacker CANNOT control input, determine WHY: - Blocked by non-default configuration → **`requires_configuration`** (status: `not_affected`) + See `requires_configuration` guidance in the VERDICT-TO-VEX MAPPING section for important + nuances on what qualifies as "non-default configuration." - 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? @@ -227,7 +242,7 @@ 2. **Full analysis reports** (all other verdicts): - All Steps 1-4 contain complete analysis - - Conditions Matrix uses ✅/❌ with explanations + - 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. @@ -246,37 +261,42 @@ | 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 (any container type) | `affected` | `vulnerable` | **Always `vulnerable`.** CONDITIONALLY EXPLOITABLE means the conditions CAN be met by an attacker. This applies to SDK, Runtime, and Infrastructure containers equally. See CRITICAL NOTE below. | +| 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 | -**CRITICAL — CONDITIONALLY EXPLOITABLE always maps to `vulnerable`:** +**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.) -If Stage 2's verdict is **CONDITIONALLY EXPLOITABLE** (regardless of container type), the VEX -justification MUST be **`vulnerable`**. Do NOT use `requires_configuration`. +**Understanding `requires_configuration`:** -- **`requires_configuration`** means the PRODUCT ITSELF must be put in a non-default configuration - to expose the vulnerability (e.g., a disabled feature flag must be turned on, a debug mode must - be enabled, a listening port must be opened that is closed by default). -- **`requires_configuration` is WRONG** when: the product is already in its default deployed state - AND the only barrier is that the ATTACKER needs special positioning (MITM, supply chain access, - social engineering). Attacker capabilities and attack complexity are CVSS metrics — not VEX - justification criteria. -- When the product's outbound network requests are active by default (e.g., model downloads at - startup), the container IS in the exposure state by default. The fact that those requests go to - trusted endpoints by default 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`. +`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). -**NOT APPLICABLE has two sub-cases** — distinguish them: -- "Version is Vulnerable: No" or "Distro Backport Check: Patched" → `false_positive` -- "Package in Filesystem: No" → `code_not_present` +`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. -**MITIGATED** requires reading Steps 3-4 to determine the specific mitigation type. +**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 @@ -286,7 +306,7 @@ | 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", "compiled without", "stripped", "not included" | +| `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" | @@ -318,9 +338,8 @@ at the FIRST matching category. Do not skip ahead. **Call reason() for each step** to record your analysis and conclusion before moving on. -2. **STEP NUMBERING IS DIFFERENT.** The report's "Step 1/2/3/4" does NOT equal the VEX - flowchart's "Step 1/2/3/4/5/6". Use the Step Mapping table above to read the correct - report sections for each VEX decision step. +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". @@ -328,11 +347,8 @@ 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 type is SDK/Development and the - exploitability verdict is CONDITIONALLY EXPLOITABLE or NOT REACHABLE, the VEX - justification MUST be `vulnerable` (status: `affected`) UNLESS one of the still-valid - exceptions applies (false_positive, code_not_present, requires_dependency, - requires_environment, *_protected). +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: @@ -419,7 +435,7 @@ def build_user_message( "| **CVE ID** | [CVE-YYYY-NNNNN] |\n" "| **Container** | [container-name] |\n" "| **Container Type** | [SDK/Development / Runtime/Microservice / Infrastructure/Platform] |\n" - "| **Vulnerable Package** | [package name] |\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" diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index c6dac8744..14fd45cf2 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -111,7 +111,8 @@ def _result_or_failure(result, agent_name: str) -> StageResult: async def _run_cve_researcher(state: PipelineState) -> StageResult: inp_data = state["input"] - use_cache = inp_data.get("cache", True) + pipeline_inp = PipelineInput(**inp_data) + use_cache = pipeline_inp.use_cache_for("cve_researcher") identifier = inp_data.get("cve_id") or inp_data.get("ghsa_id") if use_cache: @@ -151,7 +152,8 @@ async def _run_cve_researcher(state: PipelineState) -> StageResult: async def _run_container_analyzer(state: PipelineState) -> StageResult: inp_data = state["input"] - use_cache = inp_data.get("cache", True) + pipeline_inp = PipelineInput(**inp_data) + use_cache = pipeline_inp.use_cache_for("container_analyzer") if use_cache: root = Path(inp_data.get("output_dir", config.output_dir)).resolve() @@ -213,7 +215,8 @@ async def exploitability_node(state: PipelineState) -> dict: cve_report = state["cve_report"] container_report = state["container_report"] inp_data = state["input"] - use_cache = inp_data.get("cache", True) + pipeline_inp = PipelineInput(**inp_data) + use_cache = pipeline_inp.use_cache_for("exploitability_analyzer") if use_cache: container_name = extract_container_name_from_path(container_report.report_path) @@ -277,7 +280,8 @@ async def vex_node(state: PipelineState) -> dict: exploit_report = state["exploit_report"] container_report = state["container_report"] inp_data = state["input"] - use_cache = inp_data.get("cache", True) + pipeline_inp = PipelineInput(**inp_data) + use_cache = pipeline_inp.use_cache_for("vex_categorizer") if use_cache: cve_id, container_name = parse_report_filename(exploit_report.report_path) diff --git a/src/vuln_analysis/tools/binary_tools.py b/src/vuln_analysis/tools/binary_tools.py index 354441453..7f1fc4645 100644 --- a/src/vuln_analysis/tools/binary_tools.py +++ b/src/vuln_analysis/tools/binary_tools.py @@ -54,9 +54,11 @@ def _analyze_elf(path: Path, magic: bytes) -> str: parts = ["Format: ELF binary"] is_go = False try: - result = subprocess.run(["strings", str(path)], capture_output=True, text=True, timeout=timeout) - if "go1." in result.stdout[:50000]: - is_go = True + 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: diff --git a/src/vuln_analysis/tools/filesystem_tools.py b/src/vuln_analysis/tools/filesystem_tools.py index 7d5a4edc3..2a333695e 100644 --- a/src/vuln_analysis/tools/filesystem_tools.py +++ b/src/vuln_analysis/tools/filesystem_tools.py @@ -76,10 +76,12 @@ 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_glob_results=200, logger_name="filesystem_tools"): + 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] = [] @@ -194,6 +196,38 @@ def list_directory(self, path: str) -> str: 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: @@ -216,6 +250,7 @@ def grep_search(self, pattern: str, path: str, include: str | None = None) -> st 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: @@ -241,7 +276,8 @@ def _grep_python(self, pattern: str, root: Path, include: str | None) -> str: continue for lineno, line in enumerate(text.splitlines(), 1): if regex.search(line): - matches.append(f"{fpath}:{lineno}:{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) @@ -277,6 +313,7 @@ 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: @@ -284,6 +321,7 @@ def _toolkit(self) -> 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, ) diff --git a/src/vuln_analysis/tools/web_tools.py b/src/vuln_analysis/tools/web_tools.py index 7963b91e4..9fe812990 100644 --- a/src/vuln_analysis/tools/web_tools.py +++ b/src/vuln_analysis/tools/web_tools.py @@ -21,7 +21,7 @@ def _looks_like_login_page(content: str) -> bool: - if len(content) > 1000: + if len(content) > 2000: return False lower = content[:2000].lower() return any(s in lower for s in _LOGIN_SIGNALS) diff --git a/src/vuln_analysis/utils/pre_triage.py b/src/vuln_analysis/utils/pre_triage.py index 645477110..84f1c29cd 100644 --- a/src/vuln_analysis/utils/pre_triage.py +++ b/src/vuln_analysis/utils/pre_triage.py @@ -1,12 +1,16 @@ """Deterministic pre-triage for the Exploitability Analyzer. Performs Steps 0-1 of the exploitability analysis deterministically: - 1. Parse vulnerability and container reports - 2. Discover the container filesystem layout - 3. Verify the vulnerable package on the filesystem + 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 + 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 @@ -15,11 +19,14 @@ 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, @@ -33,6 +40,51 @@ 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"], @@ -42,6 +94,10 @@ "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", @@ -53,122 +109,44 @@ }) -def _raw_listdir(directory: str) -> list[str]: - try: - return sorted(os.listdir(directory)) - except (OSError, PermissionError): - return [] - - -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 deterministic pre-triage verification. +# =================================================================== +# 1. Report Parsers +# =================================================================== - Returns a PreTriageResult with either an early-exit verdict or - enriched context for Phase 2 deep analysis. - """ - vuln_report = _parse_vulnerability_report(inp.vulnerability_report_path) - container_report = _parse_container_report(inp.container_report_path) - fs_layout = _discover_filesystem(inp.filesystem_path) - pkg_verification = _verify_package( - vuln_report, container_report, inp.filesystem_path, fs_layout, - ) - - early_exit = False - early_exit_verdict = None - early_exit_reason = None - if not pkg_verification.found: - early_exit = True - early_exit_verdict = "NOT APPLICABLE" - early_exit_reason = ( - f"Package '{vuln_report.package_raw}' not found on the container filesystem. " - f"Searched names: {', '.join(pkg_verification.searched_names) if pkg_verification.searched_names else vuln_report.package_name}." - ) - elif pkg_verification.is_vulnerable is False: - early_exit = True - early_exit_verdict = "NOT APPLICABLE" - if pkg_verification.backport_status == "patched": - early_exit_reason = ( - f"Package '{vuln_report.package_raw}' is present but the distro has " - f"backported a fix. {pkg_verification.backport_evidence or ''}" - ) - else: - inst = pkg_verification.active_env_installation or ( - pkg_verification.installations[0] if pkg_verification.installations else None - ) - early_exit_reason = ( - f"Package '{vuln_report.package_raw}' is present at version " - f"{inst.version if inst else 'unknown'} which is NOT in the vulnerable " - f"range ({vuln_report.vulnerable_versions}). Fixed in: {vuln_report.fixed_in}." - ) - elif ( - pkg_verification.found - and pkg_verification.active_env_installation is None - and pkg_verification.installations - ): - early_exit = True - early_exit_verdict = "ENVIRONMENT ISOLATED" - envs = [inst.environment or "unknown" for inst in pkg_verification.installations] - early_exit_reason = ( - f"Package '{vuln_report.package_raw}' found at a vulnerable version but " - f"only in non-default environment(s): {', '.join(envs)}. " - f"The default environment is '{container_report.default_environment}'." - ) - - return PreTriageResult( - vuln_report=vuln_report, - container_report=container_report, - filesystem_layout=fs_layout, - package_verification=pkg_verification, - early_exit=early_exit, - early_exit_verdict=early_exit_verdict, - early_exit_reason=early_exit_reason, - ) - - -def _parse_vulnerability_report(path: str) -> ParsedVulnReport: - """Parse key fields from a vulnerability report.""" - try: - content = Path(path).read_text(encoding="utf-8") - except Exception: - content = "" - - cve_id = extract_markdown_table_field(content, "CVE ID") or Path(path).stem - package = extract_markdown_table_field(content, "Affected Package") or "unknown" - vulnerable_versions = extract_markdown_table_field(content, "Vulnerable Versions") or "unknown" - fixed_in = extract_markdown_table_field(content, "Fixed In") or "unknown" +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") - cwe = extract_markdown_table_field(content, "CWE") + cvss_vector = extract_markdown_table_field(content, "CVSS Vector") or None + cwe = extract_markdown_table_field(content, "CWE") or None - cvss_score = None - cvss_severity = None - if cvss_raw: - parts = cvss_raw.split() + 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(parts[0]) - except (ValueError, IndexError): + cvss_score = float(cvss_m.group(1)) + except ValueError: pass - if len(parts) > 1: - cvss_severity = parts[1] + cvss_severity = cvss_m.group(2) + + package_name, ecosystem_hint = _parse_package_field(package_raw) - vulnerable_function = None - func_match = re.search(r"\*\*Function:\*\*\s*`?([^`\n]+)`?", content) - if func_match: - vulnerable_function = func_match.group(1).strip() + vulnerable_function = _extract_vulnerable_function(content) - package_name = re.sub(r"[^a-zA-Z0-9_.-]", "", package).lower().replace("-", "_") + 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, - ecosystem_hint=None, + package_raw=package_raw, + ecosystem_hint=ecosystem_hint, vulnerable_versions=vulnerable_versions, fixed_in=fixed_in, cvss_score=cvss_score, @@ -176,144 +154,1726 @@ def _parse_vulnerability_report(path: str) -> ParsedVulnReport: cvss_vector=cvss_vector, cwe=cwe, vulnerable_function=vulnerable_function, + affected_ranges=affected_ranges, + affected_packages=affected_packages, ) -def _parse_container_report(path: str) -> ParsedContainerReport: - """Parse key fields from a container analysis report.""" - try: - content = Path(path).read_text(encoding="utf-8") - except Exception: - content = "" - - image_name = extract_markdown_table_field(content, "Image Name") or Path(path).stem - container_type = extract_markdown_table_field(content, "Container Type") or "Runtime/Microservice" - primary_language = extract_markdown_table_field(content, "Primary Language") or "unknown" - base_os = extract_markdown_table_field(content, "Base Image") or extract_markdown_table_field(content, "Base OS") or "unknown" - - default_environment = None - env_match = re.search(r"activated.*?environment.*?[`\"'](\w+)[`\"']", content, re.IGNORECASE) - if env_match: - default_environment = env_match.group(1) - if not default_environment: - env_field = extract_markdown_table_field(content, "Default Environment") - if env_field: - default_environment = env_field - - runtime_user = extract_markdown_table_field(content, "USER directive") or "root" +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 "" + 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, container_type=container_type, primary_language=primary_language, - base_os=base_os, - default_environment=default_environment, + 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 +# =================================================================== -def _discover_filesystem(filesystem_path: str) -> FilesystemLayout: - """Discover the container's filesystem layout.""" - fs = Path(filesystem_path) +_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 conda_envs_dir.iterdir() if d.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"]: + 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(): - layout.site_packages_paths.append(str(sp)) + 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 app_dir_name in ["workspace", "app", "opt", "srv"]: - candidate = fs / app_dir_name - if candidate.is_dir(): - layout.app_code_dirs.append(str(candidate)) + 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*.sh", "**/docker-entrypoint*.sh", "**/start*.sh"]: + for pattern in ("**/entrypoint*", "**/docker-entrypoint*"): for ep in fs.glob(pattern): if ep.is_file(): - layout.entrypoint_scripts.append(str(ep)) + 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 -def _verify_package( - vuln_report: ParsedVulnReport, - container_report: ParsedContainerReport, - filesystem_path: str, +# =================================================================== +# 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.""" - search_names = [vuln_report.package_name] - aliases = PACKAGE_ALIASES.get(vuln_report.package_name, []) - search_names.extend(aliases) - search_names = list(dict.fromkeys(search_names)) + 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_path in layout.site_packages_paths: - sp = Path(sp_path) + 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 metadata_file.is_file(): - try: - metadata = metadata_file.read_text(encoding="utf-8", errors="ignore") - version_match = re.search(r"^Version:\s*(.+)$", metadata, re.MULTILINE) - version = version_match.group(1).strip() if version_match else "unknown" - except Exception: - version = "unknown" - - env = None - if "conda/envs/" in sp_path: - env_match = re.search(r"conda/envs/([^/]+)/", sp_path) - if env_match: - env = env_match.group(1) + 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=str(dist_info), - source="pip", - environment=env, + location=rel, + source="npm", )) + except json.JSONDecodeError: + continue - if not installations: - return PackageVerification(found=False, searched_names=search_names) - - active_env = container_report.default_environment - active_inst = None - if active_env: - for inst in installations: - if inst.environment == active_env: - active_inst = inst + 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 not active_inst and installations: - for inst in installations: - if inst.environment is None: - active_inst = inst + 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 - is_vulnerable = None - check_inst = active_inst or installations[0] + 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_ver = Version(check_inst.version) - if vuln_report.fixed_in and vuln_report.fixed_in != "unknown": + 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_ver = Version(vuln_report.fixed_in) - is_vulnerable = installed_ver < fixed_ver + fixed = Version(fv) except InvalidVersion: - pass + 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: - pass + return None - return PackageVerification( - found=True, - installations=installations, - active_env_installation=active_inst, - is_vulnerable=is_vulnerable, - searched_names=search_names, + 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/report_generator.py b/src/vuln_analysis/utils/report_generator.py index ee3ec0405..5f58ad036 100644 --- a/src/vuln_analysis/utils/report_generator.py +++ b/src/vuln_analysis/utils/report_generator.py @@ -1,7 +1,11 @@ """Template-based report generation for early-exit exploitability verdicts. -When pre-triage determines NOT APPLICABLE or ENVIRONMENT ISOLATED, -this generates the full markdown report with zero LLM calls. +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 @@ -14,25 +18,61 @@ ) -def generate_early_exit_report(result: PreTriageResult, inp: ExploitabilityAnalyzerInput) -> str: - """Generate a complete markdown exploitability report for an early exit.""" +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" report = f"""# Container Exploitability Analysis @@ -50,27 +90,82 @@ def generate_early_exit_report(result: PreTriageResult, inp: ExploitabilityAnaly | **Confidence** | High | | **Risk Level** | Informational | -## Analysis +## 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** | {fs_version} | Filesystem verification | -| **Version is Vulnerable** | {version_vulnerable} | Version comparison | +| **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)} | -### Step 2: Code Reachability +{_additional_packages_step1(additional)}### Step 2: Code Reachability -Skipped — {reason} +Skipped — {skip_reason} ### Step 3: Data Flow and Trigger Conditions -Skipped — {reason} +Skipped — {skip_reason} ### Step 4: Security Controls and Impact -Skipped — {reason} +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} @@ -79,6 +174,10 @@ def generate_early_exit_report(result: PreTriageResult, inp: ExploitabilityAnaly **Justification:** {reason} +### Attack Scenario + +Not applicable — the vulnerability cannot be triggered in this container. + ## References - Container Report: {inp.container_report_path} @@ -86,3 +185,177 @@ def generate_early_exit_report(result: PreTriageResult, inp: ExploitabilityAnaly - 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}." From 6132f6c60053d25e1547d26651b3a1164c1b4588 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Fri, 13 Mar 2026 11:56:37 -0400 Subject: [PATCH 11/48] Update to official NAT 1.5 release --- MIGRATION_PLAN.md | 4 +- pyproject.toml | 2 +- uv.lock | 2307 +++++++++++++++++---------------------------- 3 files changed, 867 insertions(+), 1446 deletions(-) diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index bc08afde7..13bb3c3a7 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -158,10 +158,10 @@ src/vuln_analysis/ vuln_analysis = "vuln_analysis.register" ``` -2. **Update `nvidia-nat` version** from `~=1.4.0` to `==1.5.0rc5`: +2. **Update `nvidia-nat` version** from `~=1.4.0` to `==1.5.0`: ```toml -"nvidia-nat[async_endpoints,langchain,phoenix,profiling]==1.5.0rc5", +"nvidia-nat[async_endpoints,langchain,phoenix,profiling]==1.5.0", ``` 3. **Remove `langchain-classic`** from dependencies (keep `langchain` and `langchain-core`). diff --git a/pyproject.toml b/pyproject.toml index 4a21fd691..56f4e4e8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "langchain-core", "langgraph>=1.0.5,<2.0.0", "filelock", - "nvidia-nat[async_endpoints,langchain,phoenix,profiling,eval]==1.5.0rc5", + "nvidia-nat[async_endpoints,langchain,phoenix,profiling,eval]==1.5.0", "openinference-instrumentation-langchain", "packaging>=24.0", "pydpkg==1.9.4", diff --git a/uv.lock b/uv.lock index deef54de3..b59046839 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.11, <3.13" +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]] @@ -77,55 +75,23 @@ dependencies = [ ] 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" }, + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, ] [[package]] @@ -146,25 +112,12 @@ 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" }, ] -[[package]] -name = "aioresponses" -version = "0.7.6" -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" } -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" }, -] - [[package]] name = "aiosignal" 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" } wheels = [ @@ -222,7 +175,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { 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" } wheels = [ @@ -258,8 +210,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" }, @@ -397,6 +348,15 @@ 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" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "bokeh" version = "3.8.2" @@ -405,8 +365,7 @@ dependencies = [ { name = "contourpy" }, { name = "jinja2" }, { name = "narwhals" }, - { 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 = "pillow" }, @@ -447,34 +406,6 @@ 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" }, ] -[[package]] -name = "brotli" -version = "1.2.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" }, -] - [[package]] name = "cachetools" version = "6.2.2" @@ -502,31 +433,18 @@ dependencies = [ ] 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" }, + { 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, upload-time = "2025-09-08T23:23:00.879Z" }, + { 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, upload-time = "2025-09-08T23:23:02.231Z" }, + { 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, upload-time = "2025-09-08T23:23:03.472Z" }, + { 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, upload-time = "2025-09-08T23:23:04.792Z" }, + { 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, upload-time = "2025-09-08T23:23:06.127Z" }, + { 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, upload-time = "2025-09-08T23:23:07.753Z" }, + { 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, upload-time = "2025-09-08T23:23:09.648Z" }, + { 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, upload-time = "2025-09-08T23:23:10.928Z" }, + { 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, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, ] [[package]] @@ -535,38 +453,22 @@ 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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { 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, upload-time = "2025-10-14T04:41:14.461Z" }, + { 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, upload-time = "2025-10-14T04:41:15.588Z" }, + { 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, upload-time = "2025-10-14T04:41:16.738Z" }, + { 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, upload-time = "2025-10-14T04:41:17.923Z" }, + { 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, upload-time = "2025-10-14T04:41:19.106Z" }, + { 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, upload-time = "2025-10-14T04:41:20.245Z" }, + { 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, upload-time = "2025-10-14T04:41:21.398Z" }, + { 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, upload-time = "2025-10-14T04:41:22.583Z" }, + { 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, upload-time = "2025-10-14T04:41:23.754Z" }, + { 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, upload-time = "2025-10-14T04:41:25.27Z" }, + { 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, upload-time = "2025-10-14T04:41:26.725Z" }, + { 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, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, { 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" }, ] @@ -617,38 +519,32 @@ 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'" }, + { name = "numpy" }, ] 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" }, + { 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, upload-time = "2025-07-26T12:01:39.367Z" }, + { 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, upload-time = "2025-07-26T12:01:40.645Z" }, + { 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, upload-time = "2025-07-26T12:01:41.942Z" }, + { 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, upload-time = "2025-07-26T12:01:43.499Z" }, + { 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, upload-time = "2025-07-26T12:01:45.219Z" }, + { 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, upload-time = "2025-07-26T12:01:46.519Z" }, + { 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, upload-time = "2025-07-26T12:01:48.964Z" }, + { 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, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { 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, upload-time = "2025-07-26T12:01:57.051Z" }, + { 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, upload-time = "2025-07-26T12:01:58.663Z" }, + { 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, upload-time = "2025-07-26T12:02:00.343Z" }, + { 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, upload-time = "2025-07-26T12:02:02.128Z" }, + { 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, upload-time = "2025-07-26T12:02:03.615Z" }, + { 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, upload-time = "2025-07-26T12:02:05.165Z" }, + { 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, upload-time = "2025-07-26T12:02:07.379Z" }, + { 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, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, ] [[package]] @@ -657,40 +553,35 @@ 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/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { 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, upload-time = "2025-11-18T13:32:50.78Z" }, + { 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, upload-time = "2025-11-18T13:32:52.554Z" }, + { 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, upload-time = "2025-11-18T13:32:54.862Z" }, + { 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, upload-time = "2025-11-18T13:32:56.417Z" }, + { 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, upload-time = "2025-11-18T13:32:58.074Z" }, + { 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, upload-time = "2025-11-18T13:32:59.646Z" }, + { 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, upload-time = "2025-11-18T13:33:01.481Z" }, + { 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, upload-time = "2025-11-18T13:33:02.935Z" }, + { 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, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { 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, upload-time = "2025-11-18T13:33:11.153Z" }, + { 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, upload-time = "2025-11-18T13:33:12.569Z" }, + { 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, upload-time = "2025-11-18T13:33:14.037Z" }, + { 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, upload-time = "2025-11-18T13:33:15.601Z" }, + { 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, upload-time = "2025-11-18T13:33:17.354Z" }, + { 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, upload-time = "2025-11-18T13:33:18.958Z" }, + { 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, upload-time = "2025-11-18T13:33:20.644Z" }, + { 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, upload-time = "2025-11-18T13:33:22.189Z" }, + { 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, upload-time = "2025-11-18T13:33:24.239Z" }, + { 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, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, { 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'" }, -] - [[package]] name = "cryptography" version = "46.0.3" @@ -730,12 +621,6 @@ wheels = [ { 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" }, ] [[package]] @@ -755,7 +640,6 @@ dependencies = [ { name = "click" }, { name = "cloudpickle" }, { name = "fsspec" }, - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "partd" }, { name = "pyyaml" }, @@ -790,8 +674,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" }, @@ -886,12 +769,6 @@ 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" }, ] -[[package]] -name = "esprima" -version = "4.0.1" -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" } - [[package]] name = "et-xmlfile" version = "2.0.0" @@ -920,50 +797,20 @@ dependencies = [ ] 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" }, -] - [[package]] name = "fastapi" -version = "0.128.0" +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/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, -] - -[[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" } -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, upload-time = "2026-03-01T18:18:30.831Z" }, ] [[package]] @@ -972,28 +819,17 @@ 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" }, + { 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, upload-time = "2025-10-19T22:42:34.633Z" }, + { 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, upload-time = "2025-10-19T22:30:25.482Z" }, + { 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, upload-time = "2025-10-19T22:36:14.096Z" }, + { 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, upload-time = "2025-10-19T22:36:23.302Z" }, + { 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, upload-time = "2025-10-19T22:29:43.809Z" }, + { 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, upload-time = "2025-10-19T22:36:06.825Z" }, + { 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, upload-time = "2025-10-19T22:35:31.623Z" }, + { 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, upload-time = "2025-10-19T22:26:03.023Z" }, + { 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, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, ] [[package]] @@ -1039,28 +875,37 @@ 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" }, ] +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + [[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/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, { 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" }, ] @@ -1070,38 +915,38 @@ 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/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { 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, upload-time = "2025-10-06T05:36:28.855Z" }, + { 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, upload-time = "2025-10-06T05:36:29.877Z" }, + { 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, upload-time = "2025-10-06T05:36:31.301Z" }, + { 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, upload-time = "2025-10-06T05:36:32.531Z" }, + { 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, upload-time = "2025-10-06T05:36:33.706Z" }, + { 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, upload-time = "2025-10-06T05:36:34.947Z" }, + { 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, upload-time = "2025-10-06T05:36:36.534Z" }, + { 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, upload-time = "2025-10-06T05:36:38.582Z" }, + { 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, upload-time = "2025-10-06T05:36:40.152Z" }, + { 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, upload-time = "2025-10-06T05:36:41.355Z" }, + { 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, upload-time = "2025-10-06T05:36:42.716Z" }, + { 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, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { 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, upload-time = "2025-10-06T05:36:48.78Z" }, + { 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, upload-time = "2025-10-06T05:36:49.837Z" }, + { 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, upload-time = "2025-10-06T05:36:50.851Z" }, + { 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, upload-time = "2025-10-06T05:36:51.898Z" }, + { 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, upload-time = "2025-10-06T05:36:53.101Z" }, + { 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, upload-time = "2025-10-06T05:36:54.309Z" }, + { 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, upload-time = "2025-10-06T05:36:55.566Z" }, + { 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, upload-time = "2025-10-06T05:36:56.758Z" }, + { 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, upload-time = "2025-10-06T05:36:57.965Z" }, + { 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, upload-time = "2025-10-06T05:36:59.237Z" }, + { 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, upload-time = "2025-10-06T05:37:00.811Z" }, + { 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, upload-time = "2025-10-06T05:37:02.115Z" }, + { 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, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, { 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" }, ] @@ -1143,15 +988,6 @@ 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" }, ] -[[package]] -name = "google-search-results" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -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" version = "1.72.0" @@ -1179,28 +1015,17 @@ 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" }, + { 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, upload-time = "2025-08-07T13:15:50.011Z" }, + { 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, upload-time = "2025-08-07T13:42:57.23Z" }, + { 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, upload-time = "2025-08-07T13:45:29.752Z" }, + { 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, upload-time = "2025-08-07T13:53:16.314Z" }, + { 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, upload-time = "2025-08-07T13:18:32.861Z" }, + { 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, upload-time = "2025-08-07T13:18:31.636Z" }, + { 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, upload-time = "2025-08-07T13:42:41.117Z" }, + { 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, upload-time = "2025-08-07T13:18:24.072Z" }, + { 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, upload-time = "2025-11-04T12:42:19.395Z" }, + { 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, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, ] [[package]] @@ -1224,26 +1049,16 @@ dependencies = [ ] 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" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { 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, upload-time = "2025-10-21T16:21:51.142Z" }, + { 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, upload-time = "2025-10-21T16:21:54.213Z" }, + { 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, upload-time = "2025-10-21T16:21:56.476Z" }, + { 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, upload-time = "2025-10-21T16:21:59.051Z" }, + { 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, upload-time = "2025-10-21T16:22:02.049Z" }, + { 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, upload-time = "2025-10-21T16:22:04.984Z" }, + { 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, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, ] [[package]] @@ -1261,6 +1076,13 @@ 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, upload-time = "2025-10-24T19:04:32.129Z" } 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, upload-time = "2025-10-24T19:04:11.422Z" }, + { 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, upload-time = "2025-10-24T19:04:09.586Z" }, + { 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, upload-time = "2025-10-24T19:04:00.314Z" }, + { 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, upload-time = "2025-10-24T19:03:58.111Z" }, + { 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, upload-time = "2025-10-24T19:04:20.951Z" }, + { 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, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, { 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" }, @@ -1289,20 +1111,13 @@ 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" } 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, upload-time = "2025-10-10T03:54:47.089Z" }, + { 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, upload-time = "2025-10-10T03:54:48.052Z" }, + { 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, upload-time = "2025-10-10T03:54:48.919Z" }, + { 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, upload-time = "2025-10-10T03:54:49.993Z" }, + { 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, upload-time = "2025-10-10T03:54:51.066Z" }, + { 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, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, ] [[package]] @@ -1414,40 +1229,24 @@ 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" }, + { 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, upload-time = "2025-11-09T20:47:44.424Z" }, + { 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, upload-time = "2025-11-09T20:47:45.973Z" }, + { 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, upload-time = "2025-11-09T20:47:47.535Z" }, + { 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, upload-time = "2025-11-09T20:47:49.284Z" }, + { 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, upload-time = "2025-11-09T20:47:50.807Z" }, + { 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, upload-time = "2025-11-09T20:47:52.211Z" }, + { 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, upload-time = "2025-11-09T20:47:53.512Z" }, + { 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, upload-time = "2025-11-09T20:47:54.893Z" }, + { 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, upload-time = "2025-11-09T20:47:56.253Z" }, + { 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, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { 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, upload-time = "2025-11-09T20:48:03.783Z" }, + { 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, upload-time = "2025-11-09T20:48:05.55Z" }, + { 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, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, ] [[package]] @@ -1468,15 +1267,6 @@ 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" }, ] -[[package]] -name = "json5" -version = "0.12.1" -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" } -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" }, -] - [[package]] name = "jsonpatch" version = "1.33" @@ -1537,56 +1327,37 @@ 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" }, ] -[[package]] -name = "jupyter-core" -version = "5.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, -] -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" } -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" }, -] - [[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" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, ] [[package]] @@ -1610,8 +1381,7 @@ 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/52/1d/bb306951b1c394b7a27effb8eb6c9ee65dd77fcc4be7c20f76e3299a9e1e/langchain_aws-1.1.0.tar.gz", hash = "sha256:1e2f8570328eae4907c3cf7e900dc68d8034ddc865d9dc96823c9f9d8cccb901", size = 393899, upload-time = "2025-11-24T14:35:24.216Z" } @@ -1648,8 +1418,7 @@ 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" }, @@ -1911,28 +1680,28 @@ 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" }, + { 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, upload-time = "2025-09-27T18:36:41.777Z" }, + { 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, upload-time = "2025-09-27T18:36:43.257Z" }, + { 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, upload-time = "2025-09-27T18:36:44.508Z" }, + { 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, upload-time = "2025-09-27T18:36:45.385Z" }, + { 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, upload-time = "2025-09-27T18:36:46.916Z" }, + { 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, upload-time = "2025-09-27T18:36:47.884Z" }, + { 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, upload-time = "2025-09-27T18:36:48.82Z" }, + { 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, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { 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, upload-time = "2025-09-27T18:36:54.819Z" }, + { 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, upload-time = "2025-09-27T18:36:55.714Z" }, + { 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, upload-time = "2025-09-27T18:36:56.908Z" }, + { 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, upload-time = "2025-09-27T18:36:57.913Z" }, + { 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, upload-time = "2025-09-27T18:36:58.833Z" }, + { 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, upload-time = "2025-09-27T18:36:59.739Z" }, + { 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, upload-time = "2025-09-27T18:37:00.719Z" }, + { 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, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] [[package]] @@ -1956,8 +1725,7 @@ dependencies = [ { 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" }, @@ -1965,23 +1733,20 @@ dependencies = [ ] 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" } 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/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, ] [[package]] @@ -2008,24 +1773,15 @@ 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" }, + { 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, upload-time = "2025-10-08T09:15:14.552Z" }, + { 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, upload-time = "2025-10-08T09:15:15.543Z" }, + { 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, upload-time = "2025-10-08T09:15:16.567Z" }, + { 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, upload-time = "2025-10-08T09:15:17.825Z" }, + { 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, upload-time = "2025-10-08T09:15:19.003Z" }, + { 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, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, ] [[package]] @@ -2034,42 +1790,42 @@ 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/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { 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, upload-time = "2025-10-06T14:49:55.82Z" }, + { 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, upload-time = "2025-10-06T14:49:57.048Z" }, + { 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, upload-time = "2025-10-06T14:49:58.368Z" }, + { 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, upload-time = "2025-10-06T14:49:59.89Z" }, + { 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, upload-time = "2025-10-06T14:50:01.485Z" }, + { 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, upload-time = "2025-10-06T14:50:02.955Z" }, + { 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, upload-time = "2025-10-06T14:50:04.446Z" }, + { 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, upload-time = "2025-10-06T14:50:05.98Z" }, + { 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, upload-time = "2025-10-06T14:50:07.511Z" }, + { 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, upload-time = "2025-10-06T14:50:09.074Z" }, + { 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, upload-time = "2025-10-06T14:50:10.714Z" }, + { 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, upload-time = "2025-10-06T14:50:12.28Z" }, + { 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, upload-time = "2025-10-06T14:50:14.16Z" }, + { 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, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { 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, upload-time = "2025-10-06T14:50:21.223Z" }, + { 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, upload-time = "2025-10-06T14:50:22.871Z" }, + { 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, upload-time = "2025-10-06T14:50:24.258Z" }, + { 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, upload-time = "2025-10-06T14:50:25.716Z" }, + { 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, upload-time = "2025-10-06T14:50:28.192Z" }, + { 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, upload-time = "2025-10-06T14:50:29.82Z" }, + { 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, upload-time = "2025-10-06T14:50:31.731Z" }, + { 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, upload-time = "2025-10-06T14:50:33.26Z" }, + { 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, upload-time = "2025-10-06T14:50:34.808Z" }, + { 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, upload-time = "2025-10-06T14:50:36.436Z" }, + { 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, upload-time = "2025-10-06T14:50:37.953Z" }, + { 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, upload-time = "2025-10-06T14:50:39.574Z" }, + { 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, upload-time = "2025-10-06T14:50:41.612Z" }, + { 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, upload-time = "2025-10-06T14:50:43.972Z" }, + { 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, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, { 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" }, ] @@ -2082,12 +1838,10 @@ dependencies = [ ] 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" } 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/ee/25/7d7e78e750bc1aecfaf0efbf826c69a791d2eeaf29cf20cba93ff4cced78/multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334", size = 151917, upload-time = "2025-04-17T03:11:24.044Z" }, { 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" }, ] @@ -2110,37 +1864,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, ] -[[package]] -name = "nbformat" -version = "5.10.4" -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" } -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" }, -] - -[[package]] -name = "nemollm" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "requests-futures" }, - { name = "requests-toolbelt" }, - { name = "tqdm" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -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" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2170,94 +1893,82 @@ wheels = [ [[package]] name = "numpy" -version = "1.26.4" +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/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" }, +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/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { 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, upload-time = "2025-11-16T22:50:21.487Z" }, + { 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, upload-time = "2025-11-16T22:50:24.562Z" }, + { 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, upload-time = "2025-11-16T22:50:26.47Z" }, + { 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, upload-time = "2025-11-16T22:50:28.861Z" }, + { 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, upload-time = "2025-11-16T22:50:31.56Z" }, + { 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, upload-time = "2025-11-16T22:50:34.242Z" }, + { 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, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { 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, upload-time = "2025-11-16T22:50:47.715Z" }, + { 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, upload-time = "2025-11-16T22:50:51.327Z" }, + { 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, upload-time = "2025-11-16T22:50:54.542Z" }, + { 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, upload-time = "2025-11-16T22:50:56.794Z" }, + { 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, upload-time = "2025-11-16T22:50:59.208Z" }, + { 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, upload-time = "2025-11-16T22:51:01.991Z" }, + { 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, upload-time = "2025-11-16T22:51:05.291Z" }, + { 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, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, ] [[package]] -name = "numpy" -version = "2.3.5" +name = "nvidia-nat" +version = "1.5.0" 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'", +dependencies = [ + { name = "nvidia-nat-core" }, ] -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" }, + { url = "https://files.pythonhosted.org/packages/9c/5a/065c237d5f2b9e925092a50aa99c3caf00ec92c47e84597f9f28b1872d3b/nvidia_nat-1.5.0-py3-none-any.whl", hash = "sha256:7d69a2841460153f2fc6fcd8dd9b1a0fd8f094c56167a3faf78c56c4dbb48fb0", size = 52591, upload-time = "2026-03-12T21:04:59.096Z" }, +] + +[package.optional-dependencies] +async-endpoints = [ + { name = "nvidia-nat-core", extra = ["async-endpoints"] }, +] +eval = [ + { name = "nvidia-nat-eval" }, +] +langchain = [ + { name = "nvidia-nat-langchain" }, +] +phoenix = [ + { name = "nvidia-nat-phoenix" }, +] +profiling = [ + { name = "nvidia-nat-eval", extra = ["profiling"] }, ] [[package]] -name = "nvidia-nat" -version = "1.4.0" +name = "nvidia-nat-core" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aioboto3" }, { name = "authlib" }, { name = "click" }, { name = "colorama" }, - { name = "datasets" }, { name = "expandvars" }, { name = "fastapi" }, + { name = "flask" }, { name = "httpx" }, + { name = "huggingface-hub" }, { name = "jinja2" }, { name = "jsonpath-ng" }, { 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 = "numpy" }, { name = "openinference-semantic-conventions" }, - { name = "openpyxl" }, { name = "optuna" }, { name = "pandas" }, { name = "pip" }, @@ -2266,18 +1977,20 @@ dependencies = [ { name = "platformdirs" }, { name = "plotly" }, { name = "pydantic" }, + { name = "pyjwt" }, { name = "pymilvus" }, { name = "python-dotenv" }, + { name = "python-multipart" }, { name = "pyyaml" }, - { name = "ragas" }, { name = "rich" }, + { name = "starlette" }, { name = "tabulate" }, { name = "urllib3" }, { name = "uvicorn", extra = ["standard"] }, { name = "wikipedia" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/29/c6/21cf6bd5ce2126264912a696f229a559b6bf1483b712a22b5b8e787405ba/nvidia_nat-1.4.0-py3-none-any.whl", hash = "sha256:82356d49498597ca11be07082133112795ea2d7b3ec1f9c572e8dce23773f7dd", size = 1011876, upload-time = "2026-02-03T03:07:20.462Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/c2aa1b2d38fd45189bfaed4415d23eb7d55b5ce52159386102d0181f1f3f/nvidia_nat_core-1.5.0-py3-none-any.whl", hash = "sha256:923bab6860e3b4b0ad1dad8c33dd0c5b4d7fc4cbab9e4a82d744315704686083", size = 780149, upload-time = "2026-03-12T21:01:06.213Z" }, ] [package.optional-dependencies] @@ -2287,27 +2000,37 @@ 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.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datasets" }, + { name = "nvidia-nat-core" }, + { name = "openpyxl" }, + { name = "ragas" }, ] -phoenix = [ - { name = "nvidia-nat-phoenix" }, +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e7/0ecb6c6d421e123366b5c612c9148b32576d2abff14585801fe274e1b630/nvidia_nat_eval-1.5.0-py3-none-any.whl", hash = "sha256:c27371742c352ab64855208784175d937c646ef43fd454be182b0af67fd0d187", size = 208222, upload-time = "2026-03-12T21:03:22.113Z" }, ] + +[package.optional-dependencies] profiling = [ - { name = "nvidia-nat-profiling" }, + { name = "matplotlib" }, + { name = "prefixspan" }, + { name = "scikit-learn" }, ] [[package]] name = "nvidia-nat-langchain" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "langchain-aws" }, { name = "langchain-classic" }, + { name = "langchain-community" }, { name = "langchain-core" }, { name = "langchain-huggingface" }, { name = "langchain-litellm" }, @@ -2316,51 +2039,40 @@ dependencies = [ { name = "langchain-openai" }, { name = "langchain-tavily" }, { name = "langgraph" }, - { name = "nvidia-nat" }, + { name = "nvidia-nat-core" }, + { name = "nvidia-nat-opentelemetry" }, + { name = "openevals" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/83/9bf4ede51d0f2be7a3c5c5ec4c2bfa56f52a084ecedb8fb67cc5e8f5e776/nvidia_nat_langchain-1.4.0-py3-none-any.whl", hash = "sha256:7b7ac79d28f7a4bf21a68ee26e969fcf0b2fc84628042e162c74d4a3233cdeb6", size = 64528, upload-time = "2026-02-03T03:03:47.095Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/a1166335f52759b569f1015ac5f66cfbc7e3342ae33ecf6231251acf49f7/nvidia_nat_langchain-1.5.0-py3-none-any.whl", hash = "sha256:1a3a445d6f90e8b6c394ca4e8829889778e7896c258e6b9cc98736554d863a4e", size = 178153, upload-time = "2026-03-12T21:06:15.519Z" }, ] [[package]] name = "nvidia-nat-opentelemetry" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nat" }, + { name = "nvidia-nat-core" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, { name = "opentelemetry-sdk" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/6f/fd6b03ef118840a2dac1328712239c5fa94ef1f748b8eaaf929a94cf07c2/nvidia_nat_opentelemetry-1.4.0-py3-none-any.whl", hash = "sha256:20430d00ace474eac2c348631fb20a5f3b48af9f7da033083bfdee69d4da5950", size = 67395, upload-time = "2026-02-03T03:08:15.838Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fa/8c7f045dde7242e6213829ecc8029bfbda56f62790451e27e99b01333eb9/nvidia_nat_opentelemetry-1.5.0-py3-none-any.whl", hash = "sha256:32d835ecc0385fbfaaef1197fd296e8d61a25cd57a2b23bbb500f6ee48d95e41", size = 67552, upload-time = "2026-03-12T21:08:01.709Z" }, ] [[package]] name = "nvidia-nat-phoenix" -version = "1.4.0" +version = "1.5.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/2b/c4/2b2a37a98615c36ee09b4e3168e175434f3ef1270d29046a7953b7389a3b/nvidia_nat_phoenix-1.4.0-py3-none-any.whl", hash = "sha256:bca503c5ef1455086aac2ec840751b6304d616b3eda49998f5bd0c9aee16d2e6", size = 53388, upload-time = "2026-02-03T03:20:35.1Z" }, -] - -[[package]] -name = "nvidia-nat-profiling" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "nvidia-nat" }, - { name = "prefixspan" }, - { name = "scikit-learn" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/1a/824445541e8274696ebdfbc5eb1dadbfd40e5337ab91e587a0de9838372d/nvidia_nat_profiling-1.4.0-py3-none-any.whl", hash = "sha256:b71c8cc8fd4866dada425477872b4895fa8bcea83bf787197fa88be39e878e7b", size = 47931, upload-time = "2026-02-03T03:08:34.369Z" }, + { url = "https://files.pythonhosted.org/packages/68/9e/388c4a417fadda0889f414bd1270412efa85f6175b6375bc52bc7b5793a1/nvidia_nat_phoenix-1.5.0-py3-none-any.whl", hash = "sha256:af68715cd30c318d5cc05bc0e6a99a46d6dfa4ad4ac68694115860314e7f6ce7", size = 53424, upload-time = "2026-03-12T21:05:55.397Z" }, ] [[package]] @@ -2382,6 +2094,21 @@ 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" }, ] +[[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, upload-time = "2025-12-18T04:09:03.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/68/162b0d273ffef5b0ad557ebccb790725bf94d78969702324dd5726828cf0/openevals-0.1.3-py3-none-any.whl", hash = "sha256:aed448df0cfdded732e24cda026eda065435a71ffb8c406a3ce73e590156d9f9", size = 67802, upload-time = "2025-12-18T04:09:01.59Z" }, +] + [[package]] name = "openinference-instrumentation" version = "0.1.42" @@ -2570,8 +2297,7 @@ 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 = "numpy" }, { name = "packaging" }, { name = "pyyaml" }, { name = "sqlalchemy" }, @@ -2582,51 +2308,27 @@ 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" }, ] -[[package]] -name = "ordered-set" -version = "4.1.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" } -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" }, -] - [[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" }, + { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" }, ] [[package]] @@ -2635,24 +2337,15 @@ version = "1.12.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" }, + { 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, upload-time = "2025-11-04T18:29:47.431Z" }, + { 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, upload-time = "2025-11-04T18:29:48.735Z" }, + { 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, upload-time = "2025-11-04T18:29:49.99Z" }, + { 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, upload-time = "2025-11-04T18:29:51.007Z" }, + { 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, upload-time = "2025-11-04T18:29:52.103Z" }, + { 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, upload-time = "2025-11-04T18:29:53.336Z" }, + { 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, upload-time = "2025-11-04T18:29:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/ce/15/429c72d64323503fd42cc4ca8398930ded8aa8b3470df8a86b3bbae7a35c/ormsgpack-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ed37f29772432048b58174e920a1d4c4cde0404a5d448d3d8bbcc95d86a6918", size = 112949, upload-time = "2025-11-04T18:29:55.371Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/e72c451a40f8c57bfc229e0b8e536ecea7203c8f0a839676df2ffb605c62/ormsgpack-1.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:b03994bbec5d6d42e03d6604e327863f885bde67aa61e06107ce1fa5bdd3e71d", size = 106689, upload-time = "2025-11-04T18:29:56.262Z" }, ] [[package]] @@ -2669,28 +2362,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" } 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, upload-time = "2025-09-29T23:21:05.024Z" }, + { 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, upload-time = "2025-09-29T23:21:15.979Z" }, + { 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, upload-time = "2025-09-29T23:21:27.165Z" }, + { 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, upload-time = "2025-09-29T23:21:40.532Z" }, + { 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, upload-time = "2025-09-29T23:21:55.77Z" }, + { 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, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { 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, upload-time = "2025-09-29T23:22:24.343Z" }, + { 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, upload-time = "2025-09-29T23:22:37.762Z" }, + { 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, upload-time = "2025-09-29T23:22:51.688Z" }, + { 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, upload-time = "2025-09-29T23:23:05.042Z" }, + { 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, upload-time = "2025-09-29T23:23:28.57Z" }, + { 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, upload-time = "2025-09-29T23:24:24.876Z" }, ] [[package]] @@ -2725,35 +2416,31 @@ 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" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, ] [[package]] @@ -2848,36 +2535,36 @@ 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/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { 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, upload-time = "2025-10-08T19:47:08.851Z" }, + { 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, upload-time = "2025-10-08T19:47:09.982Z" }, + { 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, upload-time = "2025-10-08T19:47:11.319Z" }, + { 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, upload-time = "2025-10-08T19:47:13.146Z" }, + { 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, upload-time = "2025-10-08T19:47:14.913Z" }, + { 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, upload-time = "2025-10-08T19:47:16.277Z" }, + { 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, upload-time = "2025-10-08T19:47:17.962Z" }, + { 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, upload-time = "2025-10-08T19:47:19.355Z" }, + { 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, upload-time = "2025-10-08T19:47:21.338Z" }, + { 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, upload-time = "2025-10-08T19:47:23.059Z" }, + { 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, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { 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, upload-time = "2025-10-08T19:47:29.445Z" }, + { 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, upload-time = "2025-10-08T19:47:30.579Z" }, + { 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, upload-time = "2025-10-08T19:47:31.79Z" }, + { 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, upload-time = "2025-10-08T19:47:33.481Z" }, + { 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, upload-time = "2025-10-08T19:47:34.906Z" }, + { 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, upload-time = "2025-10-08T19:47:36.338Z" }, + { 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, upload-time = "2025-10-08T19:47:37.692Z" }, + { 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, upload-time = "2025-10-08T19:47:39.659Z" }, + { 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, upload-time = "2025-10-08T19:47:41.084Z" }, + { 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, upload-time = "2025-10-08T19:47:42.51Z" }, + { 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, upload-time = "2025-10-08T19:47:43.927Z" }, + { 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, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, { 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" }, ] @@ -2902,6 +2589,12 @@ 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" } 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, upload-time = "2025-11-02T12:25:58.161Z" }, + { 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, upload-time = "2025-11-02T12:26:00.491Z" }, + { 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, upload-time = "2025-11-02T12:26:02.613Z" }, + { 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, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, { 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" }, @@ -2916,20 +2609,20 @@ 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" } 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, upload-time = "2025-10-24T10:06:20.274Z" }, + { 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, upload-time = "2025-10-24T10:06:27.301Z" }, + { 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, upload-time = "2025-10-24T10:06:35.387Z" }, + { 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, upload-time = "2025-10-24T10:06:43.551Z" }, + { 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, upload-time = "2025-10-24T10:06:52.284Z" }, + { 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, upload-time = "2025-10-24T10:07:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, + { 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, upload-time = "2025-10-24T10:07:11.47Z" }, + { 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, upload-time = "2025-10-24T10:07:18.626Z" }, + { 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, upload-time = "2025-10-24T10:07:26.002Z" }, + { 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, upload-time = "2025-10-24T10:07:34.09Z" }, + { 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, upload-time = "2025-10-24T10:07:43.528Z" }, + { 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, upload-time = "2025-10-24T10:07:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, ] [[package]] @@ -2983,50 +2676,20 @@ dependencies = [ ] 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" }, + { 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, upload-time = "2025-11-04T13:40:25.248Z" }, + { 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, upload-time = "2025-11-04T13:40:27.099Z" }, + { 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, upload-time = "2025-11-04T13:40:29.806Z" }, + { 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, upload-time = "2025-11-04T13:40:33.544Z" }, + { 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, upload-time = "2025-11-04T13:40:35.479Z" }, + { 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, upload-time = "2025-11-04T13:40:37.436Z" }, + { 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, upload-time = "2025-11-04T13:40:40.289Z" }, + { 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, upload-time = "2025-11-04T13:40:42.809Z" }, + { 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, upload-time = "2025-11-04T13:40:44.752Z" }, + { 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, upload-time = "2025-11-04T13:40:46.66Z" }, + { 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, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, ] [[package]] @@ -3077,6 +2740,15 @@ 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" }, ] +[[package]] +name = "pyjwt" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" }, +] + [[package]] name = "pylint" version = "3.3.9" @@ -3163,7 +2835,7 @@ name = "pytest-cov" version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -3220,11 +2892,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" 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/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } 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/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -3242,25 +2914,16 @@ version = "6.0.3" 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" } 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/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { 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, upload-time = "2025-09-25T21:32:25.149Z" }, + { 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, upload-time = "2025-09-25T21:32:26.575Z" }, + { 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, upload-time = "2025-09-25T21:32:27.727Z" }, + { 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, upload-time = "2025-09-25T21:32:28.878Z" }, + { 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, upload-time = "2025-09-25T21:32:30.178Z" }, + { 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, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] [[package]] @@ -3276,8 +2939,7 @@ dependencies = [ { 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 = "numpy" }, { name = "openai" }, { name = "pydantic" }, { name = "tiktoken" }, @@ -3287,19 +2949,6 @@ 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" }, ] -[[package]] -name = "rank-bm25" -version = "0.2.2" -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" } -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" }, -] - [[package]] name = "referencing" version = "0.37.0" @@ -3307,7 +2956,6 @@ 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" } wheels = [ @@ -3320,34 +2968,34 @@ 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" }, + { 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, upload-time = "2025-11-03T21:31:55.9Z" }, + { 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, upload-time = "2025-11-03T21:31:57.758Z" }, + { 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, upload-time = "2025-11-03T21:32:01.12Z" }, + { 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, upload-time = "2025-11-03T21:32:03.006Z" }, + { 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, upload-time = "2025-11-03T21:32:04.553Z" }, + { 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, upload-time = "2025-11-03T21:32:06.148Z" }, + { 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, upload-time = "2025-11-03T21:32:08.162Z" }, + { 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, upload-time = "2025-11-03T21:32:09.769Z" }, + { 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, upload-time = "2025-11-03T21:32:11.769Z" }, + { 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, upload-time = "2025-11-03T21:32:13.906Z" }, + { 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, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { 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, upload-time = "2025-11-03T21:32:23.466Z" }, + { 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, upload-time = "2025-11-03T21:32:25.034Z" }, + { 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, upload-time = "2025-11-03T21:32:26.528Z" }, + { 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, upload-time = "2025-11-03T21:32:28.363Z" }, + { 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, upload-time = "2025-11-03T21:32:30.4Z" }, + { 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, upload-time = "2025-11-03T21:32:32.123Z" }, + { 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, upload-time = "2025-11-03T21:32:34.305Z" }, + { 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, upload-time = "2025-11-03T21:32:36.364Z" }, + { 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, upload-time = "2025-11-03T21:32:38.042Z" }, + { 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, upload-time = "2025-11-03T21:32:40.079Z" }, + { 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, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, ] [[package]] @@ -3365,18 +3013,6 @@ 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" }, ] -[[package]] -name = "requests-futures" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { 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" } -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" }, -] - [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -3408,48 +3044,35 @@ 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" }, + { 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, upload-time = "2025-11-16T14:48:41.063Z" }, + { 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, upload-time = "2025-11-16T14:48:42.423Z" }, + { 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, upload-time = "2025-11-16T14:48:43.75Z" }, + { 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, upload-time = "2025-11-16T14:48:45.498Z" }, + { 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, upload-time = "2025-11-16T14:48:47.308Z" }, + { 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, upload-time = "2025-11-16T14:48:48.615Z" }, + { 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, upload-time = "2025-11-16T14:48:50.027Z" }, + { 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, upload-time = "2025-11-16T14:48:51.515Z" }, + { 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, upload-time = "2025-11-16T14:48:53.237Z" }, + { 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, upload-time = "2025-11-16T14:48:54.769Z" }, + { 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, upload-time = "2025-11-16T14:48:56.247Z" }, + { 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, upload-time = "2025-11-16T14:48:57.691Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, + { 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, upload-time = "2025-11-16T14:49:04.133Z" }, + { 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, upload-time = "2025-11-16T14:49:05.543Z" }, + { 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, upload-time = "2025-11-16T14:49:07.301Z" }, + { 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, upload-time = "2025-11-16T14:49:08.782Z" }, + { 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, upload-time = "2025-11-16T14:49:10.253Z" }, + { 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, upload-time = "2025-11-16T14:49:12.478Z" }, + { 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, upload-time = "2025-11-16T14:49:13.88Z" }, + { 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, upload-time = "2025-11-16T14:49:15.339Z" }, + { 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, upload-time = "2025-11-16T14:49:16.832Z" }, + { 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, upload-time = "2025-11-16T14:49:18.342Z" }, + { 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, upload-time = "2025-11-16T14:49:19.767Z" }, + { 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, upload-time = "2025-11-16T14:49:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, ] [[package]] @@ -3470,23 +3093,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" } 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, upload-time = "2025-09-09T08:20:54.731Z" }, + { 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, upload-time = "2025-09-09T08:20:57.313Z" }, + { 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, upload-time = "2025-09-09T08:20:59.671Z" }, + { 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, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { 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, upload-time = "2025-09-09T08:21:06.381Z" }, + { 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, upload-time = "2025-09-09T08:21:08.628Z" }, + { 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, upload-time = "2025-09-09T08:21:11.486Z" }, + { 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, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, ] [[package]] @@ -3494,31 +3116,30 @@ 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" } 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, upload-time = "2025-10-28T17:33:31.375Z" }, + { 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, upload-time = "2025-10-28T17:33:36.516Z" }, + { 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, upload-time = "2025-10-28T17:33:42.094Z" }, + { 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, upload-time = "2025-10-28T17:33:48.483Z" }, + { 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, upload-time = "2025-10-28T17:33:56.495Z" }, + { 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, upload-time = "2025-10-28T17:34:04.722Z" }, + { 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, upload-time = "2025-10-28T17:34:13.482Z" }, + { 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, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { 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, upload-time = "2025-10-28T17:34:31.451Z" }, + { 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, upload-time = "2025-10-28T17:34:39.087Z" }, + { 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, upload-time = "2025-10-28T17:34:45.234Z" }, + { 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, upload-time = "2025-10-28T17:34:51.793Z" }, + { 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, upload-time = "2025-10-28T17:34:59.8Z" }, + { 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, upload-time = "2025-10-28T17:35:08.173Z" }, + { 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, upload-time = "2025-10-28T17:35:16.7Z" }, + { 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, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, ] [[package]] @@ -3616,22 +3237,14 @@ dependencies = [ ] 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/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, + { 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, upload-time = "2025-10-10T16:03:41.755Z" }, + { 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, upload-time = "2025-10-10T15:35:31.221Z" }, + { 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, upload-time = "2025-10-10T15:43:50.124Z" }, + { 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, upload-time = "2025-10-10T15:35:32.578Z" }, + { 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, upload-time = "2025-10-10T15:43:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, { 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" }, ] @@ -3646,30 +3259,24 @@ 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" } 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/c4/d3/30a7dc9f6030ea2a1c76fcbb220d1f5d95a16f22f50d4cd80c8778ca5018/sqlean_py-3.49.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:ba8e5fc5b9d6682f3fbb626e1f936a4c4ac7377c3d044beaef695e6306a9a39b", size = 1129238, upload-time = "2025-05-02T11:58:04.085Z" }, + { 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, upload-time = "2025-05-02T11:58:06.153Z" }, + { 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, upload-time = "2025-05-02T11:58:07.54Z" }, + { 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, upload-time = "2025-05-02T11:58:09.012Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d6/455e75e1e540b64e230f7282ec55777e030190a486e98a2fce5d6437afa2/sqlean_py-3.49.1-cp313-cp313-win_amd64.whl", hash = "sha256:de820bdb39729044f9ed94f0addfe308b020479334146036a773f470d7594d87", size = 804300, upload-time = "2025-05-02T11:58:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/b63830855455fd22278ddc78cc7c64dffb5e1a69c15245c18275317ae9d5/sqlean_py-3.49.1-cp313-cp313-win_arm64.whl", hash = "sha256:3c1661f2fcf4d10ec3940ef8d2146bb58260b409c9033f7a727a6962e2032b7c", size = 739448, upload-time = "2025-05-02T11:58:12.458Z" }, ] [[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, upload-time = "2026-01-18T13:34:11.062Z" } 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, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] @@ -3697,21 +3304,17 @@ wheels = [ ] [[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, upload-time = "2026-03-09T19:17:32.333Z" } 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, upload-time = "2026-03-09T19:17:29.593Z" }, ] [[package]] @@ -3751,20 +3354,20 @@ dependencies = [ ] 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" } 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/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { 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, upload-time = "2025-10-06T20:22:02.788Z" }, + { 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, upload-time = "2025-10-06T20:22:03.814Z" }, + { 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, upload-time = "2025-10-06T20:22:05.173Z" }, + { 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, upload-time = "2025-10-06T20:22:06.265Z" }, + { 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, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { 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, upload-time = "2025-10-06T20:22:10.939Z" }, + { 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, upload-time = "2025-10-06T20:22:12.115Z" }, + { 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, upload-time = "2025-10-06T20:22:13.541Z" }, + { 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, upload-time = "2025-10-06T20:22:14.559Z" }, + { 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, upload-time = "2025-10-06T20:22:15.545Z" }, + { 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, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, ] [[package]] @@ -3792,31 +3395,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "tomli" -version = "2.3.0" -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" } -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" }, -] - [[package]] name = "tomlkit" version = "0.13.3" @@ -3866,67 +3444,6 @@ 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" }, ] -[[package]] -name = "traitlets" -version = "5.14.3" -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" }, -] - -[[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" } -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" }, -] - -[[package]] -name = "tree-sitter-languages" -version = "1.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tree-sitter" }, -] -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" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -3985,18 +3502,6 @@ 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" }, ] -[[package]] -name = "url-normalize" -version = "1.4.3" -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" } -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" }, -] - [[package]] name = "urllib3" version = "2.6.3" @@ -4026,13 +3531,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, - { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" }, - { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" }, ] [[package]] @@ -4065,18 +3563,12 @@ 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" } 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, upload-time = "2025-10-16T22:16:36.833Z" }, + { 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, upload-time = "2025-10-16T22:16:38.275Z" }, + { 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, upload-time = "2025-10-16T22:16:39.375Z" }, + { 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, upload-time = "2025-10-16T22:16:40.547Z" }, + { 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, upload-time = "2025-10-16T22:16:41.694Z" }, + { 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, upload-time = "2025-10-16T22:16:43.224Z" }, ] [[package]] @@ -4084,31 +3576,20 @@ name = "vuln-analysis" source = { editable = "." } dependencies = [ { name = "aiofiles" }, - { name = "aiohttp-client-cache" }, { name = "aiolimiter" }, - { name = "aioresponses" }, { name = "arize-phoenix" }, { name = "bokeh" }, - { name = "brotli" }, - { name = "esprima" }, - { name = "faiss-cpu" }, { name = "filelock" }, { name = "gitpython" }, - { name = "google-search-results" }, - { name = "json5" }, + { name = "httpx" }, { name = "langchain" }, - { name = "langchain-classic" }, { name = "langchain-core" }, - { name = "nbformat" }, - { name = "nemollm" }, - { name = "nvidia-nat", extra = ["async-endpoints", "langchain", "phoenix", "profiling"] }, + { name = "langgraph" }, + { name = "nvidia-nat", extra = ["async-endpoints", "eval", "langchain", "phoenix", "profiling"] }, { 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" }, ] @@ -4133,31 +3614,20 @@ 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 = "arize-phoenix" }, { name = "bokeh", specifier = ">=3.1.0" }, - { name = "brotli" }, - { name = "esprima" }, - { name = "faiss-cpu", specifier = "==1.9.0" }, { name = "filelock" }, { name = "gitpython" }, - { name = "google-search-results", specifier = "==2.4" }, - { name = "json5" }, + { name = "httpx", specifier = ">=0.27" }, { name = "langchain" }, - { name = "langchain-classic" }, { name = "langchain-core" }, - { name = "nbformat" }, - { name = "nemollm" }, - { name = "nvidia-nat", extras = ["async-endpoints", "langchain", "phoenix", "profiling"], specifier = "~=1.4.0" }, + { name = "langgraph", specifier = ">=1.0.5,<2.0.0" }, + { name = "nvidia-nat", extras = ["async-endpoints", "eval", "langchain", "phoenix", "profiling"], specifier = "==1.5.0" }, { name = "openinference-instrumentation-langchain" }, - { name = "ordered-set" }, + { 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" }, ] @@ -4188,36 +3658,29 @@ dependencies = [ ] 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" }, + { 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, upload-time = "2025-10-14T15:05:02.063Z" }, + { 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, upload-time = "2025-10-14T15:05:03.052Z" }, + { 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, upload-time = "2025-10-14T15:05:04.004Z" }, + { 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, upload-time = "2025-10-14T15:05:04.942Z" }, + { 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, upload-time = "2025-10-14T15:05:05.905Z" }, + { 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, upload-time = "2025-10-14T15:05:06.906Z" }, + { 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, upload-time = "2025-10-14T15:05:07.906Z" }, + { 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, upload-time = "2025-10-14T15:05:09.138Z" }, + { 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, upload-time = "2025-10-14T15:05:10.117Z" }, + { 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, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { 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, upload-time = "2025-10-14T15:05:15.777Z" }, + { 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, upload-time = "2025-10-14T15:05:16.85Z" }, + { 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, upload-time = "2025-10-14T15:05:17.876Z" }, + { 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, upload-time = "2025-10-14T15:05:18.962Z" }, + { 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, upload-time = "2025-10-14T15:05:20.094Z" }, + { 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, upload-time = "2025-10-14T15:05:21.134Z" }, + { 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, upload-time = "2025-10-14T15:05:22.306Z" }, + { 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, upload-time = "2025-10-14T15:05:23.348Z" }, + { 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, upload-time = "2025-10-14T15:05:24.398Z" }, + { 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, upload-time = "2025-10-14T15:05:25.45Z" }, ] [[package]] @@ -4226,28 +3689,17 @@ 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/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { 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, upload-time = "2025-03-05T20:02:37.985Z" }, + { 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, upload-time = "2025-03-05T20:02:39.298Z" }, + { 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, upload-time = "2025-03-05T20:02:40.595Z" }, + { 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, upload-time = "2025-03-05T20:02:41.926Z" }, + { 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, upload-time = "2025-03-05T20:02:43.304Z" }, + { 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, upload-time = "2025-03-05T20:02:48.812Z" }, + { 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, upload-time = "2025-03-05T20:02:50.14Z" }, + { 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, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, { 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" }, ] @@ -4279,26 +3731,16 @@ 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/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { 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, upload-time = "2025-08-12T05:51:37.156Z" }, + { 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, upload-time = "2025-08-12T05:51:58.425Z" }, + { 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, upload-time = "2025-08-12T05:52:37.53Z" }, + { 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, upload-time = "2025-08-12T05:52:15.886Z" }, + { 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, upload-time = "2025-08-12T05:52:17.914Z" }, + { 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, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, { 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" }, ] @@ -4308,41 +3750,36 @@ 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" }, + { 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, upload-time = "2025-10-02T14:34:55.839Z" }, + { 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, upload-time = "2025-10-02T14:34:57.219Z" }, + { 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, upload-time = "2025-10-02T14:34:59.21Z" }, + { 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, upload-time = "2025-10-02T14:35:00.816Z" }, + { 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, upload-time = "2025-10-02T14:35:02.207Z" }, + { 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, upload-time = "2025-10-02T14:35:03.61Z" }, + { 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, upload-time = "2025-10-02T14:35:05.013Z" }, + { 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, upload-time = "2025-10-02T14:35:06.239Z" }, + { 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, upload-time = "2025-10-02T14:35:07.735Z" }, + { 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, upload-time = "2025-10-02T14:35:09.438Z" }, + { 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, upload-time = "2025-10-02T14:35:11.162Z" }, + { 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, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { 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, upload-time = "2025-10-02T14:35:17.413Z" }, + { 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, upload-time = "2025-10-02T14:35:18.844Z" }, + { 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, upload-time = "2025-10-02T14:35:20.31Z" }, + { 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, upload-time = "2025-10-02T14:35:21.616Z" }, + { 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, upload-time = "2025-10-02T14:35:23.32Z" }, + { 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, upload-time = "2025-10-02T14:35:25.111Z" }, + { 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, upload-time = "2025-10-02T14:35:26.586Z" }, + { 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, upload-time = "2025-10-02T14:35:28.087Z" }, + { 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, upload-time = "2025-10-02T14:35:29.857Z" }, + { 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, upload-time = "2025-10-02T14:35:31.222Z" }, + { 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, upload-time = "2025-10-02T14:35:32.517Z" }, + { 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, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, ] [[package]] @@ -4377,38 +3814,38 @@ dependencies = [ ] 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/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { 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, upload-time = "2025-10-06T14:10:16.115Z" }, + { 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, upload-time = "2025-10-06T14:10:17.993Z" }, + { 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, upload-time = "2025-10-06T14:10:19.44Z" }, + { 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, upload-time = "2025-10-06T14:10:21.124Z" }, + { 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, upload-time = "2025-10-06T14:10:22.902Z" }, + { 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, upload-time = "2025-10-06T14:10:24.523Z" }, + { 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, upload-time = "2025-10-06T14:10:26.406Z" }, + { 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, upload-time = "2025-10-06T14:10:28.461Z" }, + { 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, upload-time = "2025-10-06T14:10:30.541Z" }, + { 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, upload-time = "2025-10-06T14:10:33.352Z" }, + { 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, upload-time = "2025-10-06T14:10:35.034Z" }, + { 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, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { 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, upload-time = "2025-10-06T14:10:44.643Z" }, + { 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, upload-time = "2025-10-06T14:10:46.554Z" }, + { 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, upload-time = "2025-10-06T14:10:48.007Z" }, + { 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, upload-time = "2025-10-06T14:10:49.997Z" }, + { 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, upload-time = "2025-10-06T14:10:52.004Z" }, + { 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, upload-time = "2025-10-06T14:10:54.078Z" }, + { 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, upload-time = "2025-10-06T14:10:55.767Z" }, + { 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, upload-time = "2025-10-06T14:10:57.985Z" }, + { 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, upload-time = "2025-10-06T14:10:59.633Z" }, + { 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, upload-time = "2025-10-06T14:11:01.454Z" }, + { 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, upload-time = "2025-10-06T14:11:03.452Z" }, + { 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, upload-time = "2025-10-06T14:11:05.115Z" }, + { 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, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, { 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" }, ] @@ -4439,36 +3876,20 @@ dependencies = [ ] 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" }, + { 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, upload-time = "2024-07-15T00:16:16.005Z" }, + { 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, upload-time = "2024-07-15T00:16:17.897Z" }, + { 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, upload-time = "2024-07-15T00:16:20.136Z" }, + { 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, upload-time = "2024-07-15T00:16:23.398Z" }, + { 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, upload-time = "2024-07-15T00:16:26.391Z" }, + { 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, upload-time = "2024-07-15T00:16:29.018Z" }, + { 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, upload-time = "2024-07-15T00:16:31.871Z" }, + { 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, upload-time = "2024-07-15T00:16:34.593Z" }, + { 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, upload-time = "2024-07-15T00:16:36.887Z" }, + { 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, upload-time = "2024-07-15T00:16:39.709Z" }, + { 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, upload-time = "2024-07-15T00:16:41.83Z" }, + { 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, upload-time = "2024-07-15T00:16:44.287Z" }, + { 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, upload-time = "2024-07-15T00:16:46.423Z" }, + { 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, upload-time = "2024-07-15T00:16:49.053Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, ] From 656b11ae8dfbe5f8484d6d97c0dc1a98ac14724e Mon Sep 17 00:00:00 2001 From: Aryan Saboo Date: Mon, 16 Mar 2026 18:46:17 -0700 Subject: [PATCH 12/48] Fix Go binary detection and VEX requires_configuration override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem 1: Go binaries not scanned in containers (Dockerfile + package_tools.py)** `find_go_binaries` relies on `strings` (from binutils) to detect Go binaries and extract embedded module tables. `binutils` was not installed in the Docker image, so the tool silently failed — any CVE affecting a Go module compiled into a container binary (e.g., golang-jwt in etcd) was invisible to the agent. Changes: - Add `binutils` to the `apt-get install` block in the Dockerfile so `strings` and `readelf` are available at runtime. - Add `_extract_go_modules_via_strings()` fallback in `package_tools.py`: when `go version -m` is unavailable or times out, parse the embedded Go buildinfo block directly from the binary using `strings -n 10`. Go statically compiles a module table with `dep\t\t` lines that are readable as plain ASCII without the Go toolchain. - Raise `MAX_FILES` in `find_go_binaries` from 5,000 to 100,000 to handle large container filesystems (e.g., deepseek-r1 has ~80k files). **Problem 2: VEX categorizer incorrectly assigning requires_configuration (vex_categorizer.py)** When the exploitability analyzer returns CONDITIONALLY EXPLOITABLE with a default-off feature flag as the only barrier, the VEX categorizer was assigning `requires_configuration` even when: (a) the flag is a documented, user-facing product feature, and (b) the CVE advisory explicitly marks the installed version as affected. Root cause: VEX Step 4 in the decision flowchart directed the agent straight to `requires_configuration` for any default-off toggle. The override rule ("if the advisory considers the version affected, use `vulnerable`") existed in the guidance section below but was consistently skipped. Fix: Surface the override check inline at VEX Step 4 — the exact point where the agent makes the call — so it cannot be bypassed. No new logic was added; this duplicates an existing rule at the decision point for visibility. Validated against CVE-2025-62164 / llm-nim_1.15.2: previously returned `not_affected / requires_configuration`, now correctly returns `affected / vulnerable`. Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 5 ++- src/vuln_analysis/prompts/vex_categorizer.py | 8 ++-- src/vuln_analysis/tools/package_tools.py | 39 ++++++++++++++++++-- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index bb2d2f1e6..699e0bebf 100755 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ ARG BASE_IMAGE_URL=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 @@ -28,6 +28,7 @@ ARG PYTHON_VERSION ENV PYTHONDONTWRITEBYTECODE=1 RUN apt-get update && apt-get install -y \ + binutils \ ca-certificates \ curl \ git \ @@ -57,7 +58,7 @@ COPY ./ /workspace 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 --prerelease=allow # Activate the environment (make it default for subsequent commands) RUN echo "source /workspace/.venv/bin/activate" >> ~/.bashrc diff --git a/src/vuln_analysis/prompts/vex_categorizer.py b/src/vuln_analysis/prompts/vex_categorizer.py index 1fcf76f78..949b47714 100644 --- a/src/vuln_analysis/prompts/vex_categorizer.py +++ b/src/vuln_analysis/prompts/vex_categorizer.py @@ -168,9 +168,11 @@ 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 → **`requires_configuration`** (status: `not_affected`) - See `requires_configuration` guidance in the VERDICT-TO-VEX MAPPING section for important - nuances on what qualifies as "non-default configuration." +- 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) — diff --git a/src/vuln_analysis/tools/package_tools.py b/src/vuln_analysis/tools/package_tools.py index 395836987..1497f0b0c 100644 --- a/src/vuln_analysis/tools/package_tools.py +++ b/src/vuln_analysis/tools/package_tools.py @@ -322,6 +322,37 @@ async def find_shared_libraries(library_pattern: str, search_path: str | None = 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 --- @@ -344,7 +375,7 @@ async def find_go_binaries(search_path: str) -> str: fs = get_filesystem_root() go_binaries: list[Path] = [] files_checked = 0 - MAX_FILES = 5000 + MAX_FILES = 100_000 try: for candidate in resolved.rglob("*"): if files_checked >= MAX_FILES: @@ -394,11 +425,11 @@ async def find_go_binaries(search_path: str) -> str: if result.returncode == 0 and result.stdout.strip(): lines.append(result.stdout.strip()[:3000]) else: - lines.append("(go version -m failed)") + lines.append(_extract_go_modules_via_strings(binary, timeout)) except (FileNotFoundError, subprocess.TimeoutExpired): - lines.append("(go command not available or timed out)") + lines.append(_extract_go_modules_via_strings(binary, timeout)) else: - lines.append("('go' command not found)") + lines.append(_extract_go_modules_via_strings(binary, timeout)) if len(go_binaries) > 10: lines.append(f"\n[... {len(go_binaries) - 10} more Go binaries not shown ...]") return "\n".join(lines) From fe643566a3920f59b60988ce78f3ba9ce40b3ac9 Mon Sep 17 00:00:00 2001 From: Aryan Saboo Date: Fri, 3 Apr 2026 07:37:57 -0700 Subject: [PATCH 13/48] =?UTF-8?q?Separate=20Go=20binary=20walk=20limit=20f?= =?UTF-8?q?rom=20extraction=20limit=20(MAX=5FFILES=20=E2=86=92=20MAX=5FFIL?= =?UTF-8?q?ES=5FTO=5FWALK=20+=20MAX=5FGO=5FBINARIES)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/eval_datasets/pb_eval_expanded.csv | 29 ++++++++++ .../data/eval_datasets/pb_eval_full.csv | 58 +++++++++++++++++++ src/vuln_analysis/tools/package_tools.py | 15 +++-- 3 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_expanded.csv create mode 100644 src/vuln_analysis/data/eval_datasets/pb_eval_full.csv diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_expanded.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_expanded.csv new file mode 100644 index 000000000..937e82372 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_expanded.csv @@ -0,0 +1,29 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2024-11393,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2024-11394,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2024-9052,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,runtime_protected +CVE-2025-22869,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,code_not_reachable +CVE-2025-29783,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,code_not_present +CVE-2025-32444,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,code_not_reachable +CVE-2025-24357,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,runtime_protected +CVE-2024-9052,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,runtime_protected +CVE-2025-32444,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_reachable +CVE-2025-30204,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,vulnerable +CVE-2025-24357,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,runtime_protected +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,vulnerable +CVE-2025-6242,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +CVE-2025-59425,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2024-10907,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2024-10912,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2024-11603,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2025-48956,nvcr.io/nim/meta/llama-3.1-70b-instruct:1.13.1,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/image_config.json,code_not_reachable +CVE-2025-62372,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,code_not_reachable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/riva-translate-4b-instruct-v1.1:latest,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/image_config.json,vulnerable +CVE-2023-48022,nvcr.io/nvidia/nemo-microservices/customizer:25.10,/raid/hsinc/nspect_pb_container_data/customizer_25.10/filesystem,/raid/hsinc/nspect_pb_container_data/customizer_25.10/image_config.json,code_not_reachable +CVE-2024-9053,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2024-11041,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,code_not_present diff --git a/src/vuln_analysis/data/eval_datasets/pb_eval_full.csv b/src/vuln_analysis/data/eval_datasets/pb_eval_full.csv new file mode 100644 index 000000000..9195d89d4 --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/pb_eval_full.csv @@ -0,0 +1,58 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2023-48022,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +GHSA-5vqr-wprc-cpp7,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2025-62372,nvcr.io/nvidia/tritonserver-pb25h2:25.08.03-vllm-python-py3-stig-fips-x86,/raid/hsinc/nspect_pb_container_data/tritonserver-pb25h2_25.08.03-vllm-python-py3-stig-fips-x86/filesystem,/raid/hsinc/nspect_pb_container_data/tritonserver-pb25h2_25.08.03-vllm-python-py3-stig-fips-x86/image_config.json,code_not_reachable +CVE-2024-11392,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2025-66418,nvcr.io/nim/nvidia/riva-translate-4b-instruct-v1.1:latest,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/image_config.json,vulnerable +CVE-2025-30204,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,vulnerable +CVE-2025-57809,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-62164,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2024-48921,nvcr.io/nvidia/nemo-microservices/dgxc-admission-controller:1.801.0,/raid/hsinc/nspect_pb_container_data/dgxc-admission-controller_1.801.0/filesystem,/raid/hsinc/nspect_pb_container_data/dgxc-admission-controller_1.801.0/image_config.json,vulnerable +CVE-2025-47287,nvcr.io/nvidia/rapids-pb24h2:24.06.06-runtime,/raid/hsinc/nspect_pb_container_data/rapids-pb24h2_24.06.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/rapids-pb24h2_24.06.06-runtime/image_config.json,vulnerable +CVE-2024-11041,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2025-9900,nvcr.io/nvidia/holoscan-ltsb2:23.10.08-lws2.3.1-dgpu,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/filesystem,/raid/hsinc/nspect_pb_container_data/holoscan-ltsb2_23.10.08-lws2.3.1-dgpu/image_config.json,vulnerable +CVE-2025-57809,nvcr.io/nim/meta/llama-3.1-70b-instruct:1.13.1,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2024-9053,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2025-29783,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_present +CVE-2024-2961,nvcr.io/nvidia/gpu-operator:v24.3.0,/raid/hsinc/nspect_pb_container_data/gpu-operator_v24.3.0/filesystem,/raid/hsinc/nspect_pb_container_data/gpu-operator_v24.3.0/image_config.json,requires_configuration +CVE-2024-56171,nvcr.io/nvidia/vgpu/vgpu-guest-driver-6:570.124.06-rhel8.8,/raid/hsinc/nspect_pb_container_data/vgpu-guest-driver-6_570.124.06-rhel8.8/filesystem,/raid/hsinc/nspect_pb_container_data/vgpu-guest-driver-6_570.124.06-rhel8.8/image_config.json,code_not_reachable +CVE-2024-5187,nvcr.io/nim/defog/llama-3-sqlcoder-8b:latest,/raid/hsinc/nspect_pb_container_data/llama-3-sqlcoder-8b_latest/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3-sqlcoder-8b_latest/image_config.json,code_not_reachable +CVE-2024-6387,nvcr.io/nim/meta/llama-2-70b-chat:1,/raid/hsinc/nspect_pb_container_data/llama-2-70b-chat_1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-2-70b-chat_1/image_config.json,code_not_reachable +CVE-2025-30165,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,code_not_reachable +CVE-2024-12376,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2025-52881,nvcr.io/nvidia/mellanox/plugins:network-operator-v25.10.0,/raid/hsinc/nspect_pb_container_data/plugins_network-operator-v25.10.0/filesystem,/raid/hsinc/nspect_pb_container_data/plugins_network-operator-v25.10.0/image_config.json,code_not_reachable +CVE-2024-37059,nvcr.io/nvidia/nemo-microservices/customizer:25.10,/raid/hsinc/nspect_pb_container_data/customizer_25.10/filesystem,/raid/hsinc/nspect_pb_container_data/customizer_25.10/image_config.json,code_not_reachable +CVE-2025-22869,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_reachable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.2-3b-instruct:1.8.3,/raid/hsinc/nspect_pb_container_data/llama-3.2-3b-instruct_1.8.3/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.2-3b-instruct_1.8.3/image_config.json,runtime_protected +CVE-2024-9052,nvcr.io/nim/nvidia/llama-3.3-nemotron-super-49b-v1:1.8.3,/raid/hsinc/nspect_pb_container_data/llama-3.3-nemotron-super-49b-v1_1.8.3/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.3-nemotron-super-49b-v1_1.8.3/image_config.json,runtime_protected +CVE-2024-9053,nvcr.io/nvidia/tritonserver-pb24h2:24.08.07-vllm-python-py3,/raid/hsinc/nspect_pb_container_data/tritonserver-pb24h2_24.08.07-vllm-python-py3/filesystem,/raid/hsinc/nspect_pb_container_data/tritonserver-pb24h2_24.08.07-vllm-python-py3/image_config.json,runtime_protected +CVE-2024-11393,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2024-11394,nvcr.io/nim/nvidia/riva-tts:1,/raid/hsinc/nspect_pb_container_data/riva-tts_1/filesystem,/raid/hsinc/nspect_pb_container_data/riva-tts_1/image_config.json,runtime_protected +CVE-2024-9052,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,runtime_protected +CVE-2025-22869,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,code_not_reachable +CVE-2025-29783,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,code_not_present +CVE-2025-32444,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,code_not_reachable +CVE-2025-24357,nvcr.io/nim/deepseek-ai/deepseek-r1:1,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_1/image_config.json,runtime_protected +CVE-2024-9052,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,runtime_protected +CVE-2025-32444,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,code_not_reachable +CVE-2025-30204,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,vulnerable +CVE-2025-24357,nvcr.io/nim/deepseek-ai/deepseek-r1:latest,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/deepseek-r1_latest/image_config.json,runtime_protected +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.12,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.12/image_config.json,vulnerable +CVE-2025-6242,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +CVE-2025-59425,nvcr.io/nim/qwen/qwen3-32b-dgx-spark:1.1.0-variant,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/filesystem,/raid/hsinc/nspect_pb_container_data/qwen3-32b-dgx-spark_1.1.0-variant/image_config.json,code_not_reachable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.13.1,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.13.1/image_config.json,vulnerable +CVE-2024-10907,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2024-10912,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2024-11603,nvcr.io/nvidia/nemo-microservices/auditor:25.08,/raid/hsinc/nspect_pb_container_data/auditor_25.08/filesystem,/raid/hsinc/nspect_pb_container_data/auditor_25.08/image_config.json,code_not_reachable +CVE-2025-48956,nvcr.io/nim/meta/llama-3.1-70b-instruct:1.13.1,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct_1.13.1/image_config.json,code_not_reachable +CVE-2025-62372,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,code_not_reachable +CVE-2025-66471,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-66418,nvcr.io/nim/nvidia/llm-nim:1.15.2,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/filesystem,/raid/hsinc/nspect_pb_container_data/llm-nim_1.15.2/image_config.json,vulnerable +CVE-2025-66471,nvcr.io/nim/nvidia/riva-translate-4b-instruct-v1.1:latest,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/filesystem,/raid/hsinc/nspect_pb_container_data/riva-translate-4b-instruct-v1.1_latest/image_config.json,vulnerable +CVE-2023-48022,nvcr.io/nvidia/nemo-microservices/customizer:25.10,/raid/hsinc/nspect_pb_container_data/customizer_25.10/filesystem,/raid/hsinc/nspect_pb_container_data/customizer_25.10/image_config.json,code_not_reachable +CVE-2024-9053,nvcr.io/nim/meta/llama-3.1-8b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-8b-instruct-pb24h2_1.3.6/image_config.json,code_not_present +CVE-2024-11041,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,vulnerable +CVE-2024-9052,nvcr.io/nim/meta/llama-3.1-70b-instruct-pb24h2:1.3.6,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/filesystem,/raid/hsinc/nspect_pb_container_data/llama-3.1-70b-instruct-pb24h2_1.3.6/image_config.json,code_not_present diff --git a/src/vuln_analysis/tools/package_tools.py b/src/vuln_analysis/tools/package_tools.py index 1497f0b0c..8223d96b7 100644 --- a/src/vuln_analysis/tools/package_tools.py +++ b/src/vuln_analysis/tools/package_tools.py @@ -358,11 +358,15 @@ def _extract_go_modules_via_strings(binary: Path, timeout: int = 30) -> str: 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.""" @@ -375,10 +379,9 @@ async def find_go_binaries(search_path: str) -> str: fs = get_filesystem_root() go_binaries: list[Path] = [] files_checked = 0 - MAX_FILES = 100_000 try: for candidate in resolved.rglob("*"): - if files_checked >= MAX_FILES: + if files_checked >= max_files_to_walk: break if not candidate.is_file(): continue @@ -404,11 +407,11 @@ async def find_go_binaries(search_path: str) -> str: except PermissionError: pass if not go_binaries: - note = f" (searched {files_checked} files" + (f", stopped at limit {MAX_FILES}" if files_checked >= MAX_FILES else "") + ")" + 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[:10]: + for binary in go_binaries[:max_go_binaries]: try: rel = "/" + str(binary.relative_to(fs)) if fs else str(binary) except ValueError: @@ -430,8 +433,8 @@ async def find_go_binaries(search_path: str) -> str: lines.append(_extract_go_modules_via_strings(binary, timeout)) else: lines.append(_extract_go_modules_via_strings(binary, timeout)) - if len(go_binaries) > 10: - lines.append(f"\n[... {len(go_binaries) - 10} more Go binaries not shown ...]") + 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") From e9730902bf183d70be2c479b2da2e65aa130cb7c Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Fri, 3 Apr 2026 08:42:14 -0700 Subject: [PATCH 14/48] Docker Compose and README updates --- Dockerfile | 4 +- README.md | 425 +++++++++++++++--- docker-compose.nim.yml | 27 +- docker-compose.yml | 54 +-- nginx/templates/routes/intel.conf.template | 40 +- nginx/templates/routes/nim.conf.template | 22 - .../template-variables.conf.template | 13 - .../configs/config-container-analyzer.yml | 6 +- .../configs/config-cve-researcher.yml | 4 +- src/vuln_analysis/configs/config-eval.yml | 12 +- .../config-exploitability-analyzer.yml | 2 +- .../configs/config-vex-categorizer.yml | 2 +- src/vuln_analysis/configs/config.yml | 12 +- src/vuln_analysis/data_models/input.py | 13 +- src/vuln_analysis/tools/web_tools.py | 125 ++++-- vulnerability-analysis.code-workspace | 1 - 16 files changed, 517 insertions(+), 245 deletions(-) diff --git a/Dockerfile b/Dockerfile index 699e0bebf..de12bde7c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG BASE_IMAGE_TAG=22.04_20240212 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 COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /uvx /bin/ @@ -58,7 +58,7 @@ COPY ./ /workspace 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 --prerelease=allow + uv sync # Activate the environment (make it default for subsequent commands) RUN echo "source /workspace/.venv/bin/activate" >> ~/.bashrc diff --git a/README.md b/README.md index d79ca4673..5d7b34ea8 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,406 @@ -# Vulnerability Analysis +# Container Vulnerability Analysis Pipeline +A multi-agent LLM system that performs end-to-end vulnerability analysis for container images. Given a CVE 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 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`**. -## Getting started +## Table of Contents -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +- [Architecture Overview](#architecture-overview) +- [How It Works](#how-it-works) +- [Agents](#agents) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Preparing Container Inputs](#preparing-container-inputs) +- [Usage](#usage) +- [Output Structure](#output-structure) +- [Benchmarking](#benchmarking) +- [Configuration](#configuration) +- [Caching](#caching) +- [Design Principles](#design-principles) +- [Project Structure](#project-structure) -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +## Architecture Overview -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +The system is composed of **four specialized agents** coordinated by a **pipeline orchestrator**: ``` -cd existing_repo -git remote add origin https://gitlab-master.nvidia.com/morpheus/agent-morpheus/vulnerability-analysis.git -git branch -M main -git push -uf origin main +┌─────────────────────────────────────────────────────────────────┐ +│ Pipeline Orchestrator │ +│ │ +│ Stage 1 (parallel) │ +│ ┌──────────────────────┐ ┌───────────────────────────────┐ │ +│ │ CVE Researcher │ │ Container Analyzer │ │ +│ │ (web research) │ │ (filesystem inspection) │ │ +│ └──────────┬───────────┘ └──────────────┬────────────────┘ │ +│ │ │ │ +│ └──────────┬───────────────────┘ │ +│ ▼ │ +│ Stage 2 ┌──────────────────────────────┐ │ +│ │ Exploitability Analyzer │ │ +│ │ (pre-triage → deep analysis)│ │ +│ └──────────────┬───────────────┘ │ +│ ▼ │ +│ Stage 3 ┌──────────────────────────────┐ │ +│ │ VEX Categorizer │ │ +│ │ (formal classification) │ │ +│ └──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ ``` -## Integrate with your tools +**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. + +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). + +## How It Works + +1. **Input**: A CVE/GHSA identifier, a container image name, and the path to that image's extracted filesystem (passed as a [`PipelineInput`](./src/vuln_analysis/data_models/input.py) JSON document to `nat run` / `nat serve`). +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 structured chain-of-thought reasoning, and produces a formal VEX status with justification. +5. **Output**: Per-agent Markdown reports and a structured VEX JSON classification. + +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. + +## Agents + +### 1. CVE Researcher + +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. + +**Tools:** `web_search` (Tavily), `web_fetch`, `osv_lookup` + +### 2. Container Analyzer + +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. -- [ ] [Set up project integrations](https://gitlab-master.nvidia.com/morpheus/agent-morpheus/vulnerability-analysis/-/settings/integrations) +**Architecture:** Two-phase with security isolation: -## Collaborate with your team +- **Phase 1 (Web Research)** — Searches public registries and vendor docs. Network access only, no filesystem access. Conditionally skipped for private images (via `--skip-web-research`). +- **Phase 2 (Filesystem Analysis)** — Inspects the extracted rootfs. Filesystem access only, no network access. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +**Tools:** Phase 1: `web_search`, `web_fetch` | Phase 2: `read_file`, `list_directory`, `grep_search`, `glob_find` -## Test and Deploy +### 3. Exploitability Analyzer -Use the built-in continuous integration in GitLab. +Determines whether a CVE is actually exploitable within the specific container context. Produces a verdict with confidence level. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +**Architecture:** Deterministic pre-triage followed by LLM deep analysis: -*** +- **Pre-Triage (deterministic, zero LLM calls)** — Parses reports, verifies package presence on the filesystem, checks version ranges, inspects distro backports. Can early-exit 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. -# Editing this README +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. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +**Verdicts:** `NOT APPLICABLE` · `ENVIRONMENT ISOLATED` · `NOT REACHABLE` · `MITIGATED` · `CONDITIONALLY EXPLOITABLE` · `EXPLOITABLE` -## Suggestions for a good README +**Tools:** `read_file`, `list_directory`, `grep_search`, `glob_find`, `check_binary_dependencies`, `search_binary_content`, `check_distro_patches` -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +### 4. VEX Categorizer -## Name -Choose a self-explaining name for your project. +Assigns a formal VEX status and justification by following a precedence-ordered decision flowchart. Reads the exploitability and container reports, reasons through each step using a structured chain-of-thought tool, and produces both a Markdown summary and structured JSON output. -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +**VEX Statuses:** -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +| 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 | -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +**Tools:** `reason` (structured chain-of-thought scratchpad) + +### Shared package layout (`src/vuln_analysis/`) + +Common building blocks for the NAT-registered workflow: + +| 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 | + +## Prerequisites + +- **Python 3.13+** (see [`pyproject.toml`](./pyproject.toml)) +- **[skopeo](https://github.com/containers/skopeo)** — for downloading and extracting container images +- **[jq](https://jqlang.github.io/jq/)** — optional but recommended, for parsing image manifests during extraction +- **NVIDIA API key** — NAT `nim` LLM configs use NVIDIA's OpenAI-compatible inference API (`NVIDIA_API_KEY`) +- **Tavily API key** — for web search capabilities ## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +```bash +pip install -e . +``` + +The repo root usually provides symlinks **`configs`** → `src/vuln_analysis/configs` and **`data`** → `src/vuln_analysis/data` so paths in the examples below resolve. + +Create a `.env` file in the project root (or export variables in your shell) for local runs: + +```env +NVIDIA_API_KEY=your-nvidia-api-key +NV_BASE_URL=https://inference-api.nvidia.com +TAVILY_API_KEY=your-tavily-api-key +``` + +A **`docker compose`** setup is included for containerized runs; the `vuln-analysis` service uses the same variables at runtime. + +## Preparing Container Inputs + +Before running the pipeline, you need an extracted container filesystem. Use [skopeo](https://github.com/containers/skopeo) to download the image and extract its layers into a merged rootfs: + +```bash +# 1. Download the image as an OCI directory +skopeo copy docker://nginx:latest oci:nginx_latest + +# 2. Extract and merge filesystem layers +# (extraction approach varies by tooling — the key requirement is a merged rootfs) +mkdir -p nginx_latest/filesystem +cd nginx_latest +for layer in blobs/sha256/*; do + tar -xf "$layer" -C filesystem 2>/dev/null || true +done +cd .. + +# 3. Optionally extract image config +skopeo inspect docker://nginx:latest > nginx_latest/image_config.json +``` + +The pipeline expects: + +- **`filesystem_path`**: path to the merged rootfs directory (e.g., `nginx_latest/filesystem`) +- **`image_config_path`** (optional): path to the image configuration JSON ## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +### Full Pipeline + +Run all four agents end-to-end via NAT: + +```bash +nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json +``` + +Edit the JSON so `filesystem_path` (and optional `image_config_path`) point at your unpacked image. + +**`PipelineInput` fields** (orchestrator flags in JSON form): + +| Field | Description | +|-------|-------------| +| `cve_id` | CVE identifier (at least one of `cve_id` or `ghsa_id` required) | +| `ghsa_id` | GitHub Security Advisory identifier | +| `package_hint` | Package name hint from the scanner | +| `version_hint` | Installed version hint from the scanner | +| `image_name` | Full container image name (required) | +| `filesystem_path` | Path to extracted container filesystem (required) | +| `image_config_path` | Path to `image_config.json` or `null` | +| `skip_web_research` | If `true`, skip container web research (for private images); same role as `--skip-web-research` in the standalone CLI | +| `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 | + +### Individual Agents + +Each agent can be run independently for targeted analysis or debugging: + +```bash +nat run --config_file=configs/config-cve-researcher.yml --input_file=data/examples/cve_researcher_input.json + +nat run --config_file=configs/config-container-analyzer.yml --input_file=data/examples/container_analyzer_input.json + +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 +``` + +### HTTP server + +```bash +nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 +``` + +Send a `POST` with a JSON body matching `PipelineInput` (see [NAT API server](https://docs.nvidia.com/nemo/agent-toolkit/latest/reference/api-server-endpoints.html) for endpoints). + +### Programmatic API + +Build a `PipelineInput` and pass it to NAT as JSON (e.g. via subprocess or HTTP). Example: + +```python +from vuln_analysis.data_models.input import PipelineInput + +inp = PipelineInput( + cve_id="CVE-2025-47273", + image_name="nvcr.io/nvidia/morpheus/morpheus:25.06-runtime", + filesystem_path="/path/to/filesystem", + output_dir="outputs", +) +print(inp.model_dump_json()) +``` + +Use `nat run --input_file=...` with that JSON, or invoke the registered workflow from your own NAT entrypoint. -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +## Output Structure -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +``` +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: + +```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" +} +``` + +## Benchmarking + +Labelled datasets live under **`data/eval_datasets/`**. Evaluations run through **`nat eval`** and [`configs/config-eval.yml`](./src/vuln_analysis/configs/config-eval.yml). -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +### Included Dataset: `morpheus_benchmark_2506.csv` -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +29 hand-labelled CVE × container cases covering two NVIDIA Morpheus 25.06 images: -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +| Image | Cases | +|-------|-------| +| `nvcr.io/nvidia/morpheus/morpheus:25.06-runtime` | 24 | +| `nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06` | 5 | -## License -For open source projects, say how it is licensed. +**Ground-truth label distribution:** + +| Label | Count | +|-------|-------| +| `code_not_reachable` | 15 | +| `vulnerable` | 13 | +| `code_not_present` | 1 | + +The dataset includes a mix of CVE and GHSA identifiers. Each case has been manually assessed to produce the ground-truth VEX justification label. + +### Running Benchmarks + +```bash +nat eval --config_file=configs/config-eval.yml +``` -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +Use `--override` to point at a specific CSV: + +```bash +nat eval --config_file=configs/config-eval.yml \ + --override eval.general.dataset.file_path data/eval_datasets/morpheus_benchmark_2506.csv +``` + +**Benchmark CSV format:** + +| Column | Description | +|--------|-------------| +| `cve_id` | CVE or GHSA identifier | +| `image_name` | Full container image name | +| `filesystem_path` | Path to extracted filesystem | +| `image_config_path` | Path to image config JSON | +| `ground_truth_label` | Expected VEX justification label | + +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) follow the evaluator definitions in that file. + +## Configuration + +### Environment Variables + +| 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`) | +| `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 | + +### Key Defaults + +| 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` | +| 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` | + +Per-agent settings (temperature, max tokens, max iterations, tool lists) are defined in the same YAML files (`config.yml` and `config-*.yml`). + +## Caching + +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. + +- **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` + +Cached reports are standard Markdown files in the output directory — they can be inspected, edited, or deleted manually. + +## 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 verdicts (`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** — Stage 1 agents run concurrently. Within each agent, multiple tool calls in a single LLM turn are dispatched in parallel via `asyncio.gather`. +- **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. Exponential-backoff retry handles transient API errors. Context window compaction manages long conversations. +- **Auditability** — The VEX Categorizer's `reason()` tool forces step-by-step chain-of-thought reasoning 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 +├── docker-compose.yml +├── docker-compose.nim.yml +├── pyproject.toml # nvidia-nat, dependencies, Python 3.13 +└── README.md +``` diff --git a/docker-compose.nim.yml b/docker-compose.nim.yml index 315d3a1d3..fc033f09a 100755 --- a/docker-compose.nim.yml +++ b/docker-compose.nim.yml @@ -21,10 +21,8 @@ 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} @@ -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/docker-compose.yml index 38d4bc020..a8fa576fa 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ 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: ./ @@ -34,45 +34,22 @@ services: 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 - # 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 # Anonymous volume prevents host .venv from being mounted @@ -94,17 +71,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/templates/routes/intel.conf.template b/nginx/templates/routes/intel.conf.template index 739c1ac69..3f7eb62f6 100644 --- a/nginx/templates/routes/intel.conf.template +++ b/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/nim.conf.template b/nginx/templates/routes/nim.conf.template index b04c02cd4..dcc15d3a2 100644 --- a/nginx/templates/routes/nim.conf.template +++ b/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/variables/template-variables.conf.template b/nginx/templates/variables/template-variables.conf.template index e2f122c52..a66dd06ea 100644 --- a/nginx/templates/variables/template-variables.conf.template +++ b/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/src/vuln_analysis/configs/config-container-analyzer.yml b/src/vuln_analysis/configs/config-container-analyzer.yml index 8f60ea4a5..8efd7c71c 100644 --- a/src/vuln_analysis/configs/config-container-analyzer.yml +++ b/src/vuln_analysis/configs/config-container-analyzer.yml @@ -4,20 +4,20 @@ general: llms: container_phase1_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.1 max_tokens: 4096 container_phase2_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.15 max_tokens: 16384 functions: web_search: - _type: tavily_internet_search + _type: web_search search_depth: advanced max_results: 5 web_fetch: diff --git a/src/vuln_analysis/configs/config-cve-researcher.yml b/src/vuln_analysis/configs/config-cve-researcher.yml index de8570ffc..9dec93541 100644 --- a/src/vuln_analysis/configs/config-cve-researcher.yml +++ b/src/vuln_analysis/configs/config-cve-researcher.yml @@ -4,14 +4,14 @@ general: llms: cve_researcher_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.15 max_tokens: 16384 functions: web_search: - _type: tavily_internet_search + _type: web_search search_depth: advanced max_results: 5 web_fetch: diff --git a/src/vuln_analysis/configs/config-eval.yml b/src/vuln_analysis/configs/config-eval.yml index 1e7d57aa6..ff285f387 100644 --- a/src/vuln_analysis/configs/config-eval.yml +++ b/src/vuln_analysis/configs/config-eval.yml @@ -28,42 +28,42 @@ general: llms: cve_researcher_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.15 max_tokens: 16384 container_phase1_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.1 max_tokens: 4096 container_phase2_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.15 max_tokens: 16384 exploitability_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.1 max_tokens: 16384 vex_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.0 max_tokens: 4096 functions: web_search: - _type: tavily_internet_search + _type: web_search search_depth: advanced max_results: 5 diff --git a/src/vuln_analysis/configs/config-exploitability-analyzer.yml b/src/vuln_analysis/configs/config-exploitability-analyzer.yml index 911497129..9e190d331 100644 --- a/src/vuln_analysis/configs/config-exploitability-analyzer.yml +++ b/src/vuln_analysis/configs/config-exploitability-analyzer.yml @@ -4,7 +4,7 @@ general: llms: exploitability_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.1 max_tokens: 16384 diff --git a/src/vuln_analysis/configs/config-vex-categorizer.yml b/src/vuln_analysis/configs/config-vex-categorizer.yml index 7ce32a1f6..c3667354b 100644 --- a/src/vuln_analysis/configs/config-vex-categorizer.yml +++ b/src/vuln_analysis/configs/config-vex-categorizer.yml @@ -4,7 +4,7 @@ general: llms: vex_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.0 max_tokens: 4096 diff --git a/src/vuln_analysis/configs/config.yml b/src/vuln_analysis/configs/config.yml index efa94be84..6e66c947e 100644 --- a/src/vuln_analysis/configs/config.yml +++ b/src/vuln_analysis/configs/config.yml @@ -4,42 +4,42 @@ general: llms: cve_researcher_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CVE_RESEARCHER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.15 max_tokens: 16384 container_phase1_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.1 max_tokens: 4096 container_phase2_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${CONTAINER_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.15 max_tokens: 16384 exploitability_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${EXPLOITABILITY_ANALYZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.1 max_tokens: 16384 vex_llm: _type: nim - model_name: ${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5} + model_name: ${VEX_CATEGORIZER_MODEL:-${DEFAULT_MODEL_NAME:-aws/anthropic/claude-opus-4-5}} base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com} temperature: 0.0 max_tokens: 4096 functions: web_search: - _type: tavily_internet_search + _type: web_search search_depth: advanced max_results: 5 diff --git a/src/vuln_analysis/data_models/input.py b/src/vuln_analysis/data_models/input.py index 295464dcd..c489e7b84 100644 --- a/src/vuln_analysis/data_models/input.py +++ b/src/vuln_analysis/data_models/input.py @@ -1,7 +1,18 @@ from __future__ import annotations + +import os + from pydantic import BaseModel, 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 PipelineInput(BaseModel): """Everything needed to run the full 4-agent vulnerability analysis pipeline.""" cve_id: str | None = None @@ -14,7 +25,7 @@ class PipelineInput(BaseModel): skip_web_research: bool = False output_dir: str = "outputs" model: str | None = None - cache: bool = True + cache: bool = Field(default_factory=_cache_default_from_env) cache_cve_researcher: bool | None = None cache_container_analyzer: bool | None = None diff --git a/src/vuln_analysis/tools/web_tools.py b/src/vuln_analysis/tools/web_tools.py index 9fe812990..97768c740 100644 --- a/src/vuln_analysis/tools/web_tools.py +++ b/src/vuln_analysis/tools/web_tools.py @@ -1,7 +1,9 @@ -"""Web fetch tool for NAT registration.""" +"""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 @@ -14,8 +16,24 @@ 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", + "sign in", + "log in", + "login", + "authentication required", "you must be signed in", ] @@ -28,15 +46,63 @@ def _looks_like_login_page(content: str) -> bool: def _strip_html_basic(html: str) -> str: - html = re.sub( - r"<(script|style)[^>]*>.*?", "", html, flags=re.DOTALL | re.IGNORECASE - ) + 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) @@ -49,11 +115,10 @@ def _web_fetch_inner(url: str, max_content_length: int, timeout: int) -> str: logger.info("web_fetch: %s", url) try: - from tavily import TavilyClient import os - api_key = os.getenv("TAVILY_API_KEY") - if api_key: - client = TavilyClient(api_key=api_key) + + 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"): @@ -61,25 +126,21 @@ def _web_fetch_inner(url: str, max_content_length: int, timeout: int) -> str: 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 + 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)" - ) - }, + 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() @@ -97,11 +158,9 @@ def _web_fetch_inner(url: str, max_content_length: int, timeout: int) -> str: 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 + 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: @@ -115,15 +174,13 @@ async def _arun(url: str) -> str: if had_fragment: logger.info("web_fetch: stripping fragment #%s from URL", fragment) - content = _web_fetch_inner(stripped_url, config.max_content_length, config.timeout) + 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.]" - ) + 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/vulnerability-analysis.code-workspace b/vulnerability-analysis.code-workspace index 114728379..855f568e6 100644 --- a/vulnerability-analysis.code-workspace +++ b/vulnerability-analysis.code-workspace @@ -105,7 +105,6 @@ ], "python.analysis.inlayHints.pytestParameters": true, "python.analysis.typeCheckingMode": "basic", - "python.languageServer": "Pylance", "python.envFile": "${workspaceFolder:workspace}/.env", "python.testing.pytestArgs": [ "-s", From ba3366aff9d2f7085e3dba32068acdd30aab5f55 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Thu, 9 Apr 2026 06:50:09 -0700 Subject: [PATCH 15/48] Concurrency improvements and remove deprecation warnings --- .../configs/config-container-analyzer.yml | 4 +- .../configs/config-cve-researcher.yml | 2 +- src/vuln_analysis/configs/config-eval.yml | 10 +- .../config-exploitability-analyzer.yml | 2 +- .../configs/config-vex-categorizer.yml | 2 +- src/vuln_analysis/configs/config.yml | 10 +- .../data_models/container_analyzer.py | 10 +- .../data_models/cve_researcher.py | 9 +- .../data_models/exploitability_analyzer.py | 9 +- src/vuln_analysis/data_models/output.py | 9 + .../data_models/vex_categorizer.py | 9 +- src/vuln_analysis/eval/evaluators/accuracy.py | 2 +- src/vuln_analysis/register.py | 505 ++++++++++-------- src/vuln_analysis/utils/concurrency.py | 67 +++ tests/utils/__init__.py | 0 tests/utils/test_concurrency.py | 194 +++++++ .../{ => utils}/test_java_script_extended.py | 0 17 files changed, 583 insertions(+), 261 deletions(-) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_concurrency.py rename tests/{ => utils}/test_java_script_extended.py (100%) diff --git a/src/vuln_analysis/configs/config-container-analyzer.yml b/src/vuln_analysis/configs/config-container-analyzer.yml index 8efd7c71c..2f7cf0fa7 100644 --- a/src/vuln_analysis/configs/config-container-analyzer.yml +++ b/src/vuln_analysis/configs/config-container-analyzer.yml @@ -5,13 +5,13 @@ 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} + 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} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} temperature: 0.15 max_tokens: 16384 diff --git a/src/vuln_analysis/configs/config-cve-researcher.yml b/src/vuln_analysis/configs/config-cve-researcher.yml index 9dec93541..48620b432 100644 --- a/src/vuln_analysis/configs/config-cve-researcher.yml +++ b/src/vuln_analysis/configs/config-cve-researcher.yml @@ -5,7 +5,7 @@ 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} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} temperature: 0.15 max_tokens: 16384 diff --git a/src/vuln_analysis/configs/config-eval.yml b/src/vuln_analysis/configs/config-eval.yml index ff285f387..f9d3f7eb4 100644 --- a/src/vuln_analysis/configs/config-eval.yml +++ b/src/vuln_analysis/configs/config-eval.yml @@ -29,35 +29,35 @@ 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} + 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} + 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} + 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} + 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} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} temperature: 0.0 max_tokens: 4096 diff --git a/src/vuln_analysis/configs/config-exploitability-analyzer.yml b/src/vuln_analysis/configs/config-exploitability-analyzer.yml index 9e190d331..9f67714d9 100644 --- a/src/vuln_analysis/configs/config-exploitability-analyzer.yml +++ b/src/vuln_analysis/configs/config-exploitability-analyzer.yml @@ -5,7 +5,7 @@ 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} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} temperature: 0.1 max_tokens: 16384 diff --git a/src/vuln_analysis/configs/config-vex-categorizer.yml b/src/vuln_analysis/configs/config-vex-categorizer.yml index c3667354b..34b1c7b28 100644 --- a/src/vuln_analysis/configs/config-vex-categorizer.yml +++ b/src/vuln_analysis/configs/config-vex-categorizer.yml @@ -5,7 +5,7 @@ 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} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} temperature: 0.0 max_tokens: 4096 diff --git a/src/vuln_analysis/configs/config.yml b/src/vuln_analysis/configs/config.yml index 6e66c947e..678434a1e 100644 --- a/src/vuln_analysis/configs/config.yml +++ b/src/vuln_analysis/configs/config.yml @@ -5,35 +5,35 @@ 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} + 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} + 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} + 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} + 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} + base_url: ${NV_BASE_URL:-https://inference-api.nvidia.com/v1} temperature: 0.0 max_tokens: 4096 diff --git a/src/vuln_analysis/data_models/container_analyzer.py b/src/vuln_analysis/data_models/container_analyzer.py index 223a9438f..603263ebc 100644 --- a/src/vuln_analysis/data_models/container_analyzer.py +++ b/src/vuln_analysis/data_models/container_analyzer.py @@ -1,7 +1,8 @@ from __future__ import annotations -import re from pydantic import BaseModel, model_validator +from vuln_analysis.data_models.output import AgentResultBase + class ContainerAnalyzerInput(BaseModel): image_name: str @@ -18,15 +19,10 @@ def _check_required(self): return self -class ContainerAnalyzerResult(BaseModel): +class ContainerAnalyzerResult(AgentResultBase): image_name: str report_path: str | None = None - report_content: str | None = None - success: bool = True - error: str | None = None phase1_skipped: bool = False - tool_call_count: int = 0 - llm_call_count: int = 0 def derive_report_filename(image_name: str) -> str: diff --git a/src/vuln_analysis/data_models/cve_researcher.py b/src/vuln_analysis/data_models/cve_researcher.py index 5fd97896b..ec211baf0 100644 --- a/src/vuln_analysis/data_models/cve_researcher.py +++ b/src/vuln_analysis/data_models/cve_researcher.py @@ -1,6 +1,8 @@ from __future__ import annotations from pydantic import BaseModel, model_validator +from vuln_analysis.data_models.output import AgentResultBase + class CVEResearcherInput(BaseModel): cve_id: str | None = None @@ -19,11 +21,6 @@ def identifier(self) -> str: return self.cve_id or self.ghsa_id -class CVEResearcherResult(BaseModel): +class CVEResearcherResult(AgentResultBase): identifier: str report_path: str | None = None - report_content: str | None = None - success: bool = True - error: str | None = None - tool_call_count: int = 0 - llm_call_count: int = 0 diff --git a/src/vuln_analysis/data_models/exploitability_analyzer.py b/src/vuln_analysis/data_models/exploitability_analyzer.py index d97262657..a489b2bc9 100644 --- a/src/vuln_analysis/data_models/exploitability_analyzer.py +++ b/src/vuln_analysis/data_models/exploitability_analyzer.py @@ -3,6 +3,8 @@ 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 @@ -20,17 +22,12 @@ def _check_required(self): return self -class ExploitabilityAnalyzerResult(BaseModel): +class ExploitabilityAnalyzerResult(AgentResultBase): cve_id: str container_name: str report_path: str | None = None - report_content: str | None = None verdict: str | None = None confidence: str | None = None - success: bool = True - error: str | None = None - tool_call_count: int = 0 - llm_call_count: int = 0 class AffectedRange(BaseModel): diff --git a/src/vuln_analysis/data_models/output.py b/src/vuln_analysis/data_models/output.py index 32e644c5f..71ec424b4 100644 --- a/src/vuln_analysis/data_models/output.py +++ b/src/vuln_analysis/data_models/output.py @@ -2,6 +2,15 @@ from pydantic import BaseModel, Field +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 + + class StageResult(BaseModel): """Outcome of a single agent stage.""" agent_name: str diff --git a/src/vuln_analysis/data_models/vex_categorizer.py b/src/vuln_analysis/data_models/vex_categorizer.py index 1dc6f15c8..e2f5060b9 100644 --- a/src/vuln_analysis/data_models/vex_categorizer.py +++ b/src/vuln_analysis/data_models/vex_categorizer.py @@ -3,6 +3,8 @@ 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 @@ -17,19 +19,14 @@ def _check_required(self): return self -class VEXCategorizerResult(BaseModel): +class VEXCategorizerResult(AgentResultBase): cve_id: str container_name: str status: str | None = None justification: str | None = None reasoning: str | None = None - report_content: str | None = None summary_report_path: str | None = None json_path: str | None = None - success: bool = True - error: str | None = None - tool_call_count: int = 0 - llm_call_count: int = 0 def parse_report_filename(exploitability_report_path: str) -> tuple[str, str]: diff --git a/src/vuln_analysis/eval/evaluators/accuracy.py b/src/vuln_analysis/eval/evaluators/accuracy.py index 472897dd0..2d5b1606e 100644 --- a/src/vuln_analysis/eval/evaluators/accuracy.py +++ b/src/vuln_analysis/eval/evaluators/accuracy.py @@ -43,7 +43,7 @@ from tqdm import tqdm try: - from nat.eval.utils.tqdm_position_registry import TqdmPositionRegistry + from nat.plugins.eval.utils.tqdm_position_registry import TqdmPositionRegistry except ImportError: TqdmPositionRegistry = None # type: ignore[assignment,misc] diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index 14fd45cf2..e41ab167a 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -8,10 +8,12 @@ import logging import re import time -import traceback -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable +from collections.abc import Callable from io import TextIOWrapper from pathlib import Path +from typing import Any +from typing import TypeVar from vuln_analysis.utils.nat_logging_patch import apply_nat_logging_patch @@ -25,41 +27,50 @@ from nat.data_models.function import FunctionBaseConfig from pydantic import Field +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.exploitability_analyzer import ExploitabilityAnalyzerInput +from vuln_analysis.data_models.exploitability_analyzer import ExploitabilityAnalyzerResult +from vuln_analysis.data_models.exploitability_analyzer import extract_container_name_from_path from vuln_analysis.data_models.input import PipelineInput -from vuln_analysis.data_models.output import PipelineResult, StageResult +from vuln_analysis.data_models.output import AgentResultBase +from vuln_analysis.data_models.output import PipelineResult +from vuln_analysis.data_models.output import StageResult from vuln_analysis.data_models.state import PipelineState -from vuln_analysis.data_models.cve_researcher import CVEResearcherInput, CVEResearcherResult -from vuln_analysis.data_models.container_analyzer import ( - ContainerAnalyzerInput, - ContainerAnalyzerResult, - derive_report_filename, -) -from vuln_analysis.data_models.exploitability_analyzer import ( - ExploitabilityAnalyzerInput, - ExploitabilityAnalyzerResult, - extract_container_name_from_path, -) -from vuln_analysis.data_models.vex_categorizer import ( - VEXCategorizerInput, - VEXCategorizerResult, - parse_report_filename, -) -from vuln_analysis.utils.report_parser import extract_markdown_table_field - +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.eval.evaluators import accuracy # noqa: F401 # Import to trigger @register_function / @register_evaluator decorators -from vuln_analysis.functions import cve_researcher # noqa: F401 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.tools import web_tools # noqa: F401 -from vuln_analysis.tools import osv_tools # noqa: F401 -from vuln_analysis.tools import filesystem_tools # noqa: F401 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.eval.evaluators import accuracy # noqa: F401 +from vuln_analysis.tools import web_tools # noqa: F401 +from vuln_analysis.utils.concurrency import AsyncCoalescer +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) + + +def _coerce_result(raw: Any, result_type: type[T], fallback_kwargs: dict) -> 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") + class CVEPipelineWorkflowConfig(FunctionBaseConfig, name="cve_pipeline"): cve_researcher_name: FunctionRef = Field(description="CVE researcher function") @@ -75,13 +86,19 @@ class CVEPipelineWorkflowConfig(FunctionBaseConfig, name="cve_pipeline"): @register_function(config_type=CVEPipelineWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) async def cve_pipeline_workflow(config: CVEPipelineWorkflowConfig, builder: Builder): - from langgraph.graph import END, START, StateGraph + from langgraph.graph import END + from langgraph.graph import START + from langgraph.graph import StateGraph cve_researcher_fn = await builder.get_function(name=config.cve_researcher_name) container_analyzer_fn = await builder.get_function(name=config.container_analyzer_name) exploitability_fn = await builder.get_function(name=config.exploitability_analyzer_name) vex_fn = await builder.get_function(name=config.vex_categorizer_name) + # Coalesces concurrent requests for the same cache key so only the first + # caller executes the agent; subsequent callers await the same result. + _coalescer = AsyncCoalescer() + async def _run_with_retry( agent_name: str, coro_factory: Callable[[], Awaitable[StageResult]], @@ -92,107 +109,129 @@ async def _run_with_retry( try: result = await asyncio.wait_for(coro_factory(), timeout=config.stage_timeout) except asyncio.TimeoutError: - result = StageResult(agent_name=agent_name, success=False, + result = StageResult(agent_name=agent_name, + success=False, error=f"Stage timed out after {config.stage_timeout}s") except Exception as exc: - result = StageResult(agent_name=agent_name, success=False, - error=f"Unhandled exception: {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) + logger.warning("%s failed (attempt %d/%d): %s — retrying", agent_name, attempt, max_attempts, result.error) return result def _result_or_failure(result, agent_name: str) -> StageResult: if isinstance(result, BaseException): - return StageResult(agent_name=agent_name, success=False, - error=f"Exception: {result}") + return StageResult(agent_name=agent_name, success=False, error=f"Exception: {result}") return result + def _use_cache_for(inp_data: dict, stage_name: str) -> bool: + """Resolve cache setting from pre-validated inp_data dict without re-constructing PipelineInput.""" + 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(inp_data: dict) -> str: + """Return a resolved output_dir string for use in coalescer keys.""" + return str(Path(inp_data.get("output_dir", config.output_dir)).resolve()) + + def _check_cache(use_cache: bool, inp_data: dict, cache_subdir: str, filename: str) -> Path | None: + """Return cached report path if cache is enabled and file exists, else None.""" + if not use_cache: + return None + root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + cached_path = root / cache_subdir / filename + return cached_path if cached_path.exists() else None + + def _persist_report(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(inp_data.get("output_dir", config.output_dir)).resolve() + report_file = root / cache_subdir / filename + atomic_write(report_file, res.report_content) + return str(report_file) + return report_path + async def _run_cve_researcher(state: PipelineState) -> StageResult: inp_data = state["input"] - pipeline_inp = PipelineInput(**inp_data) - use_cache = pipeline_inp.use_cache_for("cve_researcher") identifier = inp_data.get("cve_id") or inp_data.get("ghsa_id") - if use_cache: - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() - cached_path = root / "vulnerability_reports" / f"{identifier}.md" - if cached_path.exists(): - logger.info("Cache hit — skipping CVE Researcher, report at %s", cached_path) - return StageResult( - agent_name="CVE Researcher", success=True, - report_path=str(cached_path), - ) - - agent_inp = CVEResearcherInput( - cve_id=inp_data.get("cve_id"), - ghsa_id=inp_data.get("ghsa_id"), - package_hint=inp_data.get("package_hint"), - version_hint=inp_data.get("version_hint"), - ) - res = await cve_researcher_fn.ainvoke(agent_inp) - if not isinstance(res, CVEResearcherResult): - res = CVEResearcherResult(**res) if isinstance(res, dict) else CVEResearcherResult(identifier=agent_inp.identifier, success=False, error="Unexpected result type") + async def _cve_researcher_impl() -> StageResult: + cached = _check_cache( + _use_cache_for(inp_data, "cve_researcher"), + inp_data, "vulnerability_reports", f"{identifier}.md") + if cached: + logger.info("Cache hit — skipping CVE Researcher, report at %s", cached) + return StageResult(agent_name="CVE Researcher", success=True, report_path=str(cached)) + + agent_inp = CVEResearcherInput( + cve_id=inp_data.get("cve_id"), + ghsa_id=inp_data.get("ghsa_id"), + package_hint=inp_data.get("package_hint"), + version_hint=inp_data.get("version_hint"), + ) + res = _coerce_result( + await cve_researcher_fn.ainvoke(agent_inp), + CVEResearcherResult, + {"identifier": agent_inp.identifier}, + ) + report_path = _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, + ) - report_path = res.report_path - if res.success and res.report_content and not report_path: - root = Path(state["input"].get("output_dir", config.output_dir)).resolve() - vuln_dir = root / "vulnerability_reports" - vuln_dir.mkdir(parents=True, exist_ok=True) - report_path = str(vuln_dir / f"{res.identifier}.md") - Path(report_path).write_text(res.report_content, encoding="utf-8") - - 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 _coalescer.run_once(f"cve_researcher:{identifier}:{_output_dir_key(inp_data)}", _cve_researcher_impl) async def _run_container_analyzer(state: PipelineState) -> StageResult: inp_data = state["input"] - pipeline_inp = PipelineInput(**inp_data) - use_cache = pipeline_inp.use_cache_for("container_analyzer") - - if use_cache: - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() - filename = derive_report_filename(inp_data["image_name"]) - cached_path = root / "container_reports" / filename - if cached_path.exists(): - logger.info("Cache hit — skipping Container Analyzer, report at %s", cached_path) + image_name = inp_data["image_name"] + + async def _container_analyzer_impl() -> StageResult: + filename = derive_report_filename(image_name) + cached = _check_cache( + _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_path), + agent_name="Container Analyzer", + success=True, + report_path=str(cached), extra={"phase1_skipped": True}, ) - agent_inp = ContainerAnalyzerInput( - image_name=inp_data["image_name"], - 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 = await container_analyzer_fn.ainvoke(agent_inp) - if not isinstance(res, ContainerAnalyzerResult): - res = ContainerAnalyzerResult(**res) if isinstance(res, dict) else ContainerAnalyzerResult(image_name=agent_inp.image_name, success=False, error="Unexpected result type") + agent_inp = ContainerAnalyzerInput( + image_name=image_name, + 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 container_analyzer_fn.ainvoke(agent_inp), + ContainerAnalyzerResult, + {"image_name": agent_inp.image_name}, + ) + report_path = _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}, + ) - report_path = res.report_path - if res.success and res.report_content and not report_path: - root = Path(state["input"].get("output_dir", config.output_dir)).resolve() - container_dir = root / "container_reports" - container_dir.mkdir(parents=True, exist_ok=True) - filename = derive_report_filename(inp_data["image_name"]) - report_path = str(container_dir / filename) - Path(report_path).write_text(res.report_content, encoding="utf-8") - - 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)}:{_output_dir_key(inp_data)}" + return await _coalescer.run_once(cache_key, _container_analyzer_impl) async def stage1_parallel(state: PipelineState) -> dict: results = await asyncio.gather( @@ -215,61 +254,63 @@ async def exploitability_node(state: PipelineState) -> dict: cve_report = state["cve_report"] container_report = state["container_report"] inp_data = state["input"] - pipeline_inp = PipelineInput(**inp_data) - use_cache = pipeline_inp.use_cache_for("exploitability_analyzer") - if use_cache: - container_name = extract_container_name_from_path(container_report.report_path) - cve_id = _resolve_cve_id(cve_report.report_path) - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() - from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as exploit_derive - cached_path = root / "exploitability_reports" / exploit_derive(cve_id, container_name) - if cached_path.exists(): - logger.info("Cache hit — skipping Exploitability Analyzer, report at %s", cached_path) - content = cached_path.read_text(encoding="utf-8") - verdict = extract_markdown_table_field(content, "Verdict") - confidence = extract_markdown_table_field(content, "Confidence") - stage_result = StageResult( - agent_name="Exploitability Analyzer", success=True, - report_path=str(cached_path), - extra={"verdict": verdict, "confidence": confidence}, + from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as exploit_derive + container_name = extract_container_name_from_path(container_report.report_path) + cve_id = _resolve_cve_id(cve_report.report_path) + filename = exploit_derive(cve_id, container_name) + + async def _exploitability_impl() -> StageResult: + cached = _check_cache( + _use_cache_for(inp_data, "exploitability_analyzer"), + inp_data, "exploitability_reports", filename) + if cached: + logger.info("Cache hit — skipping Exploitability Analyzer, report at %s", 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") + }, ) - return {"exploit_report": stage_result} - agent_inp = ExploitabilityAnalyzerInput( - vulnerability_report_path=cve_report.report_path, - container_report_path=container_report.report_path, - filesystem_path=inp_data["filesystem_path"], - ) - res = await exploitability_fn.ainvoke(agent_inp) - if not isinstance(res, ExploitabilityAnalyzerResult): - res = ExploitabilityAnalyzerResult(**res) if isinstance(res, dict) else ExploitabilityAnalyzerResult(cve_id="unknown", container_name="unknown", success=False, error="Unexpected result type") + 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 exploitability_fn.ainvoke(agent_inp), + ExploitabilityAnalyzerResult, + { + "cve_id": "unknown", "container_name": "unknown" + }, + ) + report_path = _persist_report( + res, + inp_data, + "exploitability_reports", + exploit_derive(res.cve_id, container_name), + ) - report_path = res.report_path - if res.success and res.report_content and not report_path: - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() - exploit_dir = root / "exploitability_reports" - exploit_dir.mkdir(parents=True, exist_ok=True) - container_name = extract_container_name_from_path(container_report.report_path) - from vuln_analysis.data_models.exploitability_analyzer import derive_report_filename as exploit_derive - filename = exploit_derive(res.cve_id, container_name) - report_path = str(exploit_dir / filename) - Path(report_path).write_text(res.report_content, encoding="utf-8") - - stage_result = 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 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 + }, + ) - result = await _run_with_retry("Exploitability Analyzer", lambda: _make_awaitable(stage_result)) + result = await _coalescer.run_once(f"exploitability:{filename}:{_output_dir_key(inp_data)}", _exploitability_impl) return {"exploit_report": result} - async def _make_awaitable(val): - return val - def check_stage2(state: PipelineState) -> str: exploit = state.get("exploit_report") if exploit and exploit.success: @@ -280,27 +321,31 @@ async def vex_node(state: PipelineState) -> dict: exploit_report = state["exploit_report"] container_report = state["container_report"] inp_data = state["input"] - pipeline_inp = PipelineInput(**inp_data) - use_cache = pipeline_inp.use_cache_for("vex_categorizer") - if use_cache: - cve_id, container_name = parse_report_filename(exploit_report.report_path) - stem = f"{cve_id}_{container_name}" - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() - cached_md = root / "vex_results" / f"{stem}.md" - cached_json = root / "vex_results" / f"{stem}.json" - if cached_md.exists(): - logger.info("Cache hit — skipping VEX Categorizer, report at %s", cached_md) - content = cached_md.read_text(encoding="utf-8") + cve_id, container_name = parse_report_filename(exploit_report.report_path) + stem = f"{cve_id}_{container_name}" + + async def _vex_impl() -> dict: + cached = _check_cache( + _use_cache_for(inp_data, "vex_categorizer"), + inp_data, "vex_results", f"{stem}.md") + if cached: + logger.info("Cache hit — skipping VEX Categorizer, report at %s", 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") stage_result = StageResult( - agent_name="VEX Categorizer", success=True, - report_path=str(cached_md), - extra={"status": status, "justification": justification, - "reasoning": reasoning, - "json_path": str(cached_json) if cached_json.exists() else None}, + 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 + }, ) return { "vex_result": stage_result, @@ -308,45 +353,61 @@ async def vex_node(state: PipelineState) -> dict: "vex_justification": justification, } - agent_inp = VEXCategorizerInput( - exploitability_report_path=exploit_report.report_path, - container_report_path=container_report.report_path, - ) - res = await vex_fn.ainvoke(agent_inp) - if not isinstance(res, VEXCategorizerResult): - res = VEXCategorizerResult(**res) if isinstance(res, dict) else VEXCategorizerResult(cve_id="unknown", container_name="unknown", success=False, error="Unexpected result type") + agent_inp = VEXCategorizerInput( + exploitability_report_path=exploit_report.report_path, + container_report_path=container_report.report_path, + ) + res = _coerce_result( + await vex_fn.ainvoke(agent_inp), + VEXCategorizerResult, + { + "cve_id": "unknown", "container_name": "unknown" + }, + ) - summary_path = res.summary_report_path - json_path = res.json_path + summary_path = res.summary_report_path + json_path = res.json_path + + if res.success and res.report_content and not summary_path: + root = Path(inp_data.get("output_dir", config.output_dir)).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, + ) - if res.success and res.report_content and not summary_path: - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() - vex_dir = root / "vex_results" - vex_dir.mkdir(parents=True, exist_ok=True) - stem = f"{res.cve_id}_{res.container_name}" - summary_path = str(vex_dir / f"{stem}.md") - json_path = str(vex_dir / f"{stem}.json") - Path(summary_path).write_text(res.report_content, encoding="utf-8") - _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, + stage_result = 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 { + "vex_result": stage_result, + "vex_status": res.status, + "vex_justification": res.justification, + } - stage_result = 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 { - "vex_result": stage_result, - "vex_status": res.status, - "vex_justification": res.justification, - } + return await _coalescer.run_once(f"vex:{stem}:{_output_dir_key(inp_data)}", _vex_impl) async def fail_safe_node(state: PipelineState) -> dict: logger.warning("Fail-safe: defaulting to unknown/uncertain") @@ -356,10 +417,7 @@ async def fail_safe_node(state: PipelineState) -> dict: } async def output_results_node(state: PipelineState) -> dict: - success = bool( - state.get("vex_result") - and state["vex_result"].success - ) + success = bool(state.get("vex_result") and state["vex_result"].success) return {"success": success} graph_builder = StateGraph(PipelineState) @@ -370,12 +428,14 @@ async def output_results_node(state: PipelineState) -> dict: 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_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) @@ -452,9 +512,16 @@ def _resolve_cve_id(vulnerability_report_path: str) -> str: 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, + 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: data: dict = { "cve_id": cve_id, @@ -470,7 +537,5 @@ def _write_vex_json( data["report_path"] = summary_path data["exploitability_report_path"] = exploitability_report_path - p = Path(json_path) - p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(json.dumps(data, indent=2), encoding="utf-8") + atomic_write(Path(json_path), json.dumps(data, indent=2)) logger.info("Wrote VEX JSON to %s", json_path) diff --git a/src/vuln_analysis/utils/concurrency.py b/src/vuln_analysis/utils/concurrency.py index 1c2819e27..3fee44988 100644 --- a/src/vuln_analysis/utils/concurrency.py +++ b/src/vuln_analysis/utils/concurrency.py @@ -23,9 +23,12 @@ 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 contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING @@ -195,3 +198,67 @@ def file_lock(resource_path: Path, timeout: int = 300, lock_dir: Path | None = N 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/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..486ce205c --- /dev/null +++ b/tests/utils/test_concurrency.py @@ -0,0 +1,194 @@ +# 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 + + +# --------------------------------------------------------------------------- +# 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 diff --git a/tests/test_java_script_extended.py b/tests/utils/test_java_script_extended.py similarity index 100% rename from tests/test_java_script_extended.py rename to tests/utils/test_java_script_extended.py From 35ce23d82e8008530e16c06a61c4cb62b523bc4a Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Tue, 14 Apr 2026 11:11:54 -0400 Subject: [PATCH 16/48] Update installation instructions in readme --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5d7b34ea8..9316f1ca7 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,34 @@ The workflow is implemented with **[NeMo Agent Toolkit](https://docs.nvidia.com/ ## Table of Contents -- [Architecture Overview](#architecture-overview) -- [How It Works](#how-it-works) -- [Agents](#agents) -- [Prerequisites](#prerequisites) -- [Installation](#installation) -- [Preparing Container Inputs](#preparing-container-inputs) -- [Usage](#usage) -- [Output Structure](#output-structure) -- [Benchmarking](#benchmarking) -- [Configuration](#configuration) -- [Caching](#caching) -- [Design Principles](#design-principles) -- [Project Structure](#project-structure) +- [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) + - [Usage](#usage) + - [Full Pipeline](#full-pipeline) + - [Individual Agents](#individual-agents) + - [HTTP server](#http-server) + - [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) + - [Key Defaults](#key-defaults) + - [Caching](#caching) + - [Design Principles](#design-principles) + - [Project Structure](#project-structure) ## Architecture Overview @@ -143,8 +158,22 @@ Common building blocks for the NAT-registered workflow: ## Installation +This project uses **[uv](https://docs.astral.sh/uv/)** as its package manager. + +```bash +# Install uv (if not already installed) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Create a virtual environment and install the project +uv venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +uv sync +``` + +For development (linting, testing, formatting tools): + ```bash -pip install -e . +uv sync --group dev ``` The repo root usually provides symlinks **`configs`** → `src/vuln_analysis/configs` and **`data`** → `src/vuln_analysis/data` so paths in the examples below resolve. From 7dcd0b4b68e98fe2f0cdb12c61ba535f1374b76d Mon Sep 17 00:00:00 2001 From: Hsin Chen Date: Wed, 22 Apr 2026 13:43:14 -0700 Subject: [PATCH 17/48] Update the morpheus labels to match what we think is the golden answer for the agent --- .../data/eval_datasets/morpheus_benchmark_2506.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv index 23a2c6790..df0db57b5 100644 --- a/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv +++ b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv @@ -2,11 +2,11 @@ cve_id,image_name,filesystem_path,image_config_path,ground_truth_label GHSA-53q9-r3pm-6pq6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-4517,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-13836,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable -GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-wf7f-8fxf-xfxc,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-pgqp-8h46-6x4j,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-gm62-xv2j-4w53,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable -GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-7f5h-v6xp-fcq8,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-6mq8-rvhq-8wgg,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable GHSA-63vm-454h-vhhq,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable @@ -20,7 +20,7 @@ CVE-2024-9287,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_ CVE-2025-4138,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-4330,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable CVE-2025-4435,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable -CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,code_not_reachable +CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-8194,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-9230,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus_25.06-runtime/image_config.json,vulnerable CVE-2025-48384,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/filesystem,/raid/hsinc/nspect_pb_container_data/morpheus-tritonserver-models_25.06/image_config.json,code_not_reachable From b5b7f1615473e411d3a8a3ff97c17fa37d0a82d1 Mon Sep 17 00:00:00 2001 From: Eli Fajardo Date: Tue, 28 Apr 2026 11:53:11 -0700 Subject: [PATCH 18/48] Add RH python eval dataset --- ...edhat_benchmark.csv => redhat_go_benchmark.csv} | 0 .../data/eval_datasets/redhat_python_benchmark.csv | 14 ++++++++++++++ 2 files changed, 14 insertions(+) rename src/vuln_analysis/data/eval_datasets/{redhat_benchmark.csv => redhat_go_benchmark.csv} (100%) create mode 100644 src/vuln_analysis/data/eval_datasets/redhat_python_benchmark.csv diff --git a/src/vuln_analysis/data/eval_datasets/redhat_benchmark.csv b/src/vuln_analysis/data/eval_datasets/redhat_go_benchmark.csv similarity index 100% rename from src/vuln_analysis/data/eval_datasets/redhat_benchmark.csv rename to src/vuln_analysis/data/eval_datasets/redhat_go_benchmark.csv diff --git a/src/vuln_analysis/data/eval_datasets/redhat_python_benchmark.csv b/src/vuln_analysis/data/eval_datasets/redhat_python_benchmark.csv new file mode 100644 index 000000000..824dd67df --- /dev/null +++ b/src/vuln_analysis/data/eval_datasets/redhat_python_benchmark.csv @@ -0,0 +1,14 @@ +cve_id,image_name,filesystem_path,image_config_path,ground_truth_label +CVE-2024-6827,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_present +CVE-2024-1135,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_reachable +CVE-2025-47273,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_reachable +CVE-2021-23437,registry.redhat.io/quay/quay-rhel8:v3.5.7-8,/raid/efajardo/redhat_containers//quay-rhel8_v3.5.7-8/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.5.7-8/image_config.json,code_not_reachable +CVE-2019-1010083,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_reachable +CVE-2021-33503,registry.redhat.io/quay/quay-rhel8:v3.5.7-8,/raid/efajardo/redhat_containers//quay-rhel8_v3.5.7-8/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.5.7-8/image_config.json,code_not_reachable +CVE-2021-3426,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,vulnerable +CVE-2024-35195,rregistry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers/oadp-registry-rhel8_1.0.6-3/filesystem,/raid/efajardo/redhat_containers/oadp-registry-rhel8_1.0.6-3/image_config.json,requires_configuration +CVE-2023-49083,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_reachable +CVE-2024-34062,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_reachable +CVE-2024-37891,registry.redhat.io/quay/quay-rhel8:v3.9.16-3,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.16-3/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.16-3/image_config.json,code_not_reachable +CVE-2025-27516,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_reachable +CVE-2024-49767,registry.redhat.io/quay/quay-rhel8:v3.9.10-7,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/filesystem,/raid/efajardo/redhat_containers//quay-rhel8_v3.9.10-7/image_config.json,code_not_present From 9eba31bb5ae65d6ebbfeae338d3318f042d3eba8 Mon Sep 17 00:00:00 2001 From: Shawn Davis Date: Mon, 11 May 2026 08:56:29 -0700 Subject: [PATCH 19/48] Made output_dir default come from the workflow while letting input entries overwrite it if desired. This primarily helps with allowing eval runs to specify the output_dir. --- src/vuln_analysis/data_models/input.py | 2 +- src/vuln_analysis/register.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vuln_analysis/data_models/input.py b/src/vuln_analysis/data_models/input.py index c489e7b84..db762e72f 100644 --- a/src/vuln_analysis/data_models/input.py +++ b/src/vuln_analysis/data_models/input.py @@ -23,7 +23,7 @@ class PipelineInput(BaseModel): filesystem_path: str = "" image_config_path: str | None = None skip_web_research: bool = False - output_dir: str = "outputs" + output_dir: str | None = None model: str | None = None cache: bool = Field(default_factory=_cache_default_from_env) diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index e41ab167a..8afbba1e4 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -133,13 +133,15 @@ def _use_cache_for(inp_data: dict, stage_name: str) -> bool: def _output_dir_key(inp_data: dict) -> str: """Return a resolved output_dir string for use in coalescer keys.""" + if inp_data.get("output_dir") is None: + return str(Path(config.output_dir).resolve()) return str(Path(inp_data.get("output_dir", config.output_dir)).resolve()) def _check_cache(use_cache: bool, inp_data: dict, cache_subdir: str, filename: str) -> Path | None: """Return cached report path if cache is enabled and file exists, else None.""" if not use_cache: return None - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + root = Path(_output_dir_key(inp_data)).resolve() cached_path = root / cache_subdir / filename return cached_path if cached_path.exists() else None @@ -147,7 +149,7 @@ def _persist_report(res: AgentResultBase, inp_data: dict, cache_subdir: str, fil """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(inp_data.get("output_dir", config.output_dir)).resolve() + root = Path(_output_dir_key(inp_data)).resolve() report_file = root / cache_subdir / filename atomic_write(report_file, res.report_content) return str(report_file) @@ -369,7 +371,7 @@ async def _vex_impl() -> dict: json_path = res.json_path if res.success and res.report_content and not summary_path: - root = Path(inp_data.get("output_dir", config.output_dir)).resolve() + root = Path(_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) From 59850f0ccd13c49df467b747fc3e70db324a96e2 Mon Sep 17 00:00:00 2001 From: Shawn Davis Date: Tue, 19 May 2026 13:25:40 -0700 Subject: [PATCH 20/48] Apply 2 suggestion(s) to 1 file(s) Co-authored-by: Greptile --- src/vuln_analysis/register.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vuln_analysis/register.py b/src/vuln_analysis/register.py index 8afbba1e4..2bdbc0ed1 100644 --- a/src/vuln_analysis/register.py +++ b/src/vuln_analysis/register.py @@ -135,13 +135,18 @@ def _output_dir_key(inp_data: dict) -> str: """Return a resolved output_dir string for use in coalescer keys.""" if inp_data.get("output_dir") is None: return str(Path(config.output_dir).resolve()) - return str(Path(inp_data.get("output_dir", config.output_dir)).resolve()) + def _output_dir_key(inp_data: dict) -> str: + """Return a resolved output_dir string for use in coalescer keys.""" + output_dir = inp_data.get("output_dir") + if output_dir is None: + return str(Path(config.output_dir).resolve()) + return str(Path(output_dir).resolve()) def _check_cache(use_cache: bool, inp_data: dict, cache_subdir: str, filename: str) -> Path | None: """Return cached report path if cache is enabled and file exists, else None.""" if not use_cache: return None - root = Path(_output_dir_key(inp_data)).resolve() + root = Path(_output_dir_key(inp_data)) cached_path = root / cache_subdir / filename return cached_path if cached_path.exists() else None From 9a77716616fa60cfa39b3e998daa3836ea6b6eb6 Mon Sep 17 00:00:00 2001 From: shawn-davis <12801620+shawn-davis@users.noreply.github.com> Date: Tue, 26 May 2026 16:05:37 -0400 Subject: [PATCH 21/48] Updated the quick start guide to reflect the new workflow --- quick_start/quick_start_guide.ipynb | 6490 +++++++-------------------- 1 file changed, 1673 insertions(+), 4817 deletions(-) diff --git a/quick_start/quick_start_guide.ipynb b/quick_start/quick_start_guide.ipynb index fc9963c0e..eae06c451 100644 --- a/quick_start/quick_start_guide.ipynb +++ b/quick_start/quick_start_guide.ipynb @@ -1,4830 +1,1686 @@ { - "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\n", + "\n", + "This notebook runs the current vulnerability analysis workflow from the repository README: the NeMo Agent Toolkit `cve_pipeline` graph. The v3 flow takes one CVE or GHSA identifier, a container image name, and an extracted container filesystem, then runs the CVE Researcher, Container Analyzer, Exploitability Analyzer, and VEX Categorizer agents.\n", + "\n", + "The important change from the original quick start is that this workflow no longer requires an SBOM input or notebook-side edits to `config.yml`. Runtime choices such as `filesystem_path`, `image_config_path`, `output_dir`, and caching live in the `PipelineInput` JSON passed to `nat run`.\n", + "\n", + "For full details, see the [README](../README.md)." + ] + }, { - "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 notebook 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", + "- An extracted, merged container filesystem directory\n", + "- Optional: `skopeo` and `jq` if you need to download and unpack an image locally\n", + "\n", + "Set `user_container_image` and `vuln_id` in the cells below for the container and vulnerability you want to analyze." + ] + }, { - "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": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import json\n", + "import os\n", + "import shlex\n", + "import shutil\n", + "import subprocess\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "import pandas as pd\n", + "from IPython.display import Markdown, display" + ] + }, { - "data": { - "text/plain": [ - "" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set Repository Root\n", + "\n", + "Change the notebook working directory to the repository root. All paths below are relative to `REPO_ROOT` unless they are explicitly absolute." ] - }, - "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": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "REPO_ROOT = /Users/shdavis/workspace/vulnerability-analysis\n" + ] + } + ], + "source": [ + "def find_repo_root(start: Path | None = 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}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check Local Setup\n", + "\n", + "This cell does not contact external services. It verifies that the notebook can find the project files, a NAT command, and the expected environment variables." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAT command: nat\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", + "
checkok
0configs/config.ymlTrue
1data/examples/pipeline_input.jsonTrue
2NVIDIA_API_KEYTrue
3TAVILY_API_KEYTrue
4skopeoTrue
\n", + "
" + ], + "text/plain": [ + " check ok\n", + "0 configs/config.yml True\n", + "1 data/examples/pipeline_input.json True\n", + "2 NVIDIA_API_KEY True\n", + "3 TAVILY_API_KEY True\n", + "4 skopeo True" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def resolve_nat_command() -> list[str]:\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()},\n", + " {'check': 'data/examples/pipeline_input.json', 'ok': Path('data/examples/pipeline_input.json').exists()},\n", + " {'check': 'NVIDIA_API_KEY', 'ok': bool(os.getenv('NVIDIA_API_KEY'))},\n", + " {'check': 'TAVILY_API_KEY', 'ok': bool(os.getenv('TAVILY_API_KEY'))},\n", + " {'check': 'skopeo', 'ok': bool(shutil.which('skopeo'))},\n", + "]\n", + "setup_df = pd.DataFrame(setup_rows)\n", + "display(setup_df)\n", + "\n", + "missing_env = [row['check'] for row in setup_rows if row['check'].endswith('_API_KEY') and not row['ok']]\n", + "if missing_env:\n", + " print('Set these environment variables before running the pipeline:', ', '.join(missing_env))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Download And Unpack An Image With Skopeo\n", + "\n", + "Use this section when you want to manage the image locally instead of relying on the pipeline to materialize it. The commands below download the image into an OCI layout with `skopeo`, extract the filesystem layers into a merged rootfs directory, and save the image config JSON used by the Container Analyzer.\n", + "\n", + "The cells default to a dry run so the notebook does not download a large image unless you opt in. Set `RUN_SKOPEO_STEPS = True` in the next cell to execute the commands. For private registries, run `skopeo login ` first. The example pins `linux/amd64`; change the platform variables if you need a different image variant.\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", - "
" + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "$ skopeo copy --override-os linux --override-arch amd64 docker://nvcr.io/nvidia/morpheus/morpheus:25.06-runtime oci:/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/oci:latest\n", + "$ skopeo inspect --config --override-os linux --override-arch amd64 docker://nvcr.io/nvidia/morpheus/morpheus:25.06-runtime > /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/image_config.json\n", + "Getting image source signatures\n", + "Copying blob sha256:31a2ace3886c15d9cbb15cf098c5af61d70c69da1f10d3c58c933d540da8ad84\n", + "Copying blob sha256:7478e0ac0f23f94b2f27848fbcdf804a670fbf8d4bab26df842d40a10cd33059\n", + "Copying blob sha256:7d21de8cade150523dfe4c80e31ded729bb70dce5336648c5d0ef4a618ef9e3c\n", + "Copying blob sha256:2d01ee89ef0b66169d84c496b3175d51ff2a47b2e211d7e4f4fe69868681cd31\n", + "Copying blob sha256:ad69d38804778a7dd78e609a0b3cbeb96542df5c37738a8c398ec31498a1490b\n", + "Copying blob sha256:4b650590013c9e92d469a7b5514dbe45cd755b8d75f0d6ae09e2a3e089b0c82c\n", + "Copying blob sha256:bfd78c3051935607da8031a210afa2d380ca1785674ec3be126fe8c67e52b14a\n", + "Copying blob sha256:fe059e1b54f496dbe886dfca76f2afa6a4324d1c38eb3b6986f1aef0ee104c78\n", + "Copying blob sha256:c14474a099892262b89eef9c096d3161ad3deac41c50c7ae904ed36b6a718f81\n", + "Copying blob sha256:8ee1050917f1f0e4b28b49971c79dd88fbf1119218b51b95cc641bd21e7f97e1\n", + "Copying blob sha256:11588c2bcef934cf56d6246b980edec89e47922826297591fe4517337306b5da\n", + "Copying blob sha256:0a549b84e671fc30401cace0692006797959332fff60fddce5e1ddfbee7ec215\n", + "Copying blob sha256:268a30437666265152c6d8297efaba639d4e6df1652b8564ad5810bd518e5fae\n", + "Copying blob sha256:ab38304091e52352cc2a00b18614c31f1034f8c6335f5e18c279cbc652303b7b\n", + "Copying blob sha256:fd7f30b008a518a5dd387cee2fc26556d76c7c88667e70723c5f32083caeb3a5\n", + "Copying blob sha256:4d5f30ce15faf5731d7037419a57c7c484d21bbd731f2b029a0e30fdd96879e5\n", + "Copying blob sha256:d29605a7c9c9b94a84ab0889d103d4aab9848d5f11e98132f5f737e60fe52f2c\n", + "Copying blob sha256:2b3cc7575fe94fea43264b001c417251a3b732c7958c78168efed4b5659062f3\n", + "Copying blob sha256:a8661635528582214a9bde8cd2c7da323052be1327fccce31d4ddcbfe436ad37\n", + "Copying blob sha256:fc2e325ce804b26754365355ee985a9f09df1657e450220fef4f37b484006d94\n", + "Copying blob sha256:44b087db3773dbf8f68799ada642bd76571482fd3b3b70c67bab4773b248c4e2\n", + "Copying blob sha256:ab2de9d36c616202ca16d27ca4770efe00e2103c1a8ac290fe9c5ccf67b13629\n", + "Copying blob sha256:75978293e08a043ada0aa44f2f1a348728543e1d4fcf46ddbafa62b735e32110\n", + "Copying blob sha256:9a8ae0076a89130713393aff64f40d8044821cc1d755e6e1fc1f6ad9032b6fd2\n", + "Copying blob sha256:b37a9dd3848afbf79d42de41461be326cdbbccac03a07304605add2691d49963\n", + "Copying blob sha256:ff4c34f629b325fd8d2cd7b2f5dd20a95b21bc8cf81305293021a7c33fb644c8\n", + "Copying config sha256:13ff7687f92dec2b06d387666ba54b18ea6834efac1c2e2f5777a9c5865ecd55\n", + "Writing manifest to image destination\n", + "Downloaded OCI layout to /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/oci\n", + "Wrote image config to /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/image_config.json\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 " + "source": [ + "RUN_SKOPEO_STEPS = True\n", + "\n", + "user_container_image = 'registry.example.com/namespace/container:tag'\n", + "user_container_dir_name = 'user_defined_container'\n", + "skopeo_image_name = user_container_image\n", + "skopeo_platform_os = 'linux'\n", + "skopeo_platform_arch = 'amd64'\n", + "skopeo_source = f'docker://{skopeo_image_name}'\n", + "skopeo_work_dir = REPO_ROOT / '.tmp' / 'skopeo' / user_container_dir_name\n", + "skopeo_oci_dir = skopeo_work_dir / 'oci'\n", + "skopeo_rootfs_dir = skopeo_work_dir / 'filesystem'\n", + "skopeo_image_config_path = skopeo_work_dir / 'image_config.json'\n", + "\n", + "if user_container_image == 'registry.example.com/namespace/container:tag':\n", + " print('Replace user_container_image with the container image you want to analyze before running skopeo.')\n", + "\n", + "skopeo_platform_args = ['--override-os', skopeo_platform_os, '--override-arch', skopeo_platform_arch]\n", + "skopeo_copy_cmd = ['skopeo', 'copy', *skopeo_platform_args, skopeo_source, f'oci:{skopeo_oci_dir}:latest']\n", + "skopeo_config_cmd = ['skopeo', 'inspect', '--config', *skopeo_platform_args, skopeo_source]\n", + "\n", + "print('$ ' + shlex.join(skopeo_copy_cmd))\n", + "print('$ ' + shlex.join(skopeo_config_cmd) + f' > {skopeo_image_config_path}')\n", + "\n", + "if RUN_SKOPEO_STEPS:\n", + " skopeo_work_dir.mkdir(parents=True, exist_ok=True)\n", + " subprocess.run(skopeo_copy_cmd, cwd=REPO_ROOT, check=True)\n", + " with skopeo_image_config_path.open('w', encoding='utf-8') as config_out:\n", + " subprocess.run(skopeo_config_cmd, cwd=REPO_ROOT, stdout=config_out, check=True)\n", + " print(f'Downloaded OCI layout to {skopeo_oci_dir}')\n", + " print(f'Wrote image config to {skopeo_image_config_path}')\n", + "else:\n", + " print('Dry run only. Set RUN_SKOPEO_STEPS = True to download the image.')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract The Rootfs From The OCI Layout\n", + "\n", + "The OCI layout stores image layers under `blobs/sha256`. The helper below reads `index.json` and the selected manifest so layers are unpacked in manifest order.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Extracting 7478e0ac0f23f94b2f27848fbcdf804a670fbf8d4bab26df842d40a10cd33059\n", + "Extracting ad69d38804778a7dd78e609a0b3cbeb96542df5c37738a8c398ec31498a1490b\n", + "Extracting 2d01ee89ef0b66169d84c496b3175d51ff2a47b2e211d7e4f4fe69868681cd31\n", + "Extracting 7d21de8cade150523dfe4c80e31ded729bb70dce5336648c5d0ef4a618ef9e3c\n", + "Extracting 4b650590013c9e92d469a7b5514dbe45cd755b8d75f0d6ae09e2a3e089b0c82c\n", + "Extracting 31a2ace3886c15d9cbb15cf098c5af61d70c69da1f10d3c58c933d540da8ad84\n", + "Extracting bfd78c3051935607da8031a210afa2d380ca1785674ec3be126fe8c67e52b14a\n", + "Extracting fe059e1b54f496dbe886dfca76f2afa6a4324d1c38eb3b6986f1aef0ee104c78\n", + "Extracting c14474a099892262b89eef9c096d3161ad3deac41c50c7ae904ed36b6a718f81\n", + "Extracting 8ee1050917f1f0e4b28b49971c79dd88fbf1119218b51b95cc641bd21e7f97e1\n", + "Extracting 11588c2bcef934cf56d6246b980edec89e47922826297591fe4517337306b5da\n", + "Extracting 0a549b84e671fc30401cace0692006797959332fff60fddce5e1ddfbee7ec215\n", + "Extracting 268a30437666265152c6d8297efaba639d4e6df1652b8564ad5810bd518e5fae\n", + "Extracting ab38304091e52352cc2a00b18614c31f1034f8c6335f5e18c279cbc652303b7b\n", + "Extracting fd7f30b008a518a5dd387cee2fc26556d76c7c88667e70723c5f32083caeb3a5\n", + "Extracting 4d5f30ce15faf5731d7037419a57c7c484d21bbd731f2b029a0e30fdd96879e5\n", + "Extracting d29605a7c9c9b94a84ab0889d103d4aab9848d5f11e98132f5f737e60fe52f2c\n", + "Extracting 2b3cc7575fe94fea43264b001c417251a3b732c7958c78168efed4b5659062f3\n", + "Extracting a8661635528582214a9bde8cd2c7da323052be1327fccce31d4ddcbfe436ad37\n", + "Extracting fc2e325ce804b26754365355ee985a9f09df1657e450220fef4f37b484006d94\n", + "Extracting 44b087db3773dbf8f68799ada642bd76571482fd3b3b70c67bab4773b248c4e2\n", + "Extracting ab2de9d36c616202ca16d27ca4770efe00e2103c1a8ac290fe9c5ccf67b13629\n", + "Extracting 75978293e08a043ada0aa44f2f1a348728543e1d4fcf46ddbafa62b735e32110\n", + "Extracting 9a8ae0076a89130713393aff64f40d8044821cc1d755e6e1fc1f6ad9032b6fd2\n", + "Extracting b37a9dd3848afbf79d42de41461be326cdbbccac03a07304605add2691d49963\n", + "Extracting ff4c34f629b325fd8d2cd7b2f5dd20a95b21bc8cf81305293021a7c33fb644c8\n", + "Extracted filesystem to /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem\n" + ] + } + ], + "source": [ + "def oci_layer_paths(oci_dir: Path) -> list[Path]:\n", + " index_path = oci_dir / 'index.json'\n", + " index = json.loads(index_path.read_text(encoding='utf-8'))\n", + " manifest_digest = index['manifests'][0]['digest']\n", + " manifest_algo, manifest_hash = manifest_digest.split(':', 1)\n", + " manifest_path = oci_dir / 'blobs' / manifest_algo / manifest_hash\n", + " manifest = json.loads(manifest_path.read_text(encoding='utf-8'))\n", + "\n", + " layer_paths = []\n", + " for layer in manifest.get('layers', []):\n", + " layer_algo, layer_hash = layer['digest'].split(':', 1)\n", + " layer_paths.append(oci_dir / 'blobs' / layer_algo / layer_hash)\n", + " return layer_paths\n", + "\n", + "\n", + "if RUN_SKOPEO_STEPS:\n", + " skopeo_rootfs_dir.mkdir(parents=True, exist_ok=True)\n", + " for layer_path in oci_layer_paths(skopeo_oci_dir):\n", + " print(f'Extracting {layer_path.name}')\n", + " subprocess.run(['tar', '-xf', str(layer_path), '-C', str(skopeo_rootfs_dir)], check=True)\n", + " print(f'Extracted filesystem to {skopeo_rootfs_dir}')\n", + "else:\n", + " print('Dry run only. After downloading with skopeo, set RUN_SKOPEO_STEPS = True to extract layers.')\n", + " print(f'Target filesystem path: {skopeo_rootfs_dir}')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use The Local Rootfs In The Pipeline\n", + "\n", + "After the skopeo cells run, use these values in `PipelineInput`:\n", + "\n", + "- `image_name`: `skopeo_image_name`\n", + "- `filesystem_path`: `skopeo_rootfs_dir`\n", + "- `image_config_path`: `skopeo_image_config_path`\n", + "\n", + "The next section picks those paths automatically when the files exist; otherwise, edit the paths manually.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prepare Container Inputs\n", + "\n", + "The full pipeline uses the container filesystem as ground truth. If you ran the skopeo cells above, this section will point `filesystem_path` at the extracted rootfs and `image_config_path` at the saved image config. If you already have a different extracted filesystem, edit the paths in the next cell.\n", + "\n", + "Set the container image and local rootfs paths for your own target container in the next cell. If you ran the skopeo cells, these values are already wired to the skopeo output paths.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "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", + "
namepathexists
0filesystem_path/Users/shdavis/workspace/vulnerability-analysi...True
1image_config_path/Users/shdavis/workspace/vulnerability-analysi...True
\n", + "
" + ], + "text/plain": [ + " name path \\\n", + "0 filesystem_path /Users/shdavis/workspace/vulnerability-analysi... \n", + "1 image_config_path /Users/shdavis/workspace/vulnerability-analysi... \n", + "\n", + " exists \n", + "0 True \n", + "1 True " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_name = globals().get('skopeo_image_name', globals().get('user_container_image', 'registry.example.com/namespace/container:tag'))\n", + "local_image_dir = Path(globals().get('skopeo_work_dir', REPO_ROOT / '.tmp' / 'skopeo' / 'user_defined_container'))\n", + "filesystem_path = Path(globals().get('skopeo_rootfs_dir', local_image_dir / 'filesystem'))\n", + "image_config_path = Path(globals().get('skopeo_image_config_path', local_image_dir / 'image_config.json'))\n", + "\n", + "path_rows = [\n", + " {'name': 'filesystem_path', 'path': str(filesystem_path), 'exists': filesystem_path.exists()},\n", + " {'name': 'image_config_path', 'path': str(image_config_path), 'exists': image_config_path.exists()},\n", + "]\n", + "display(pd.DataFrame(path_rows))\n", + "\n", + "if not filesystem_path.exists():\n", + " print('Update `filesystem_path` above to point at an extracted container filesystem before running the pipeline.')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build `PipelineInput`\n", + "\n", + "`PipelineInput` represents a single CVE or GHSA against one container image. To run more vulnerabilities, repeat this input creation step per identifier or use the small helper in the batching section near the end." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote .tmp/quick_start_v3/pipeline_input.json\n", + "{\n", + " \"image_name\": \"nvcr.io/nvidia/morpheus/morpheus:25.06-runtime\",\n", + " \"filesystem_path\": \"/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem\",\n", + " \"image_config_path\": \"/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/image_config.json\",\n", + " \"skip_web_research\": false,\n", + " \"output_dir\": \"/Users/shdavis/workspace/vulnerability-analysis/.tmp/quick_start_v3/outputs\",\n", + " \"cache\": false,\n", + " \"cve_id\": \"CVE-2025-47273\"\n", + "}\n" + ] + } + ], + "source": [ + "def make_pipeline_input(\n", + " vuln_id: str,\n", + " *,\n", + " image_name: str,\n", + " filesystem_path: Path,\n", + " image_config_path: Path | None,\n", + " output_dir: Path,\n", + " cache: bool = False,\n", + " skip_web_research: bool = False,\n", + " package_hint: str | None = None,\n", + " version_hint: str | None = None,\n", + ") -> dict:\n", + " payload = {\n", + " 'image_name': image_name,\n", + " 'filesystem_path': str(filesystem_path),\n", + " 'image_config_path': str(image_config_path) if image_config_path and image_config_path.exists() else None,\n", + " 'skip_web_research': skip_web_research,\n", + " 'output_dir': str(output_dir),\n", + " 'cache': cache,\n", + " }\n", + " if vuln_id.upper().startswith('GHSA-'):\n", + " payload['ghsa_id'] = vuln_id\n", + " else:\n", + " payload['cve_id'] = vuln_id\n", + " if package_hint:\n", + " payload['package_hint'] = package_hint\n", + " if version_hint:\n", + " payload['version_hint'] = version_hint\n", + " return payload\n", + "\n", + "\n", + "vuln_id = 'CVE-YYYY-NNNN'\n", + "if vuln_id == 'CVE-YYYY-NNNN':\n", + " print('Replace vuln_id with the CVE or GHSA identifier you want to analyze.')\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", + "\n", + "pipeline_input = make_pipeline_input(\n", + " vuln_id,\n", + " image_name=image_name,\n", + " filesystem_path=filesystem_path,\n", + " image_config_path=image_config_path,\n", + " output_dir=output_dir,\n", + " cache=False,\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))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Validate Required Paths\n", + "\n", + "The pipeline can run without `image_config_path`, but `filesystem_path` is required." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input paths are ready.\n" + ] + } + ], + "source": [ + "if not Path(pipeline_input['filesystem_path']).exists():\n", + " raise FileNotFoundError(\n", + " f\"filesystem_path does not exist: {pipeline_input['filesystem_path']}\\n\"\n", + " 'Extract a container filesystem first, or edit the path in the previous cell.'\n", + " )\n", + "\n", + "if pipeline_input.get('image_config_path') and not Path(pipeline_input['image_config_path']).exists():\n", + " raise FileNotFoundError(f\"image_config_path does not exist: {pipeline_input['image_config_path']}\")\n", + "\n", + "print('Input paths are ready.')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Run the Full Pipeline\n", + "\n", + "This invokes the registered NAT workflow exactly as shown in the README, using the generated `PipelineInput` file. The first run can take several minutes because it may perform web research and LLM analysis. Later runs are faster when `cache` is enabled and output files already exist." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "$ nat run --config_file=configs/config.yml --input_file=/Users/shdavis/workspace/vulnerability-analysis/.tmp/quick_start_v3/pipeline_input.json\n", + "2026-05-26 14:31:29 - INFO - nat.cli.commands.start:192 - Starting NAT from config file: 'configs/config.yml'\n", + "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/_common.py:180: UserWarning: https://inference-api.nvidia.com does not end in /v1, you may have inference and listing issues. This check will be deprecated in the next release. Please ensure /v1 is appended to the provided URL\n", + " warnings.warn(\n", + "\n", + "Configuration Summary:\n", + "--------------------\n", + "Workflow Type: cve_pipeline\n", + "Number of Functions: 25\n", + "Number of Function Groups: 0\n", + "Number of LLMs: 5\n", + "Number of Embedders: 0\n", + "Number of Memory: 0\n", + "Number of Object Stores: 0\n", + "Number of Retrievers: 0\n", + "Number of TTC Strategies: 0\n", + "Number of Authentication Providers: 0\n", + "\n", + "2026-05-26 14:31:30 - INFO - nat.runtime.session:327 - Shared workflow built (entry_function=None)\n", + "2026-05-26 14:31:30 - INFO - vuln_analysis.functions.cve_researcher:39 - Starting CVE research for CVE-2025-47273\n", + "2026-05-26 14:31:30 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 1\n", + "2026-05-26 14:31:30 - INFO - vuln_analysis.functions.container_analyzer:54 - Starting container analysis for nvcr.io/nvidia/morpheus/morpheus:25.06-runtime\n", + "2026-05-26 14:31:30 - INFO - vuln_analysis.functions.container_analyzer:84 - Phase 1: running web research for public image\n", + "2026-05-26 14:31:30 - INFO - vuln_analysis.utils.web_researcher:36 - Phase 1: starting web research for nvcr.io/nvidia/morpheus/morpheus:25.06-runtime\n", + "2026-05-26 14:31:30 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 1 — iteration 1\n", + "2026-05-26 14:31:33 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_search({'question': 'nvcr.io/nvidia/morpheus/morpheus NGC catalog container image'})\n", + "2026-05-26 14:31:33 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_search({'question': 'NVIDIA Morpheus container documentation'})\n", + "2026-05-26 14:31:34 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" site:nvd.nist.gov OR site:cve.org'})\n", + "2026-05-26 14:31:34 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" vulnerability details'})\n", + "2026-05-26 14:31:34 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" CVSS CWE affected versions'})\n", + "2026-05-26 14:31:34 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" exploit OR PoC OR proof-of-concept'})\n", + "2026-05-26 14:31:34 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" CISA KEV known exploited'})\n", + "2026-05-26 14:31:36 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_search completed (\n", + "light logo\n", + "dark logo\n", + "\n", + "##...)\n", + "2026-05-26 14:31:36 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "Action Due May 17, 2026 Ta...)\n", + "2026-05-26 14:31:37 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "There was an error while loading. Please reload ...)\n", + "2026-05-26 14:31:37 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_search completed (\n", + "The NVIDIA BlueField Data Processing Unit (DPU) can be...)\n", + "2026-05-26 14:31:37 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 1 — iteration 2\n", + "2026-05-26 14:31:37 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "## Navigation Menu\n", + "\n", + "# Search code, repositories...)\n", + "2026-05-26 14:31:38 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "## CVE-2025-47273 Detail\n", + "\n", + "### Description\n", + "\n", + "setupto...)\n", + "2026-05-26 14:31:39 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "| Action | Type | Old Value | New Value |\n", + " --- --...)\n", + "2026-05-26 14:31:39 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 2\n", + "2026-05-26 14:31:41 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_fetch({'url': 'https://catalog.ngc.nvidia.com/orgs/nvidia/teams/morpheus/containers/morpheus'})\n", + "2026-05-26 14:31:41 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_fetch({'url': 'https://docs.nvidia.com/morpheus/index.html'})\n", + "2026-05-26 14:31:41 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://catalog.ngc.nvidia.com/orgs/nvidia/teams/morpheus/containers/morpheus\n", + "2026-05-26 14:31:41 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://docs.nvidia.com/morpheus/index.html\n", + "2026-05-26 14:31:41 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_fetch completed (![Morpheus (25.06) - Home](_static/nvidia-logo-horiz-rgb-blk-for-screen.svg)\n", + "![Morpheus (25.06) - Home](_static/nvidi...)\n", + "2026-05-26 14:31:45 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_fetch({'url': 'https://github.com/pypa/setuptools/security/advisories/GHSA-5rjg-fvgr-3xxf'})\n", + "2026-05-26 14:31:45 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" setuptools patch commit fix'})\n", + "2026-05-26 14:31:45 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" \"_download_url\" OR \"PackageIndex.download\" vulnerable function'})\n", + "2026-05-26 14:31:45 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/pypa/setuptools/security/advisories/GHSA-5rjg-fvgr-3xxf\n", + "2026-05-26 14:31:46 - INFO - httpx:1025 - HTTP Request: GET https://catalog.ngc.nvidia.com/orgs/nvidia/teams/morpheus/containers/morpheus \"HTTP/1.1 200 OK\"\n", + "2026-05-26 14:31:46 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_fetch completed (Morpheus | NVIDIA NGC)\n", + "2026-05-26 14:31:46 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 1 — iteration 3\n", + "2026-05-26 14:31:46 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", + "\n", + "# Search code, repositories, users, issues, pull requests...\n", + "\n", + "# Provide feedback\n", + "\n", + "We read every p...)\n", + "2026-05-26 14:31:47 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "`PackageIndex`\n", + "\n", + "### Details\n", + "\n", + " `def _download_url...)\n", + "2026-05-26 14:31:48 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 3\n", + "2026-05-26 14:31:50 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_search({'question': 'NVIDIA Morpheus SDK framework pipeline cybersecurity AI application'})\n", + "2026-05-26 14:31:50 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_fetch({'url': 'https://docs.nvidia.com/morpheus/getting_started.html'})\n", + "2026-05-26 14:31:50 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://docs.nvidia.com/morpheus/getting_started.html\n", + "2026-05-26 14:31:51 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_fetch completed (![Morpheus (25.06) - Home](_static/nvidia-logo-horiz-rgb-blk-for-screen.svg)\n", + "![Morpheus (25.06) - Home](_static/nvidi...)\n", + "2026-05-26 14:31:53 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_search completed (\n", + "## Latest commit\n", + "\n", + "## History\n", + "\n", + "## Repository files navigati...)\n", + "2026-05-26 14:31:53 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 1 — iteration 4\n", + "2026-05-26 14:31:54 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_fetch({'url': 'https://github.com/pypa/setuptools/issues/4946'})\n", + "2026-05-26 14:31:54 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" reporter credit SCH227'})\n", + "2026-05-26 14:31:54 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" EPSS score probability'})\n", + "2026-05-26 14:31:54 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/pypa/setuptools/issues/4946\n", + "2026-05-26 14:31:56 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "See the full list of npm packages c...)\n", + "2026-05-26 14:31:56 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "##### Vulnerable System Impact Metrics\n", + "\n", + "##### Su...)\n", + "2026-05-26 14:31:56 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 1 tool: web_search({'question': 'NVIDIA Morpheus runtime container vs SDK developer framework build pipelines'})\n", + "2026-05-26 14:31:57 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", + "\n", + "# Search code, repositories, users, issues, pull requests...\n", + "\n", + "# Provide feedback\n", + "\n", + "We read every p...)\n", + "2026-05-26 14:31:57 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 4\n", + "2026-05-26 14:32:00 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 1 tool web_search completed (\n", + "## Latest commit\n", + "\n", + "## History\n", + "\n", + "## Repository files navigati...)\n", + "2026-05-26 14:32:00 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 1 — iteration 5\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2025-47273', 'version': '78.1.1', 'package_name': 'setuptools'})\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2025-47273', 'version': '78.1.0', 'package_name': 'setuptools'})\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_fetch({'url': 'https://github.com/pypa/setuptools/commit/250a6d17978f9f6ac3ac887091f2d32886fbbb0b'})\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2025-47273 version=78.1.1 pkg=setuptools\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.tools.osv_tools:34 - osv_lookup: fetching https://api.osv.dev/v1/vulns/CVE-2025-47273\n", + "2026-05-26 14:32:04 - INFO - httpx:1025 - HTTP Request: GET https://api.osv.dev/v1/vulns/CVE-2025-47273 \"HTTP/1.1 200 OK\"\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2025-47273 version=78.1.0 pkg=setuptools\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/pypa/setuptools/commit/250a6d17978f9f6ac3ac887091f2d32886fbbb0b\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool osv_lookup completed (CVE-2025-47273 | version 78.1.1 | NOT AFFECTED\n", + " 78.1.1 is NOT in the OSV affected versions list.)\n", + "2026-05-26 14:32:04 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool osv_lookup completed (CVE-2025-47273 | version 78.1.0 | AFFECTED\n", + " 78.1.0 is in the OSV affected versions list.\n", + " Nearest unaffected 78.1: ...)\n", + "2026-05-26 14:32:06 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", + "\n", + "# Search code, repositories, users, issues, pull requests...\n", + "\n", + "# Provide feedback\n", + "\n", + "We read every p...)\n", + "2026-05-26 14:32:06 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 5\n", + "2026-05-26 14:32:12 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_fetch({'url': 'https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5'})\n", + "2026-05-26 14:32:12 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" \"CISA\" \"known exploited\" OR \"KEV\"'})\n", + "2026-05-26 14:32:12 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5\n", + "2026-05-26 14:32:14 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_search completed (\n", + "Action Due May 17, 2026 Ta...)\n", + "2026-05-26 14:32:14 - INFO - httpx:1025 - HTTP Request: GET https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5 \"HTTP/1.1 200 OK\"\n", + "2026-05-26 14:32:15 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_fetch completed (huntr - The world’s first bug bounty platform for AI/ML Bounties Partners Community Info SUBMIT REPORT Supported by  ...)\n", + "2026-05-26 14:32:15 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 6\n", + "2026-05-26 14:32:21 - INFO - vuln_analysis.utils.agentic_loop:145 - CVE Researcher tool: web_fetch({'url': 'https://wiz.io/vulnerability-database/cve/cve-2025-47273'})\n", + "2026-05-26 14:32:21 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://wiz.io/vulnerability-database/cve/cve-2025-47273\n", + "2026-05-26 14:32:22 - INFO - vuln_analysis.utils.agentic_loop:169 - CVE Researcher tool web_fetch completed (# CVE-2025-47273:  Python vulnerability analysis and mitigation\n", + "\n", + "## Overview\n", + "\n", + "CVE-2025-47273 affects setuptools, a Py...)\n", + "2026-05-26 14:32:22 - INFO - vuln_analysis.utils.agentic_loop:199 - CVE Researcher — iteration 7\n", + "2026-05-26 14:32:23 - INFO - vuln_analysis.utils.agentic_loop:249 - Phase 1 finished (no more tool calls).\n", + "2026-05-26 14:32:23 - INFO - vuln_analysis.utils.web_researcher:53 - Phase 1 complete (7 tool calls). Summary length: 4628 chars\n", + "2026-05-26 14:32:23 - INFO - vuln_analysis.functions.container_analyzer:92 - Phase 2: starting filesystem analysis\n", + "2026-05-26 14:32:23 - INFO - vuln_analysis.tools.filesystem_tools:56 - Filesystem tools: configured allowed roots=[PosixPath('/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem')], files=[], fs_root=None\n", + "2026-05-26 14:32:23 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 2 — iteration 1\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: read_file({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/etc/os-release'})\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: read_file({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/docker/entrypoint.sh'})\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: list_directory({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace'})\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: list_directory({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/opt/conda/envs'})\n", + "2026-05-26 14:32:30 - INFO - filesystem_tools:135 - read_file: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/etc/os-release\n", + "2026-05-26 14:32:30 - INFO - filesystem_tools:135 - read_file: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/docker/entrypoint.sh\n", + "2026-05-26 14:32:30 - INFO - filesystem_tools:164 - list_directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace\n", + "2026-05-26 14:32:30 - INFO - filesystem_tools:164 - list_directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/opt/conda/envs\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 2 tool read_file completed (PRETTY_NAME=\"Ubuntu 22.04.5 LTS\"\n", + "NAME=\"Ubuntu\"\n", + "VERSION_ID=\"22.04\"\n", + "VERSION=\"22.04.5 LTS (Jammy Jellyfish)\"\n", + "VERSION_COD...)\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 2 tool read_file completed (#!/bin/bash --login\n", + "# SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights re...)\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 2 tool list_directory completed (Directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace (10 e...)\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 2 tool list_directory completed (Directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/opt/conda/envs ...)\n", + "2026-05-26 14:32:30 - INFO - vuln_analysis.utils.agentic_loop:199 - Phase 2 — iteration 2\n", + "2026-05-26 14:32:34 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: read_file({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/README.md'})\n", + "2026-05-26 14:32:34 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: list_directory({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/examples'})\n", + "2026-05-26 14:32:34 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: list_directory({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/docs'})\n", + "2026-05-26 14:32:34 - INFO - vuln_analysis.utils.agentic_loop:145 - Phase 2 tool: list_directory({'path': '/Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/conda/environments'})\n", + "2026-05-26 14:32:34 - INFO - filesystem_tools:135 - read_file: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/README.md\n", + "2026-05-26 14:32:34 - INFO - filesystem_tools:164 - list_directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/examples\n", + "2026-05-26 14:32:34 - INFO - filesystem_tools:164 - list_directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/docs\n", + "2026-05-26 14:32:34 - INFO - filesystem_tools:164 - list_directory: /Users/shdavis/workspace/vulnerability-analysis/.tmp/skopeo/morpheus_quickstart/filesystem/workspace/conda/environments\n", + "2026-05-26 14:32:34 - INFO - vuln_analysis.utils.agentic_loop:169 - Phase 2 tool read_file completed ( 2a9f71d82aa4\n", + "Step 6/27 : COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /uvx /bin/\n", + " ---> Using cache\n", + " ---> 671310c795f5\n", + "Step 7/27 : COPY --from=gcr.io/go-containerregistry/crane:v0.21.6 /ko-app/crane /usr/local/bin/crane\n", + " ---> Using cache\n", + " ---> b61c02ba42a2\n", + "Step 8/27 : ARG VULN_ANALYSIS_VERSION\n", + " ---> Using cache\n", + " ---> 71a1c3538be8\n", + "Step 9/27 : ARG PYTHON_VERSION\n", + " ---> Using cache\n", + " ---> e764926f2c34\n", + "Step 10/27 : ENV PYTHONDONTWRITEBYTECODE=1\n", + " ---> Using cache\n", + " ---> 9e4adaf8c35e\n", + "Step 11/27 : RUN apt-get update && apt-get install -y binutils ca-certificates curl git git-lfs wget && apt-get clean && update-ca-certificates\n", + " ---> Using cache\n", + " ---> fc79bb2ec1ba\n", + "Step 12/27 : ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt\n", + " ---> Using cache\n", + " ---> 8c49a1340946\n", + "Step 13/27 : ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\n", + " ---> Using cache\n", + " ---> 9a06d1d74bc6\n", + "Step 14/27 : ENV TINI_VERSION=v0.19.0\n", + " ---> Using cache\n", + " ---> b23345c6bc7e\n", + "Step 15/27 : ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini\n", + "Downloading [==================================================>] 24.06kB/24.06kBding [==================================================>] 24.06kB/24.06kB\n", + "\n", + " ---> Using cache\n", + " ---> 3445d63c119b\n", + "Step 16/27 : RUN chmod +x /tini\n", + " ---> Using cache\n", + " ---> d5ae27b25431\n", + "Step 17/27 : SHELL [\"/bin/bash\", \"-c\"]\n", + " ---> Using cache\n", + " ---> 69be35ca6e8c\n", + "Step 18/27 : WORKDIR /workspace\n", + " ---> Using cache\n", + " ---> 02433054d74e\n", + "Step 19/27 : COPY ./ /workspace\n", + " ---> 8cb0f9382256\n", + "Step 20/27 : 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\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled\n" + ] + }, + { + "ename": "CalledProcessError", + "evalue": "Command 'b'set -euo pipefail\\n\\ndocker compose build vuln-analysis\\ndocker compose up -d vuln-analysis nginx-cache\\n'' returned non-zero exit status 1.", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mCalledProcessError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m get_ipython().run_cell_magic(\u001b[33m'bash'\u001b[39m, \u001b[33m''\u001b[39m, \u001b[33m'set -euo pipefail\\n\\ndocker compose build vuln-analysis\\ndocker compose up -d vuln-analysis nginx-cache\\n'\u001b[39m)\n", + "\u001b[31mCalledProcessError\u001b[39m: Command 'b'set -euo pipefail\\n\\ndocker compose build vuln-analysis\\ndocker compose up -d vuln-analysis nginx-cache\\n'' returned non-zero exit status 1." + ] + } + ], + "source": [ + "%%bash\n", + "set -euo pipefail\n", + "\n", + "docker compose build vuln-analysis\n", + "docker compose up -d vuln-analysis nginx-cache\n", + "docker 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", + "docker compose exec -T vuln-analysis sh -lc 'if pgrep -f \"[n]at serve --config_file=configs/config.yml\" >/dev/null; then echo \"nat serve already running\"; else nohup nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 > /var/tmp/nat-serve.log 2>&1 & echo \"started nat serve\"; fi'\n", + "sleep 10\n", + "docker compose exec -T vuln-analysis sh -lc 'tail -n 30 /var/tmp/nat-serve.log || true' \n" + ] + }, + { + "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\"\n", + "{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\"\n", + "{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 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", + "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.5" } - }, - "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 7378e7762..44be183d2 100644 --- a/deploy/2_Customize_CVE.ipynb +++ b/deploy/2_Customize_CVE.ipynb @@ -1,1150 +1,460 @@ { - "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 `pull_ref` 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 container image to generate an SBOM for\n", - "image_name = \"docker.io/langchain/langchain\"\n", - "image_tag = \"0.1.0\"\n", - "pull_ref = f\"{image_name}:{image_tag}\"\n", - "print(pull_ref)" - ] - }, - { - "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 {pull_ref} -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 {pull_ref} --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" + ] + }, + { + "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": [ - "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": "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" + ] }, { - "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": "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", + "!uv run 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": [ + "Use the relaxed VEX categorizer config when you want the final classification to lean toward `code_not_reachable` or `requires_configuration` rather than `vulnerable` for ambiguous cases.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!uv run nat run --config_file=configs/config-relaxed-vex.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\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": "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.13" } - ], - "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 } From cf4de614ea1aeb11271de6d585a31ec36f1e046f Mon Sep 17 00:00:00 2001 From: shawn-davis <12801620+shawn-davis@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:45:38 -0400 Subject: [PATCH 34/48] Updated NV_BASE_URL defaults to include `/v1` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 549f88f5f..091d665ad 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Create a `.env` file in the project root (or export variables in your shell) for ```env NVIDIA_API_KEY=your-nvidia-api-key -NV_BASE_URL=https://inference-api.nvidia.com +NV_BASE_URL=https://inference-api.nvidia.com/v1 TAVILY_API_KEY=your-tavily-api-key ``` @@ -483,7 +483,7 @@ NAT evaluators write metrics under the directory configured in **`eval.general.o | 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`) | +| `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 | From bc9a06b18e9232b21911f6a8e465d90577127eca Mon Sep 17 00:00:00 2001 From: shdavis <12801620+shawn-davis@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:23:35 -0700 Subject: [PATCH 35/48] Update CVE deployment notebooks --- deploy/1_Deploy_CVE.ipynb | 541 ++---------------------- deploy/2_Customize_CVE.ipynb | 781 ++++++++++++++++++++++++++++++++--- 2 files changed, 768 insertions(+), 554 deletions(-) diff --git a/deploy/1_Deploy_CVE.ipynb b/deploy/1_Deploy_CVE.ipynb index 456bc0c44..0782fdbac 100644 --- a/deploy/1_Deploy_CVE.ipynb +++ b/deploy/1_Deploy_CVE.ipynb @@ -78,18 +78,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "ff29582e", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Repository root: /Users/shdavis/workspace/vulnerability-analysis\n" - ] - } - ], + "outputs": [], "source": [ "from pathlib import Path\n", "import json\n", @@ -122,18 +114,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "4355172e", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using existing /Users/shdavis/workspace/vulnerability-analysis/.env\n" - ] - } - ], + "outputs": [], "source": [ "env_path = REPO_ROOT / \".env\"\n", "\n", @@ -150,19 +134,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "af2ee035", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Required API keys are present.\n", - "NV_BASE_URL: https://inference-api.nvidia.com\n" - ] - } - ], + "outputs": [], "source": [ "def load_dotenv_values(path: Path) -> dict[str, str]:\n", " values = {}\n", @@ -221,397 +196,31 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "6c9cf3c0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"vulns\": [\n", - " {\n", - " \"vuln_id\": \"CVE-2025-47273\"\n", - " },\n", - " {\n", - " \"vuln_id\": \"CVE-2026-25990\"\n", - " }\n", - " ],\n", - " \"image_name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", - " \"image_tag\": \"25.06-runtime\",\n", - " \"output_dir\": \"outputs\",\n", - " \"cache\": true\n", - "}\n" - ] - } - ], + "outputs": [], "source": [ "!python -m json.tool data/examples/pipeline_input.json\n" ] }, { "cell_type": "code", - "execution_count": 8, + "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": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2026-06-03 16:45:53 - INFO - nat.cli.commands.start:192 - Starting NAT from config file: 'configs/config.yml'\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/_common.py:178: UserWarning: https://inference-api.nvidia.com does not end in /v1, you may have inference and listing issues. This check will be deprecated in the next release. Please ensure /v1 is appended to the provided URL\n", - " warnings.warn(\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/_common.py:178: UserWarning: https://inference-api.nvidia.com does not end in /v1, you may have inference and listing issues. This check will be deprecated in the next release. Please ensure /v1 is appended to the provided URL\n", - " warnings.warn(\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/_common.py:178: UserWarning: https://inference-api.nvidia.com does not end in /v1, you may have inference and listing issues. This check will be deprecated in the next release. Please ensure /v1 is appended to the provided URL\n", - " warnings.warn(\n", - "2026-06-03 16:45:53 - INFO - vuln_analysis.pipeline.workflow:40 - Image extraction concurrency limit: 2\n", - "\n", - "Configuration Summary:\n", - "--------------------\n", - "Workflow Type: cve_pipeline\n", - "Number of Functions: 25\n", - "Number of Function Groups: 0\n", - "Number of LLMs: 5\n", - "Number of Embedders: 0\n", - "Number of Memory: 0\n", - "Number of Object Stores: 0\n", - "Number of Retrievers: 0\n", - "Number of TTC Strategies: 0\n", - "Number of Authentication Providers: 0\n", - "\n", - "2026-06-03 16:45:53 - INFO - nat.runtime.session:329 - Shared workflow built (entry_function=None)\n", - "2026-06-03 16:45:55 - INFO - vuln_analysis.utils.image_extraction:98 - Image cache hit for nvcr.io/nvidia/morpheus/morpheus:25.06-runtime (sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf)\n", - "2026-06-03 16:45:55 - INFO - vuln_analysis.pipeline.stage1:33 - Cache hit - skipping CVE Researcher for CVE-2025-47273, report at /Users/shdavis/workspace/vulnerability-analysis/outputs/vulnerability_reports/CVE-2025-47273.md\n", - "2026-06-03 16:45:55 - INFO - vuln_analysis.functions.cve_researcher:46 - Starting CVE research for CVE-2026-25990\n", - "2026-06-03 16:45:55 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 1\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:45:55 - INFO - vuln_analysis.pipeline.stage1:78 - Cache hit - skipping Container Analyzer, report at /Users/shdavis/workspace/vulnerability-analysis/outputs/container_reports/morpheus_25.06-runtime.md\n", - "2026-06-03 16:45:59 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" site:nvd.nist.gov OR site:cve.org'})\n", - "2026-06-03 16:45:59 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" vulnerability details'})\n", - "2026-06-03 16:45:59 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" CVSS CWE affected versions'})\n", - "2026-06-03 16:45:59 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" exploit OR PoC OR proof-of-concept'})\n", - "2026-06-03 16:45:59 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" CISA KEV known exploited'})\n", - "2026-06-03 16:46:02 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "Track actively exploited vulnerabili...)\n", - "2026-06-03 16:46:03 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "## CVE-2026-25990 Detail\n", - "\n", - "This CVE record has been...)\n", - "2026-06-03 16:46:03 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "Speak to Experts\n", - "\n", - "This vulner...)\n", - "2026-06-03 16:46:03 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "### Affected Products\n", - "\n", - "### Disco...)\n", - "2026-06-03 16:46:04 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "Published: February 13, 2026\n", - "\n", - "##...)\n", - "2026-06-03 16:46:04 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 2\n", - "2026-06-03 16:46:10 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"GHSA-cfh3-3jmp-rvhc\" Pillow security advisory'})\n", - "2026-06-03 16:46:10 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" Pillow patch commit fix'})\n", - "2026-06-03 16:46:10 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" \"_setimage\" encode.c decode.c vulnerable function'})\n", - "2026-06-03 16:46:12 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "\"version\": \"4.0\", \"vulnAvailabilityImpact\": \"HIGH\", \"v...)\n", - "2026-06-03 16:46:12 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "Upgrade `pillow` to version 12.1.1 or hi...)\n", - "2026-06-03 16:46:12 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "Source: GitHub Commit Changes\n", - "\n", - "#...)\n", - "2026-06-03 16:46:12 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 3\n", - "2026-06-03 16:46:18 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/python-pillow/Pillow/security/advisories/GHSA-cfh3-3jmp-rvhc'})\n", - "2026-06-03 16:46:18 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/python-pillow/Pillow/commit/5158d98c807e719c5938aa3886913ef0ea6814e9'})\n", - "2026-06-03 16:46:18 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/python-pillow/Pillow/security/advisories/GHSA-cfh3-3jmp-rvhc\n", - "2026-06-03 16:46:18 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/python-pillow/Pillow/commit/5158d98c807e719c5938aa3886913ef0ea6814e9\n", - "2026-06-03 16:46:19 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed ([Skip to content](#start-of-content) \n", - "\n", - "\n", - "\n", - "## Navigation Menu\n", - "\n", - "[Sign in](/login?return_to=https%3A%2F%2Fgithub.com%2F...)\n", - "2026-06-03 16:46:19 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", - "\n", - "# Search code, repositories, users, issues, pull requests...\n", - "\n", - "# Provide feedback\n", - "\n", - "We read every p...)\n", - "2026-06-03 16:46:19 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 4\n", - "2026-06-03 16:46:25 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/python-pillow/Pillow/commit/9000313'})\n", - "2026-06-03 16:46:25 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" Pillow \"xoff\" \"yoff\" tile extents validation commit'})\n", - "2026-06-03 16:46:25 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/python-pillow/Pillow/commit/9000313\n", - "2026-06-03 16:46:25 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", - "\n", - "# Search code, repositories, users, issues, pull requests...\n", - "\n", - "# Provide feedback\n", - "\n", - "We read every p...)\n", - "2026-06-03 16:46:27 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "PyPI: pillow\n", - "\n", - "# CVE-2026-25990\n", - "\n", - "Safety vulnerability I...)\n", - "2026-06-03 16:46:27 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 5\n", - "2026-06-03 16:46:34 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2026-25990', 'version': '12.1.1', 'package_name': 'pillow'})\n", - "2026-06-03 16:46:34 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2026-25990', 'version': '12.1.0', 'package_name': 'pillow'})\n", - "2026-06-03 16:46:34 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2026-25990', 'version': '10.3.0', 'package_name': 'pillow'})\n", - "2026-06-03 16:46:34 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2026-25990', 'version': '10.2.0', 'package_name': 'pillow'})\n", - "2026-06-03 16:46:34 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2026-25990 version=12.1.1 pkg=pillow\n", - "2026-06-03 16:46:34 - INFO - vuln_analysis.tools.osv_tools:34 - osv_lookup: fetching https://api.osv.dev/v1/vulns/CVE-2026-25990\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2026-25990 version=12.1.0 pkg=pillow\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2026-25990 version=10.3.0 pkg=pillow\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2026-25990 version=10.2.0 pkg=pillow\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool osv_lookup completed (CVE-2026-25990 | version 12.1.1 | NOT AFFECTED\n", - " 12.1.1 is NOT in the OSV affected versions list.)\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool osv_lookup completed (CVE-2026-25990 | version 12.1.0 | AFFECTED\n", - " 12.1.0 is in the OSV affected versions list.\n", - " Nearest unaffected 12.1: ...)\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool osv_lookup completed (CVE-2026-25990 | version 10.3.0 | AFFECTED\n", - " 10.3.0 is in the OSV affected versions list.\n", - " Nearest unaffected 10.3: ...)\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool osv_lookup completed (CVE-2026-25990 | version 10.2.0 | UNCERTAIN (INCOMPLETE DATA)\n", - " No versions from the 10.2 branch appear in OSV data.\n", - "...)\n", - "2026-06-03 16:46:35 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 6\n", - "2026-06-03 16:46:41 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" EPSS score probability'})\n", - "2026-06-03 16:46:41 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/python-pillow/Pillow/commit/5c89d88eee199ba53f64581ea39b6a1bc52feb1a'})\n", - "2026-06-03 16:46:41 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/python-pillow/Pillow/commit/5c89d88eee199ba53f64581ea39b6a1bc52feb1a\n", - "2026-06-03 16:46:41 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", - "\n", - "# Search code, repositories, users, issues, pull requests...\n", - "\n", - "# Provide feedback\n", - "\n", - "We read every p...)\n", - "2026-06-03 16:46:43 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "How does it work?\n", - "\n", - "EPSS=P(exploitation within 30 days | CVE is pu...)\n", - "2026-06-03 16:46:43 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 7\n", - "2026-06-03 16:46:49 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" \"0.01%\" EPSS percentile Snyk'})\n", - "2026-06-03 16:46:49 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2026-25990\" reporter \"Yarden Porat\" credits'})\n", - "2026-06-03 16:46:51 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "# Out-of-bounds Write Affecting pillow p...)\n", - "2026-06-03 16:46:51 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", - "Test your applications\n", - "\n", - "### Snyk Learn\n", - "\n", - "...)\n", - "2026-06-03 16:46:51 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 8\n", - "2026-06-03 16:47:42 - INFO - vuln_analysis.utils.agentic_loop:255 - CVE Researcher finished (no more tool calls).\n", - "2026-06-03 16:47:42 - INFO - vuln_analysis.functions.cve_researcher:76 - Research complete: CVE-2026-25990 (9284 chars, 20 tool calls, 8 LLM calls)\n", - "2026-06-03 16:47:42 - INFO - vuln_analysis.pipeline.exploitability:41 - Cache hit - skipping Exploitability Analyzer for CVE-2025-47273, report at /Users/shdavis/workspace/vulnerability-analysis/outputs/exploitability_reports/CVE-2025-47273_morpheus_25.06-runtime.md\n", - "2026-06-03 16:47:42 - INFO - vuln_analysis.functions.exploitability_analyzer:81 - Starting exploitability analysis: CVE-2026-25990 × morpheus_25.06-runtime\n", - "2026-06-03 16:47:42 - INFO - vuln_analysis.utils.pre_triage:1598 - Pre-triage: CVE=CVE-2026-25990 package=pillow ecosystem_hint=Python Imaging Library container_type=SDK/Development\n", - "2026-06-03 16:47:42 - INFO - vuln_analysis.utils.pre_triage:1606 - Pre-triage: 4 affected packages listed: ['pillow', 'python-pillow', 'python-Pillow', 'python3-pillow']\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1619 - Pre-triage: search_names=['pillow', 'pillow', 'pil'] ecosystem=stdlib\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1650 - Pre-triage: found=True installations=2\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1544 - Pre-triage: verifying additional package 'python-pillow' (search=['python-pillow', 'pillow'], eco=pip)\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1563 - Pre-triage: additional package 'python-pillow' found=True\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1544 - Pre-triage: verifying additional package 'python-Pillow' (search=['python-pillow', 'pillow'], eco=pip)\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1563 - Pre-triage: additional package 'python-Pillow' found=True\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1544 - Pre-triage: verifying additional package 'python3-pillow' (search=['python3-pillow', 'pillow'], eco=pip)\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1563 - Pre-triage: additional package 'python3-pillow' found=True\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.pre_triage:1868 - Pre-triage: CONTINUE to Phase 2 (any_vulnerable=False, uncertain=True, additional_found=True)\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.functions.exploitability_analyzer:119 - Pre-triage complete (primary_package=found, additional_found=3). Proceeding to Phase 2.\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.tools.filesystem_tools:56 - Filesystem tools: configured allowed roots=[PosixPath('/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs')], files=[PosixPath('/Users/shdavis/workspace/vulnerability-analysis/outputs/vulnerability_reports/CVE-2026-25990.md'), PosixPath('/Users/shdavis/workspace/vulnerability-analysis/outputs/container_reports/morpheus_25.06-runtime.md')], fs_root=/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.functions.exploitability_analyzer:132 - Phase 2: starting deep analysis\n", - "2026-06-03 16:47:50 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 1\n", - "2026-06-03 16:47:54 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '/Users/shdavis/workspace/vulnerability-analysis/outputs/vulnerability_reports/CVE-2026-25990.md'})\n", - "2026-06-03 16:47:54 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '/Users/shdavis/workspace/vulnerability-analysis/outputs/container_reports/morpheus_25.06-runtime.md'})\n", - "2026-06-03 16:47:54 - INFO - filesystem_tools:135 - read_file: /Users/shdavis/workspace/vulnerability-analysis/outputs/vulnerability_reports/CVE-2026-25990.md\n", - "2026-06-03 16:47:54 - INFO - filesystem_tools:135 - read_file: /Users/shdavis/workspace/vulnerability-analysis/outputs/container_reports/morpheus_25.06-runtime.md\n", - "2026-06-03 16:47:54 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (I now have all the information needed to produce the comprehensive vulnerability report. Let me compile the final rep...)\n", - "2026-06-03 16:47:54 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (Now I have gathered sufficient information to produce the container analysis report. Let me compile my findings.\n", - "\n", - "# C...)\n", - "2026-06-03 16:47:54 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 2\n", - "2026-06-03 16:48:37 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages'})\n", - "2026-06-03 16:48:37 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: glob_find({'pattern': '**/pillow*', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs'})\n", - "2026-06-03 16:48:37 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: glob_find({'pattern': '**/Pillow*', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs'})\n", - "2026-06-03 16:48:37 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages\n", - "2026-06-03 16:48:37 - INFO - filesystem_tools:289 - glob_find: pattern='**/pillow*' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs\n", - "2026-06-03 16:48:40 - INFO - filesystem_tools:289 - glob_find: pattern='**/Pillow*' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs\n", - "2026-06-03 16:48:44 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool list_directory completed (Directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs...)\n", - "2026-06-03 16:48:44 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool glob_find completed (opt/conda/envs/morpheus/conda-meta/pillow-10.3.0-py312hdcec9eb_0.json)\n", - "2026-06-03 16:48:44 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool glob_find completed (No files matching '**/Pillow*' found in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49...)\n", - "2026-06-03 16:48:44 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 3\n", - "2026-06-03 16:48:51 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/conda-meta/pillow-10.3.0-py312hdcec9eb_0.json'})\n", - "2026-06-03 16:48:51 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: glob_find({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs', 'pattern': '**/PIL/**'})\n", - "2026-06-03 16:48:51 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/conda-meta/pillow-10.3.0-py312hdcec9eb_0.json\n", - "2026-06-03 16:48:51 - INFO - filesystem_tools:289 - glob_find: pattern='**/PIL/**' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs\n", - "2026-06-03 16:48:53 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed ({\n", - " \"build\": \"py312hdcec9eb_0\",\n", - " \"build_number\": 0,\n", - " \"channel\": \"https://conda.anaconda.org/conda-forge/linux-64\",\n", - "...)\n", - "2026-06-03 16:48:53 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool glob_find completed (opt/conda/envs/morpheus/lib/python3.12/site-packages/PIL/BdfFontFile.py\n", - "opt/conda/envs/morpheus/lib/python3.12/site-p...)\n", - "2026-06-03 16:48:53 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 4\n", - "2026-06-03 16:49:03 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'pattern': 'from PIL|import PIL|from pillow|import pillow', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace'})\n", - "2026-06-03 16:49:03 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'pattern': 'Image\\\\.open|PIL\\\\.Image', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace'})\n", - "2026-06-03 16:49:03 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'pattern': 'from PIL|import PIL', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-pa...)\n", - "2026-06-03 16:49:03 - INFO - filesystem_tools:232 - grep_search: pattern='from PIL|import PIL|from pillow|import pillow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace include=None\n", - "2026-06-03 16:49:33 - INFO - filesystem_tools:232 - grep_search: pattern='Image\\\\.open|PIL\\\\.Image' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace include=None\n", - "2026-06-03 16:50:03 - INFO - filesystem_tools:232 - grep_search: pattern='from PIL|import PIL' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages include=*.py\n", - "2026-06-03 16:50:33 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (Error: grep timed out searching for 'from PIL|import PIL|from pillow|import pillow' in .cache/vuln_analysis/images/sh...)\n", - "2026-06-03 16:50:33 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (Error: grep timed out searching for 'Image\\.open|PIL\\.Image' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30...)\n", - "2026-06-03 16:50:33 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (Error: grep timed out searching for 'from PIL|import PIL' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd...)\n", - "2026-06-03 16:50:33 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 5\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:50:38 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace'})\n", - "2026-06-03 16:50:38 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/docker/entrypoint.sh'})\n", - "2026-06-03 16:50:38 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace\n", - "2026-06-03 16:50:38 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/docker/entrypoint.sh\n", - "2026-06-03 16:50:38 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool list_directory completed (Directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs...)\n", - "2026-06-03 16:50:38 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (#!/bin/bash --login\n", - "# SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights re...)\n", - "2026-06-03 16:50:38 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 6\n", - "2026-06-03 16:50:43 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: glob_find({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages', 'pattern': '**/METADATA'})\n", - "2026-06-03 16:50:43 - INFO - filesystem_tools:289 - glob_find: pattern='**/METADATA' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages\n", - "2026-06-03 16:50:45 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool glob_find completed (Deprecated-1.2.18.dist-info/METADATA\n", - "GitPython-3.1.44.dist-info/METADATA\n", - "MarkupSafe-3.0.2.dist-info/METADATA\n", - "PyNaCl-1...)\n", - "2026-06-03 16:50:45 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 7\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/matplotlib-3.10.3.dist-info...)\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/bokeh-3.7.3.dist-info', 'pa...)\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/mlflow-3.1.4.dist-info', 'p...)\n", - "2026-06-03 16:50:52 - INFO - filesystem_tools:232 - grep_search: pattern='Requires-Dist.*[Pp]illow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/matplotlib-3.10.3.dist-info include=None\n", - "2026-06-03 16:50:52 - INFO - filesystem_tools:232 - grep_search: pattern='Requires-Dist.*[Pp]illow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/bokeh-3.7.3.dist-info include=None\n", - "2026-06-03 16:50:52 - INFO - filesystem_tools:232 - grep_search: pattern='Requires-Dist.*[Pp]illow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/mlflow-3.1.4.dist-info include=None\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695...)\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695...)\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern 'Requires-Dist.*[Pp]illow' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40...)\n", - "2026-06-03 16:50:52 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 8\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:50:58 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus-25.6.0.dist-info/M...)\n", - "2026-06-03 16:50:58 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus'})\n", - "2026-06-03 16:50:58 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus-25.6.0.dist-info/METADATA\n", - "2026-06-03 16:50:58 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus\n", - "2026-06-03 16:50:58 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (Metadata-Version: 2.4\n", - "Name: morpheus\n", - "Version: 25.6.0\n", - "Summary: Morpheus\n", - "Author: NVIDIA Corporation\n", - "License: Apache\n", - "Cla...)\n", - "2026-06-03 16:50:58 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool list_directory completed (Directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs...)\n", - "2026-06-03 16:50:58 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 9\n", - "2026-06-03 16:51:06 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus', 'pattern': 'PIL|...)\n", - "2026-06-03 16:51:06 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus_llm', 'pattern': '...)\n", - "2026-06-03 16:51:06 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus_dfp', 'pattern': '...)\n", - "2026-06-03 16:51:06 - INFO - filesystem_tools:232 - grep_search: pattern='PIL|pillow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus include=*.py\n", - "2026-06-03 16:51:06 - INFO - filesystem_tools:232 - grep_search: pattern='PIL|pillow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus_llm include=*.py\n", - "2026-06-03 16:51:06 - INFO - filesystem_tools:232 - grep_search: pattern='PIL|pillow' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus_dfp include=*.py\n", - "2026-06-03 16:51:07 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern 'PIL|pillow' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a...)\n", - "2026-06-03 16:51:07 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern 'PIL|pillow' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a...)\n", - "2026-06-03 16:51:07 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern 'PIL|pillow' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a...)\n", - "2026-06-03 16:51:07 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 10\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:51:13 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/matplotl...)\n", - "2026-06-03 16:51:13 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/bokeh', ...)\n", - "2026-06-03 16:51:13 - INFO - filesystem_tools:232 - grep_search: pattern='from PIL|import PIL' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/matplotlib include=*.py\n", - "2026-06-03 16:51:15 - INFO - filesystem_tools:232 - grep_search: pattern='from PIL|import PIL' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/bokeh include=*.py\n", - "2026-06-03 16:51:15 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695...)\n", - "2026-06-03 16:51:15 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695...)\n", - "2026-06-03 16:51:15 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 11\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:51:22 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/matplotl...)\n", - "2026-06-03 16:51:22 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/bokeh', ...)\n", - "2026-06-03 16:51:22 - INFO - filesystem_tools:232 - grep_search: pattern='\\\\.psd|PSD|PsdImage' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/matplotlib include=*.py\n", - "2026-06-03 16:51:23 - INFO - filesystem_tools:232 - grep_search: pattern='\\\\.psd|PSD|PsdImage' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/bokeh include=*.py\n", - "2026-06-03 16:51:23 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695...)\n", - "2026-06-03 16:51:23 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern '\\.psd|PSD|PsdImage' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb...)\n", - "2026-06-03 16:51:23 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 12\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:51:29 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/PIL/PsdImagePlugin.py'})\n", - "2026-06-03 16:51:29 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/PIL/PsdImagePlugin.py\n", - "2026-06-03 16:51:29 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (#\n", - "# The Python Imaging Library\n", - "# $Id$\n", - "#\n", - "# Adobe PSD 2.5/3.0 file handling\n", - "#\n", - "# History:\n", - "# 1995-09-01 fl Created\n", - "# 19...)\n", - "2026-06-03 16:51:29 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 13\n", - "2026-06-03 16:51:35 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: check_security_controls({'scope': 'all'})\n", - "2026-06-03 16:51:35 - INFO - vuln_analysis.tools.package_tools:454 - check_security_controls\n", - "2026-06-03 16:51:35 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool check_security_controls completed (No explicit security controls found baked into the container image.\n", - "Note: Docker default seccomp and K8s pod security...)\n", - "2026-06-03 16:51:35 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 14\n", - "2026-06-03 16:51:40 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples'})\n", - "2026-06-03 16:51:40 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples\n", - "2026-06-03 16:51:40 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool list_directory completed (Directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs...)\n", - "2026-06-03 16:51:40 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 15\n", - "2026-06-03 16:51:45 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples', 'pattern': 'PIL|pillow|Image\\\\.open'})\n", - "2026-06-03 16:51:45 - INFO - filesystem_tools:232 - grep_search: pattern='PIL|pillow|Image\\\\.open' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples include=*.py\n", - "2026-06-03 16:51:45 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern 'PIL|pillow|Image\\.open' in .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd4095...)\n", - "2026-06-03 16:51:45 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 16\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:51:51 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus...)\n", - "2026-06-03 16:51:51 - INFO - filesystem_tools:232 - grep_search: pattern='import matplotlib|from matplotlib|import bokeh|from bokeh' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus include=*.py\n", - "2026-06-03 16:51:51 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (/Users/shdavis/workspace/vulnerability-analysis/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695...)\n", - "2026-06-03 16:51:51 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 17\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:51:57 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus/models/dfencoder/l...)\n", - "2026-06-03 16:51:57 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: grep_search({'include': '*.py', 'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples', 'pattern': 'import matplotlib|from matpl...)\n", - "2026-06-03 16:51:57 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs/morpheus/lib/python3.12/site-packages/morpheus/models/dfencoder/logging.py\n", - "2026-06-03 16:51:57 - INFO - filesystem_tools:232 - grep_search: pattern='import matplotlib|from matplotlib|import bokeh|from bokeh' path=.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples include=*.py\n", - "2026-06-03 16:51:57 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", - "# SPDX-Licen...)\n", - "2026-06-03 16:51:57 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool grep_search completed (No matches found for pattern 'import matplotlib|from matplotlib|import bokeh|from bokeh' in .cache/vuln_analysis/imag...)\n", - "2026-06-03 16:51:57 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 18\n", - "/Users/shdavis/workspace/vulnerability-analysis/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", - " warnings.warn(\n", - "2026-06-03 16:53:05 - INFO - vuln_analysis.utils.agentic_loop:255 - Phase 2 finished (no more tool calls).\n", - "2026-06-03 16:53:05 - INFO - vuln_analysis.functions.exploitability_analyzer:167 - Analysis complete: CVE-2026-25990 × morpheus_25.06-runtime (verdict=CONDITIONALLY EXPLOITABLE, confidence=High, 32 tool calls, 18 LLM calls)\n", - "2026-06-03 16:53:05 - INFO - vuln_analysis.pipeline.vex:41 - Cache hit - skipping VEX Categorizer for CVE-2025-47273, report at /Users/shdavis/workspace/vulnerability-analysis/outputs/vex_results/CVE-2025-47273_morpheus_25.06-runtime.md\n", - "2026-06-03 16:53:06 - INFO - vuln_analysis.functions.vex_categorizer:80 - Starting VEX categorization: CVE-2026-25990 × morpheus_25.06-runtime\n", - "2026-06-03 16:53:06 - INFO - vuln_analysis.utils.agentic_loop:205 - VEX Categorizer — iteration 1\n", - "2026-06-03 16:53:15 - INFO - vuln_analysis.utils.agentic_loop:150 - VEX Categorizer tool: reason({'step': 'VEX Step 1: Is the CVE Association Correct?', 'analysis': 'Examining the \"Step 1: Vulnerable Component Presence\" table from the exploitability report:\\n\\n- **Package in Filesystem:** Yes ...)\n", - "2026-06-03 16:53:15 - INFO - vuln_analysis.functions.vex_categorizer:32 - reason: step=VEX Step 1: Is the CVE Association Correct?, conclusion=PASS - CVE association is correct. Pillow 10.3.0 is confirmed vulnerable. Proceeding to VEX Step 2.\n", - "2026-06-03 16:53:15 - INFO - vuln_analysis.utils.agentic_loop:174 - VEX Categorizer tool reason completed (Recorded. Step: VEX Step 1: Is the CVE Association Correct? → Conclusion: PASS - CVE association is correct. Pillow 1...)\n", - "2026-06-03 16:53:15 - INFO - vuln_analysis.utils.agentic_loop:205 - VEX Categorizer — iteration 2\n", - "2026-06-03 16:53:24 - INFO - vuln_analysis.utils.agentic_loop:150 - VEX Categorizer tool: reason({'step': 'VEX Step 2: Is the Vulnerable Code Present?', 'analysis': 'Examining the \"Step 1: Vulnerable Component Presence\" table and \"Conditions Matrix\":\\n\\nFrom Step 1 table:\\n- **Package in Files...)\n", - "2026-06-03 16:53:24 - INFO - vuln_analysis.functions.vex_categorizer:32 - reason: step=VEX Step 2: Is the Vulnerable Code Present?, conclusion=PASS - Vulnerable code IS present. Pillow 10.3.0 with PsdImagePlugin is installed in the active morpheus conda enviro...\n", - "2026-06-03 16:53:24 - INFO - vuln_analysis.utils.agentic_loop:174 - VEX Categorizer tool reason completed (Recorded. Step: VEX Step 2: Is the Vulnerable Code Present? → Conclusion: PASS - Vulnerable code IS present. Pillow 1...)\n", - "2026-06-03 16:53:24 - INFO - vuln_analysis.utils.agentic_loop:205 - VEX Categorizer — iteration 3\n", - "2026-06-03 16:53:37 - INFO - vuln_analysis.utils.agentic_loop:150 - VEX Categorizer tool: reason({'analysis': 'Checking container type from both reports:\\n\\nFrom Container Report - Summary table:\\n- **Container Type:** SDK/Development\\n\\nFrom Container Report - Container Type Analysis:\\n- Clas...)\n", - "2026-06-03 16:53:37 - INFO - vuln_analysis.functions.vex_categorizer:32 - reason: step=SDK Policy Check, conclusion=MATCH - SDK container with accessible vulnerable library. Pillow is installed in the active environment, users can im...\n", - "2026-06-03 16:53:37 - INFO - vuln_analysis.utils.agentic_loop:174 - VEX Categorizer tool reason completed (Recorded. Step: SDK Policy Check → Conclusion: MATCH - SDK container with accessible vulnerable library. Pillow is in...)\n", - "2026-06-03 16:53:37 - INFO - vuln_analysis.utils.agentic_loop:205 - VEX Categorizer — iteration 4\n", - "2026-06-03 16:53:49 - INFO - vuln_analysis.utils.agentic_loop:255 - VEX Categorizer finished (no more tool calls).\n", - "2026-06-03 16:53:49 - INFO - vuln_analysis.functions.vex_categorizer:122 - VEX categorization complete: CVE-2026-25990 × morpheus_25.06-runtime → status=affected, justification=vulnerable\n", - "2026-06-03 16:53:49 - INFO - vuln_analysis.pipeline.helpers:102 - Wrote VEX JSON to /Users/shdavis/workspace/vulnerability-analysis/outputs/vex_results/CVE-2026-25990_morpheus_25.06-runtime.json\n", - "2026-06-03 16:53:49 - INFO - nat.front_ends.console.console_front_end_plugin:160 - --------------------------------------------------\n", - "\u001b[32mWorkflow Result:\n", - "{\"image_name\":\"nvcr.io/nvidia/morpheus/morpheus\",\"image_tag\":\"25.06-runtime\",\"success\":true,\"container_analyzer\":{\"agent_name\":\"Container Analyzer\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/container_reports/morpheus_25.06-runtime.md\",\"error\":null,\"model\":null,\"tool_call_count\":0,\"llm_call_count\":0,\"extra\":{\"phase1_skipped\":true}},\"elapsed_seconds\":475.119666374987,\"results\":[{\"vuln_id\":\"CVE-2025-47273\",\"cve_id\":\"CVE-2025-47273\",\"ghsa_id\":\"GHSA-5rjg-fvgr-3xxf\",\"success\":true,\"cve_researcher\":{\"agent_name\":\"CVE Researcher\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/vulnerability_reports/CVE-2025-47273.md\",\"error\":null,\"model\":null,\"tool_call_count\":0,\"llm_call_count\":0,\"extra\":{}},\"exploitability_analyzer\":{\"agent_name\":\"Exploitability Analyzer\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/exploitability_reports/CVE-2025-47273_morpheus_25.06-runtime.md\",\"error\":null,\"model\":null,\"tool_call_count\":0,\"llm_call_count\":0,\"extra\":{\"verdict\":\"NOT APPLICABLE\",\"confidence\":\"High\"}},\"vex_categorizer\":{\"agent_name\":\"VEX Categorizer\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/vex_results/CVE-2025-47273_morpheus_25.06-runtime.md\",\"error\":null,\"model\":null,\"tool_call_count\":0,\"llm_call_count\":0,\"extra\":{\"status\":\"not_affected\",\"justification\":\"false_positive\",\"reasoning\":\"The scanner incorrectly identified setuptools as vulnerable. The active morpheus environment has setuptools version 80.9.0 installed, which is greater than the fixed version 78.1.1. The installed version is NOT within the vulnerable range (< 78.1.1).\",\"json_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/vex_results/CVE-2025-47273_morpheus_25.06-runtime.json\"}},\"vex_status\":\"not_affected\",\"vex_justification\":\"false_positive\",\"error\":null},{\"vuln_id\":\"CVE-2026-25990\",\"cve_id\":\"CVE-2026-25990\",\"ghsa_id\":\"GHSA-cfh3-3jmp-rvhc\",\"success\":true,\"cve_researcher\":{\"agent_name\":\"CVE Researcher\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/vulnerability_reports/CVE-2026-25990.md\",\"error\":null,\"model\":null,\"tool_call_count\":20,\"llm_call_count\":8,\"extra\":{}},\"exploitability_analyzer\":{\"agent_name\":\"Exploitability Analyzer\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/exploitability_reports/CVE-2026-25990_morpheus_25.06-runtime.md\",\"error\":null,\"model\":null,\"tool_call_count\":32,\"llm_call_count\":18,\"extra\":{\"verdict\":\"CONDITIONALLY EXPLOITABLE\",\"confidence\":\"High\"}},\"vex_categorizer\":{\"agent_name\":\"VEX Categorizer\",\"success\":true,\"report_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/vex_results/CVE-2026-25990_morpheus_25.06-runtime.md\",\"error\":null,\"model\":null,\"tool_call_count\":3,\"llm_call_count\":4,\"extra\":{\"status\":\"affected\",\"justification\":\"vulnerable\",\"reasoning\":\"This is an SDK/Development container where Pillow 10.3.0 (vulnerable to CVE-2026-25990) is installed in the active morpheus conda environment. Users can import PIL and call `Image.open()` on PSD files, directly accessing the vulnerable code path with no architectural barriers.\",\"json_path\":\"/Users/shdavis/workspace/vulnerability-analysis/outputs/vex_results/CVE-2026-25990_morpheus_25.06-runtime.json\"}},\"vex_status\":\"affected\",\"vex_justification\":\"vulnerable\",\"error\":null}]}\u001b[39m\n", - "--------------------------------------------------\n", - "\u001b[0m\u001b[0m" - ] - } - ], + "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" @@ -632,92 +241,7 @@ "execution_count": null, "id": "1032b372", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "time=\"2026-06-03T17:12:06-04:00\" level=warning msg=\"Docker Compose requires buildx plugin to be installed\"\n", - " Image vuln-analysis:3.0.0 Building \n", - " Image vuln-analysis:3.0.0 Building \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sending build context to Docker daemon 7.426GB\n", - "Step 1/27 : ARG BASE_IMAGE_NAME=nvcr.io/nvidia/base/ubuntu\n", - "Step 2/27 : ARG BASE_IMAGE_TAG=22.04_20240212\n", - "Step 3/27 : ARG PYTHON_VERSION=3.13\n", - "Step 4/27 : ARG VULN_ANALYSIS_VERSION=3.0.0\n", - "Step 5/27 : FROM ${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG} AS base\n", - " ---> 2a9f71d82aa4\n", - "Step 6/27 : COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /uvx /bin/\n", - " ---> Using cache\n", - " ---> 671310c795f5\n", - "Step 7/27 : COPY --from=gcr.io/go-containerregistry/crane:v0.21.6 /ko-app/crane /usr/local/bin/crane\n", - " ---> Using cache\n", - " ---> b61c02ba42a2\n", - "Step 8/27 : ARG VULN_ANALYSIS_VERSION\n", - " ---> Using cache\n", - " ---> 71a1c3538be8\n", - "Step 9/27 : ARG PYTHON_VERSION\n", - " ---> Using cache\n", - " ---> e764926f2c34\n", - "Step 10/27 : ENV PYTHONDONTWRITEBYTECODE=1\n", - " ---> Using cache\n", - " ---> 9e4adaf8c35e\n", - "Step 11/27 : RUN apt-get update && apt-get install -y binutils ca-certificates curl git git-lfs wget && apt-get clean && update-ca-certificates\n", - " ---> Using cache\n", - " ---> fc79bb2ec1ba\n", - "Step 12/27 : ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt\n", - " ---> Using cache\n", - " ---> 8c49a1340946\n", - "Step 13/27 : ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\n", - " ---> Using cache\n", - " ---> 9a06d1d74bc6\n", - "Step 14/27 : ENV TINI_VERSION=v0.19.0\n", - " ---> Using cache\n", - " ---> b23345c6bc7e\n", - "Step 15/27 : ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini\n", - "Downloading [==================================================>] 24.06kB/24.06kBding [==================================================>] 24.06kB/24.06kB\n", - "\n", - " ---> Using cache\n", - " ---> 3445d63c119b\n", - "Step 16/27 : RUN chmod +x /tini\n", - " ---> Using cache\n", - " ---> d5ae27b25431\n", - "Step 17/27 : SHELL [\"/bin/bash\", \"-c\"]\n", - " ---> Using cache\n", - " ---> 69be35ca6e8c\n", - "Step 18/27 : WORKDIR /workspace\n", - " ---> Using cache\n", - " ---> 02433054d74e\n", - "Step 19/27 : COPY ./ /workspace\n", - " ---> 8cb0f9382256\n", - "Step 20/27 : 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\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled\n" - ] - }, - { - "ename": "CalledProcessError", - "evalue": "Command 'b'set -euo pipefail\\n\\ndocker compose build vuln-analysis\\ndocker compose up -d vuln-analysis nginx-cache\\n'' returned non-zero exit status 1.", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mCalledProcessError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m get_ipython().run_cell_magic(\u001b[33m'bash'\u001b[39m, \u001b[33m''\u001b[39m, \u001b[33m'set -euo pipefail\\n\\ndocker compose build vuln-analysis\\ndocker compose up -d vuln-analysis nginx-cache\\n'\u001b[39m)\n", - "\u001b[31mCalledProcessError\u001b[39m: Command 'b'set -euo pipefail\\n\\ndocker compose build vuln-analysis\\ndocker compose up -d vuln-analysis nginx-cache\\n'' returned non-zero exit status 1." - ] - } - ], + "outputs": [], "source": [ "%%bash\n", "set -euo pipefail\n", @@ -747,9 +271,12 @@ "%%bash\n", "set -euo pipefail\n", "\n", - "docker compose exec -T vuln-analysis sh -lc 'if pgrep -f \"[n]at serve --config_file=configs/config.yml\" >/dev/null; then echo \"nat serve already running\"; else nohup nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 26466 > /var/tmp/nat-serve.log 2>&1 & echo \"started nat serve\"; fi'\n", - "sleep 10\n", - "docker compose exec -T vuln-analysis sh -lc 'tail -n 30 /var/tmp/nat-serve.log || true' \n" + "if docker compose exec -T vuln-analysis pgrep -af '[n]at serve' >/dev/null; then\n", + " echo \"nat server is running\"\n", + "else\n", + " docker 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" ] }, { @@ -783,7 +310,7 @@ "\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" + "python -m json.tool .tmp/deploy_generate_response.json | sed -n '1,160p'\n" ] }, { @@ -852,8 +379,7 @@ "output_root = Path(\"outputs\")\n", "for subdir in [\"vulnerability_reports\", \"container_reports\", \"exploitability_reports\", \"vex_results\"]:\n", " path = output_root / subdir\n", - " print(f\"\n", - "{path}\")\n", + " print(f\"{path}\")\n", " if path.exists():\n", " for item in sorted(path.iterdir())[:10]:\n", " print(\" -\", item)\n", @@ -880,8 +406,7 @@ "import json\n", "\n", "for result_path in sorted(Path(\"outputs/vex_results\").glob(\"*.json\"))[:3]:\n", - " print(f\"\n", - "{result_path}\")\n", + " print(f\"{result_path}\")\n", " print(json.dumps(json.loads(result_path.read_text()), indent=2)[:4000])\n" ] }, @@ -930,7 +455,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": ".venv (3.13.2)", "language": "python", "name": "python3" }, @@ -944,7 +469,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.5" + "version": "3.13.2" } }, "nbformat": 4, diff --git a/deploy/2_Customize_CVE.ipynb b/deploy/2_Customize_CVE.ipynb index 44be183d2..11f86f236 100644 --- a/deploy/2_Customize_CVE.ipynb +++ b/deploy/2_Customize_CVE.ipynb @@ -43,9 +43,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Repository root: \n" + ] + } + ], "source": [ "from pathlib import Path\n", "import json\n", @@ -95,9 +103,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".tmp/custom_pipeline_input.json\n", + "{\n", + " \"vulns\": [\n", + " {\n", + " \"vuln_id\": \"CVE-2025-47273\"\n", + " }\n", + " ],\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" + ] + } + ], "source": [ "custom_input = {\n", " \"vulns\": [{\"vuln_id\": \"CVE-2025-47273\"}],\n", @@ -109,8 +136,7 @@ "\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", + "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" ] @@ -124,9 +150,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".tmp/custom_multi_cve_input.json\n", + "{\n", + " \"vulns\": [\n", + " {\n", + " \"vuln_id\": \"CVE-2025-47273\"\n", + " },\n", + " {\n", + " \"vuln_id\": \"CVE-2026-25990\",\n", + " \"package_hint\": \"setuptools\"\n", + " }\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" + ] + } + ], "source": [ "multi_cve_input = {\n", " \"vulns\": [\n", @@ -140,8 +189,7 @@ "}\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", + "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" ] @@ -182,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -200,16 +248,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"vulns\": [\n", + " {\n", + " \"vuln_id\": \"CVE-2025-47273\"\n", + " }\n", + " ],\n", + " \"image_name\": \"nvcr.io/nvidia/morpheus/morpheus\",\n", + " \"image_tag\": \"25.06-runtime\",\n", + " \"output_dir\": \"outputs/pre-extracted-morpheus\"\n", + "}\n" + ] + } + ], "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", + " # \"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", @@ -227,12 +292,433 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-06-04 13:38:26 - INFO - nat.cli.commands.start:192 - Starting NAT from config file: 'configs/config.yml'\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.pipeline.workflow:40 - Image extraction concurrency limit: 2\n", + "\n", + "Configuration Summary:\n", + "--------------------\n", + "Workflow Type: cve_pipeline\n", + "Number of Functions: 25\n", + "Number of Function Groups: 0\n", + "Number of LLMs: 5\n", + "Number of Embedders: 0\n", + "Number of Memory: 0\n", + "Number of Object Stores: 0\n", + "Number of Retrievers: 0\n", + "Number of TTC Strategies: 0\n", + "Number of Authentication Providers: 0\n", + "\n", + "2026-06-04 13:38:27 - INFO - nat.runtime.session:329 - Shared workflow built (entry_function=None)\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.utils.image_extraction:98 - Image cache hit for nvcr.io/nvidia/morpheus/morpheus:25.06-runtime (sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf)\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.functions.cve_researcher:46 - Starting CVE research for CVE-2025-47273\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 1\n", + "/.venv/lib/python3.13/site-packages/langchain_nvidia_ai_endpoints/chat_models.py:1029: UserWarning: Model 'aws/anthropic/claude-opus-4-5' is not known to support tools. Your tool binding may fail at inference time.\n", + " warnings.warn(\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.functions.container_analyzer:62 - Starting container analysis for nvcr.io/nvidia/morpheus/morpheus:25.06-runtime\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.functions.container_analyzer:92 - Phase 1: running web research for public image\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.utils.web_researcher:38 - Phase 1: starting web research for nvcr.io/nvidia/morpheus/morpheus:25.06-runtime\n", + "2026-06-04 13:38:27 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 1 — iteration 1\n", + "2026-06-04 13:38:30 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 1 tool: web_search({'question': 'nvcr.io/nvidia/morpheus/morpheus NGC catalog container image'})\n", + "2026-06-04 13:38:30 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 1 tool: web_search({'question': 'NVIDIA Morpheus container documentation'})\n", + "2026-06-04 13:38:31 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" site:nvd.nist.gov OR site:cve.org'})\n", + "2026-06-04 13:38:31 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" vulnerability details'})\n", + "2026-06-04 13:38:31 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" CVSS CWE affected versions'})\n", + "2026-06-04 13:38:31 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" exploit OR PoC OR proof-of-concept'})\n", + "2026-06-04 13:38:31 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" CISA KEV known exploited'})\n", + "2026-06-04 13:38:33 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 1 tool web_search completed (\n", + "# Welcome to Morpheus Documentation#\n", + "\n", + "NVIDIA Morpheus\n", + "...)\n", + "2026-06-04 13:38:33 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 1 — iteration 2\n", + "2026-06-04 13:38:34 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "There was an error while loading. Please reload ...)\n", + "2026-06-04 13:38:34 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "Speak to Experts\n", + "\n", + "CVE-2025-47...)\n", + "2026-06-04 13:38:34 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "## Navigation Menu\n", + "\n", + "# Search code, repositories...)\n", + "2026-06-04 13:38:34 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "`PackageIndex`\n", + "\n", + "### Details\n", + "\n", + " `def _download_url...)\n", + "2026-06-04 13:38:45 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 1 tool web_fetch completed (![Morpheus (25.06) - Home](_static/nvidia-logo-horiz-rgb-blk-for-screen.svg)\n", + "![Morpheus (25.06) - Home](_static/nvidi...)\n", + "2026-06-04 13:38:45 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "OSV logo OSV logo\n", + "\n", + " Vulnerability Database\n", + " Blog...)\n", + "2026-06-04 13:38:45 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 3\n", + "2026-06-04 13:38:46 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 1 tool web_search completed (\n", + "...)\n", + "2026-06-04 13:38:46 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 1 — iteration 4\n", + "2026-06-04 13:38:49 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 1 tool: web_fetch({'url': 'https://catalog.ngc.nvidia.com/orgs/nvidia/teams/morpheus/containers/morpheus'})\n", + "2026-06-04 13:38:49 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://catalog.ngc.nvidia.com/orgs/nvidia/teams/morpheus/containers/morpheus\n", + "2026-06-04 13:38:50 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/pypa/setuptools/security/advisories/GHSA-5rjg-fvgr-3xxf'})\n", + "2026-06-04 13:38:50 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/pypa/setuptools/issues/4946'})\n", + "2026-06-04 13:38:50 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/pypa/setuptools/issues/4946\n", + "2026-06-04 13:38:50 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/pypa/setuptools/security/advisories/GHSA-5rjg-fvgr-3xxf\n", + "2026-06-04 13:38:51 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", + "\n", + "# Search code, repositories, users, issues, pull requests...\n", + "\n", + "# Provide feedback\n", + "\n", + "We read every p...)\n", + "2026-06-04 13:38:52 - INFO - httpx:1025 - HTTP Request: GET https://catalog.ngc.nvidia.com/orgs/nvidia/teams/morpheus/containers/morpheus \"HTTP/1.1 200 OK\"\n", + "2026-06-04 13:38:52 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 1 tool web_fetch completed (Morpheus | NVIDIA NGC)\n", + "2026-06-04 13:38:52 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 1 — iteration 5\n", + "2026-06-04 13:38:52 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", + "\n", + "# Search code, repositories, users, issues, pull requests...\n", + "\n", + "# Provide feedback\n", + "\n", + "We read every p...)\n", + "2026-06-04 13:38:52 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 4\n", + "2026-06-04 13:38:56 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': 'setuptools 78.1.1 release commit \"CVE-2025-47273\" OR \"4946\" fix'})\n", + "2026-06-04 13:38:56 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://github.com/pypa/setuptools/commit/250a6d17978f9f6ac3ac887091f2d32886fbbb0b'})\n", + "2026-06-04 13:38:56 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://github.com/pypa/setuptools/commit/250a6d17978f9f6ac3ac887091f2d32886fbbb0b\n", + "2026-06-04 13:38:57 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (## Navigation Menu\n", + "\n", + "# Search code, repositories, users, issues, pull requests...\n", + "\n", + "# Provide feedback\n", + "\n", + "We read every p...)\n", + "2026-06-04 13:38:57 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 1 tool: web_search({'question': '\"NVIDIA Morpheus\" \"AI application framework\" cybersecurity SDK pipeline developer'})\n", + "2026-06-04 13:38:59 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "78.1.1 fixes the issue.\", \"A path traversal vulnerabilit...)\n", + "2026-06-04 13:38:59 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 5\n", + "2026-06-04 13:38:59 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 1 tool web_search completed (\n", + "# Welcome to Morpheus Documentation#\n", + "\n", + "NVIDIA Morpheus\n", + "...)\n", + "2026-06-04 13:38:59 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 1 — iteration 6\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2025-47273', 'version': '78.1.1', 'package_name': 'setuptools'})\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: osv_lookup({'cve_id': 'CVE-2025-47273', 'version': '78.1.0', 'package_name': 'setuptools'})\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" EPSS score probability'})\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2025-47273 version=78.1.1 pkg=setuptools\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.tools.osv_tools:34 - osv_lookup: fetching https://api.osv.dev/v1/vulns/CVE-2025-47273\n", + "2026-06-04 13:39:04 - INFO - httpx:1025 - HTTP Request: GET https://api.osv.dev/v1/vulns/CVE-2025-47273 \"HTTP/1.1 200 OK\"\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.tools.osv_tools:52 - osv_lookup: CVE-2025-47273 version=78.1.0 pkg=setuptools\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool osv_lookup completed (CVE-2025-47273 | version 78.1.1 | NOT AFFECTED\n", + " 78.1.1 is NOT in the OSV affected versions list.)\n", + "2026-06-04 13:39:04 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool osv_lookup completed (CVE-2025-47273 | version 78.1.0 | AFFECTED\n", + " 78.1.0 is in the OSV affected versions list.\n", + " Nearest unaffected 78.1: ...)\n", + "2026-06-04 13:39:06 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "EPSS is a daily estimate of the probability of explo...)\n", + "2026-06-04 13:39:06 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 6\n", + "2026-06-04 13:39:11 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5'})\n", + "2026-06-04 13:39:11 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_search({'question': '\"CVE-2025-47273\" workaround mitigation'})\n", + "2026-06-04 13:39:11 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5\n", + "2026-06-04 13:39:13 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_search completed (\n", + "1 Applied a manual patch to remediate CVE-202...)\n", + "2026-06-04 13:39:14 - INFO - httpx:1025 - HTTP Request: GET https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5 \"HTTP/1.1 200 OK\"\n", + "2026-06-04 13:39:14 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (huntr - The world’s first bug bounty platform for AI/ML Bounties Partners Community Info SUBMIT REPORT Supported by  ...)\n", + "2026-06-04 13:39:14 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 7\n", + "2026-06-04 13:39:17 - INFO - vuln_analysis.utils.agentic_loop:150 - CVE Researcher tool: web_fetch({'url': 'https://access.redhat.com/security/cve/CVE-2025-47273'})\n", + "2026-06-04 13:39:17 - INFO - vuln_analysis.tools.web_tools:115 - web_fetch: https://access.redhat.com/security/cve/CVE-2025-47273\n", + "2026-06-04 13:39:17 - INFO - vuln_analysis.utils.agentic_loop:174 - CVE Researcher tool web_fetch completed (# CVE-2025-47273 - Red Hat Customer Portal\n", + "\n", + "[Skip to navigation](https://access.redhat.com/security/cve/CVE-2025-4727...)\n", + "2026-06-04 13:39:17 - INFO - vuln_analysis.utils.agentic_loop:205 - CVE Researcher — iteration 8\n", + "2026-06-04 13:39:22 - INFO - vuln_analysis.utils.agentic_loop:255 - Phase 1 finished (no more tool calls).\n", + "2026-06-04 13:39:22 - INFO - vuln_analysis.utils.web_researcher:56 - Phase 1 complete (8 tool calls). Summary length: 5046 chars\n", + "2026-06-04 13:39:22 - INFO - vuln_analysis.functions.container_analyzer:101 - Phase 2: starting filesystem analysis\n", + "2026-06-04 13:39:22 - INFO - vuln_analysis.tools.filesystem_tools:56 - Filesystem tools: configured allowed roots=[PosixPath('/.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs')], files=[], fs_root=None\n", + "2026-06-04 13:39:22 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 1\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/etc/os-release'})\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/docker/entrypoint.sh'})\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace'})\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs'})\n", + "2026-06-04 13:39:29 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/etc/os-release\n", + "2026-06-04 13:39:29 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/docker/entrypoint.sh\n", + "2026-06-04 13:39:29 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace\n", + "2026-06-04 13:39:29 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/opt/conda/envs\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (PRETTY_NAME=\"Ubuntu 22.04.5 LTS\"\n", + "NAME=\"Ubuntu\"\n", + "VERSION_ID=\"22.04\"\n", + "VERSION=\"22.04.5 LTS (Jammy Jellyfish)\"\n", + "VERSION_COD...)\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed (#!/bin/bash --login\n", + "# SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights re...)\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool list_directory completed (Directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs...)\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool list_directory completed (Directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs...)\n", + "2026-06-04 13:39:29 - INFO - vuln_analysis.utils.agentic_loop:205 - Phase 2 — iteration 2\n", + "2026-06-04 13:39:35 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: read_file({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/README.md'})\n", + "2026-06-04 13:39:35 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples'})\n", + "2026-06-04 13:39:35 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/docs'})\n", + "2026-06-04 13:39:35 - INFO - vuln_analysis.utils.agentic_loop:150 - Phase 2 tool: list_directory({'path': '.cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/conda/environments'})\n", + "2026-06-04 13:39:35 - INFO - filesystem_tools:135 - read_file: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/README.md\n", + "2026-06-04 13:39:35 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/examples\n", + "2026-06-04 13:39:35 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/docs\n", + "2026-06-04 13:39:35 - INFO - filesystem_tools:164 - list_directory: .cache/vuln_analysis/images/sha256:d0e62171420ee98d30ecd40958ecb08695f0a28e49c8cb2e3a478f70ee8325bf/rootfs/workspace/conda/environments\n", + "2026-06-04 13:39:35 - INFO - vuln_analysis.utils.agentic_loop:174 - Phase 2 tool read_file completed ( Stage1["stage1_parallel node"] - Stage1 -->|"asyncio.gather"| CVERes["cve_researcher fn"] - Stage1 -->|"asyncio.gather"| ContAn["container_analyzer fn"] - Stage1 -->|check success| Cond1{"Stage 1 OK?"} - Cond1 -->|no| FailSafe["fail_safe node"] - Cond1 -->|yes| Stage2["exploitability_analyzer fn"] - Stage2 -->|check success| Cond2{"Stage 2 OK?"} - Cond2 -->|no| FailSafe - Cond2 -->|yes| Stage3["vex_categorizer fn"] - Stage3 --> OutputNode["output_results node"] - FailSafe --> OutputNode - OutputNode --> Result["PipelineResult"] - end - - subgraph natComponents [NAT Components] - LLM["llms: nim LLM config"] - Tools["tools: web, filesystem, binary, osv"] - Builder["Builder wires LLMs + tools into functions"] - end - - Builder --> CVERes - Builder --> ContAn - Builder --> Stage2 - Builder --> Stage3 -``` - - - -## 0. Removals -- old vuln_analysis code that no longer applies - -The following files/directories inside `src/vuln_analysis/` are **deleted** because they implement the old workflow and are replaced by the cve-agent-v3 pipeline: - -**Entire directories to delete:** - -- `functions/` -- all 10 `cve_*.py` files (cve_agent, cve_generate_vdbs, cve_fetch_intel, cve_process_sbom, cve_check_vuln_deps, cve_checklist, cve_summarize, cve_justify, cve_file_output, cve_http_output) -- `tools/` -- local_vdb.py, serp.py, lexical_full_search.py (replaced by new tool set) -- `test_time_compute/` -- execute_then_select_function.py, majority_voting_selector.py (TTC tied to old output schema) -- `eval/` -- parse_eval_input.py, evaluators/accuracy.py, evaluators/consistency.py, visualizations/ (evaluators tied to old AgentMorpheusOutput) -- `configs/` -- all old YAML files (config.yml, config-eval.yml, config-ttc.yml, config-tracing.yml) and openapi/ (replaced by new configs) -- `data/` -- input_messages/, sboms/, profiler_datasets/, eval_datasets/ (old sample data) -- `data_models/` -- all old models (common.py, input.py, output.py, state.py, cve_intel.py, dependencies.py, info.py, vdb_type.py, eval_input.py) -- replaced by new cve-agent-v3 schemas - -**Individual files in `utils/` to delete:** - -- `prompting.py` -- old agent/summary/checklist prompts -- `checklist_prompt_generator.py` -- checklist generation logic -- `justification_parser.py` -- label/status parsing for old workflow -- `llm_engine_utils.py` -- skip reasons, preprocess/postprocess tied to old state -- `output_formatter.py` -- markdown report tied to AgentMorpheusOutput -- `intel_retriever.py` -- NVD/GHSA/RHSA/Ubuntu/EPSS orchestrator (replaced by cve_researcher agent) -- `vulnerable_dependency_checker.py` -- deps.dev checker (replaced by cve_researcher) -- `document_embedding.py` -- FAISS VDB builder (not used in new pipeline) -- `full_text_search.py` -- Tantivy indexer (not used in new pipeline) -- `source_code_git_loader.py` -- git blob loader (not used in new pipeline) -- `js_extended_parser.py` -- JS parser for VDB (not used in new pipeline) -- `serp_api_wrapper.py` -- SerpAPI wrapper (replaced by Tavily) -- `intel_utils.py` -- NVD/CVE version parsing (replaced by cve_researcher) -- `clients/` -- nvd_client.py, ghsa_client.py, rhsa_client.py, ubuntu_client.py, first_client.py, intel_client.py (replaced by cve_researcher agent) - -**Old `register.py`** -- deleted and replaced with new pipeline registration. - -**Kept utils** (generic, reusable across workflows): - -- `string_utils.py` -- CVE/GHSA ID validation, list parsing -- `concurrency.py` -- rate limiting, file locking -- `error_utils.py` -- auth error detection -- `nat_logging_patch.py` -- NAT logging enhancement -- `http_utils.py` -- sync HTTP with retries -- `async_http_utils.py` -- async HTTP with retries -- `url_utils.py` -- URL joining -- `data_utils.py` -- serialization helpers -- `git_utils.py` -- GitPython repo helper - -## 1. Project Restructuring - -Replace the contents of `src/vuln_analysis/` with the new cve-agent-v3 pipeline. The package name and NAT entry point stay the same (`vuln_analysis`). - -**New layout of `src/vuln_analysis/` after migration:** - -``` -src/vuln_analysis/ - __init__.py # (kept, unchanged) - register.py # NAT entry point (replaced) - configs/ - config.yml # full pipeline - config-cve-researcher.yml # individual agent - config-container-analyzer.yml - config-exploitability-analyzer.yml - config-vex-categorizer.yml - data/ - examples/ # sample inputs for validation - data_models/ - __init__.py - input.py # PipelineInput (Pydantic) - output.py # PipelineResult, StageResult (Pydantic) - state.py # PipelineState (TypedDict for LangGraph) - cve_researcher.py # CVEResearcherInput, CVEResearcherResult - container_analyzer.py # ContainerAnalyzerInput, ContainerAnalyzerResult - exploitability_analyzer.py # ExploitabilityAnalyzerInput, ExploitabilityAnalyzerResult - vex_categorizer.py # VEXCategorizerInput, VEXCategorizerResult - functions/ - __init__.py - cve_researcher.py # @register_function - container_analyzer.py - exploitability_analyzer.py - vex_categorizer.py - tools/ - __init__.py - web_tools.py # web_fetch - osv_tools.py # osv_lookup - filesystem_tools.py # read_file, list_directory, grep_search, glob_find - binary_tools.py # check_binary_dependencies, search_binary_content, check_distro_patches, get_binary_linked_libraries - package_tools.py # 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 - prompts/ - __init__.py - cve_researcher.py # system prompts, user message builders - container_analyzer.py - exploitability_analyzer.py - vex_categorizer.py - utils/ - __init__.py - # --- kept from old vuln_analysis --- - string_utils.py - concurrency.py - error_utils.py - nat_logging_patch.py - http_utils.py - async_http_utils.py - url_utils.py - data_utils.py - git_utils.py - # --- new, ported from cve-agent-v3 --- - agentic_loop.py # Adapted BaseAgent loop logic - pre_triage.py # Deterministic pre-triage (from exploitability_analyzer) - report_parser.py # extract_markdown_table_field - report_generator.py # Early-exit report templates - helpers.py # estimate_message_tokens, truncate, extract_final_response - web_researcher.py # WebResearchSubAgent (Phase 1 of container analyzer) -``` - -**pyproject.toml changes** (modify in place): - -1. **Keep the existing entry point** (no new entry point needed): - -```toml -[project.entry-points.'nat.components'] -vuln_analysis = "vuln_analysis.register" -``` - -2. **Update `nvidia-nat` version** from `~=1.4.0` to `==1.5.0`: - -```toml -"nvidia-nat[async_endpoints,langchain,phoenix,profiling]==1.5.0", -``` - -3. **Remove `langchain-classic`** from dependencies (keep `langchain` and `langchain-core`). - -4. **Update Python version** from `>=3.11,<3.13` to `>=3.13,<3.14`: - -```toml -requires-python = ">=3.13,<3.14" -``` - -5. **Add new dependencies** not already present: - -```toml -"langgraph>=1.0.5,<2.0.0", -"httpx>=0.27", -"tavily-python>=0.5.0", -"packaging>=24.0", -``` - -6. **Remove old dependencies** no longer needed (verify each before removing): - - `tantivy` (Tantivy full-text search -- replaced) - - `faiss-cpu` / `faiss-gpu` (FAISS VDB -- replaced) - - `google-search-results` (SerpAPI -- replaced by Tavily) - - Any other deps exclusive to the old workflow - -Port source code from the cve-agent-v3 repo's `cve_agent/` into `src/vuln_analysis/`. - ---- - -## 2. Data Models - -Port existing dataclass schemas to Pydantic models. The LangGraph state uses `TypedDict`. - -**`data_models/state.py`** -- LangGraph pipeline state: - -```python -from typing import TypedDict -from vuln_analysis.data_models.output import StageResult - -class PipelineState(TypedDict): - input: dict # serialized PipelineInput - cve_report: StageResult | None - container_report: StageResult | None - exploit_report: StageResult | None - vex_result: StageResult | None - vex_status: str | None - vex_justification: str | None - success: bool -``` - -**`data_models/input.py`** -- port `PipelineInput` from `orchestrator/schemas.py` to Pydantic `BaseModel`. Same for per-agent input schemas. - -**`data_models/output.py`** -- port `StageResult` and `PipelineResult` to Pydantic. Add `model_dump_json()` converter for NAT's `FunctionInfo.from_fn` output. - ---- - -## 3. Shared Agentic Loop - -Port `BaseAgent._run_agentic_loop` from `shared/base_agent.py` into a standalone async utility in `utils/agentic_loop.py`. Instead of using `AsyncOpenAI` directly, it takes a LangChain `BaseChatModel` (obtained from `builder.get_llm()`) and uses `model.bind_tools(tool_defs).ainvoke(messages)`. - -**`utils/agentic_loop.py`** -- key function: - -```python -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 = "", -) -> tuple[str, int, int]: - """Run the tool-calling loop. Returns (final_content, tool_calls, llm_calls).""" -``` - -Preserves from `BaseAgent`: - -- **Context compaction** (`_compact_context`) -- shrinks old tool/assistant messages when nearing token limit -- **Deadline nudge** (`_deadline_nudge`) -- wrap-up message at `wrap_up_fraction` of max iterations -- **Loop detection** -- skip repeated identical tool calls after threshold -- **LLM retry** -- exponential backoff on 429/5xx (handled by LangChain's retry or custom wrapper) -- **Empty response nudge** -- re-prompts on empty assistant content - -Adaptation: convert between LangChain `BaseMessage` types and the loop logic. LangChain's `AIMessage.tool_calls` provides `[{"name", "args", "id"}]` -- same information as OpenAI's format. - -### NAT intermediate steps for observability - -The agentic loop emits NAT intermediate steps so that `nat serve` can stream per-iteration progress and tracing backends (Phoenix) capture individual tool calls. Uses NAT's `IntermediateStepManager` via `SyncBuilder.current().context` to record each iteration: - -```python -from nat.builder import SyncBuilder - -async def run_agentic_loop(...) -> tuple[str, int, int]: - ctx = SyncBuilder.current().context - step_mgr = ctx.intermediate_step_manager - - for iteration in range(max_iterations): - # ... LLM call ... - step_mgr.add_step( - agent_action=f"[{phase_label}] LLM call #{iteration + 1}", - observation=_summarize_response(ai_message), - ) - - for tool_call in ai_message.tool_calls: - result = await _execute_tool(tool_call, tools) - step_mgr.add_step( - agent_action=f"[{phase_label}] tool:{tool_call['name']}", - observation=_truncate(result, max_len=500), - ) -``` - ---- - -## 4. Tool Registration - -Each tool is registered as its own `@register_function` with its own config class, yielding exactly **one** `FunctionInfo` per registration. Default values come from the cve-agent-v3 config files. - -> **CRITICAL -- One tool per `@register_function`:** NAT only uses the first yielded `FunctionInfo` per registration and silently ignores the rest. Do NOT bundle multiple tools under one `@register_function`. Use a shared base config class to avoid duplicating field definitions. - -> **CRITICAL -- No `from __future__ import annotations`:** NAT's `FunctionInfo.from_fn` calls `typing.get_type_hints()` on inner functions. With `from __future__ import annotations`, annotations become lazy strings that can't be resolved in nested closures, causing `NameError`. Since we target Python 3.13, the `X | Y` syntax works natively. This applies to all files under `tools/` and `functions/`. - -> **CRITICAL -- At least one parameter per tool:** NAT's `FunctionInfo.from_fn` requires every tool function to have at least one parameter. For tools that logically take no arguments, add a dummy parameter with a default: `scope: str = "all"`. - -**`tools/web_tools.py`** -- web_fetch tool: - -```python -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): - async def _arun(url: str) -> str: - ... - yield FunctionInfo.from_fn(_arun, description="Fetch and extract content from a URL") -``` - -For **web_search**, use NAT's built-in `tavily_internet_search` tool in config.yml. Override defaults: `search_depth: advanced`, `max_results: 5`. - -**`tools/osv_tools.py`** -- osv_lookup: port from `shared/tools.py`. - -**`tools/filesystem_tools.py`** -- read_file, list_directory, grep_search, glob_find: port from `shared/filesystem_tools.py`. Each tool gets its own `@register_function` with a shared base config class. Includes `contextvars.ContextVar`-based path sandboxing (see below): - -```python -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_glob_results: int = Field(default=200) - -class ReadFileConfig(_FsBaseConfig, name="read_file"): pass -class ListDirectoryConfig(_FsBaseConfig, name="list_directory"): pass -class GrepSearchConfig(_FsBaseConfig, name="grep_search"): pass -class GlobFindConfig(_FsBaseConfig, name="glob_find"): pass -``` - -### Filesystem path sandboxing with `contextvars.ContextVar` - -In cve-agent-v3, each agent owned its own `FilesystemToolkit` instance and called `set_allowed_paths()` directly. In NAT, tools are registered globally and shared. The solution is a `contextvars.ContextVar` bridge in `filesystem_tools.py`: - -1. **`configure_allowed_paths(roots, files, filesystem_root)`** -- module-level function that stores the config in a `ContextVar`. Agent functions call this before their agentic loop. -2. **`get_filesystem_root()`** -- module-level function that reads the filesystem root. Used by `binary_tools.py` and `package_tools.py`. -3. **`FilesystemToolkit._validate_path()`** -- falls through to the `ContextVar` when instance-level config hasn't been set. - -**`tools/binary_tools.py`** -- port from `exploitability_analyzer/tools.py`: - -```python -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 -class SearchBinaryContentConfig(_BinaryBaseConfig, name="search_binary_content"): pass -class CheckDistroPatchesConfig(_BinaryBaseConfig, name="check_distro_patches"): pass -class GetBinaryLinkedLibsConfig(_BinaryBaseConfig, name="get_binary_linked_libraries"): pass -``` - -**`tools/package_tools.py`** -- port from `exploitability_analyzer/tools.py`. Package database, library discovery, and security control tools. Each uses `get_filesystem_root()`: - -| Tool Name | Purpose | -|-----------|---------| -| `query_dpkg_database` | Search dpkg/status for Debian/Ubuntu packages | -| `search_dpkg_file_lists` | Find which package owns a file | -| `query_rpm_database` | Search RPM database for RHEL/Fedora/SUSE packages | -| `list_python_packages` | Enumerate pip packages across all environments | -| `find_shared_libraries` | Locate .so files by name pattern | -| `find_go_binaries` | Find Go binaries and list embedded modules | -| `check_security_controls` | Detect AppArmor, seccomp, non-root users | -| `query_npm_packages` | Find Node.js/npm packages | -| `find_java_packages` | Locate JARs and extract manifest/maven metadata | -| `list_installed_packages` | Aggregate summary of all package ecosystems | - -Import all tool modules in `register.py` to trigger `@register_function` decorators: - -```python -from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_tools, package_tools -``` - -### Complete tool inventory - -All 21 tools across 5 files, plus 1 NAT built-in: - -| File | Tools | -|------|-------| -| NAT built-in | `tavily_internet_search` (configured as `web_search` in YAML) | -| `tools/web_tools.py` | `web_fetch` | -| `tools/osv_tools.py` | `osv_lookup` | -| `tools/filesystem_tools.py` | `read_file`, `list_directory`, `grep_search`, `glob_find` | -| `tools/binary_tools.py` | `check_binary_dependencies`, `search_binary_content`, `check_distro_patches`, `get_binary_linked_libraries` | -| `tools/package_tools.py` | `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` | - -Agent tool assignments in YAML configs: - -| Agent | Tools | -|-------|-------| -| `cve_researcher` | `web_search`, `web_fetch`, `osv_lookup` (3 tools) | -| `container_analyzer` (Phase 1) | `web_search`, `web_fetch` (2 tools) | -| `container_analyzer` (Phase 2) | `read_file`, `list_directory`, `grep_search`, `glob_find` (4 tools) | -| `exploitability_analyzer` | All 18 filesystem/binary/package tools | -| `vex_categorizer` | `reason` (LangChain `@tool`, not a NAT registration) | - ---- - -## 5. Agent Functions - -Each agent is a `@register_function` that: - -1. Gets an LLM from `builder.get_llm(wrapper_type=LLMFrameworkEnum.LANGCHAIN)` -2. Gets tools from `builder.get_tools(wrapper_type=LLMFrameworkEnum.LANGCHAIN)` -3. Runs the agentic loop from `utils/agentic_loop.py` -4. Returns a typed result -5. Provides `converters` for standalone `nat run` (string-to-model and model-to-string) - -> **CRITICAL -- Converters are required:** When an agent is used as the standalone `workflow:` entry point (`nat run --config_file=config-cve-researcher.yml --input='...'`), NAT passes the raw CLI `--input` string to `ainvoke`. Without converters, NAT can't convert the JSON string into the Pydantic model, causing `TypeError`. Add `str → Model` and `Model → str` converters to every `FunctionInfo.from_fn` call. - -All default values come directly from the corresponding `config.py` in cve-agent-v3. - -**`functions/cve_researcher.py`**: - -```python -class CVEResearcherConfig(FunctionBaseConfig, name="cve_researcher"): - llm_name: str = Field(description="LLM for CVE research") - tool_names: list[FunctionRef] = Field(description="Tools: web_search, web_fetch, osv_lookup") - max_iterations: int = Field(default=30) - context_window_tokens: int = Field(default=262144) - wrap_up_fraction: float = Field(default=0.8) - loop_detection_threshold: int = Field(default=2) - output_subdir: str = Field(default="vulnerability_reports") - description: str = Field(default="Researches a CVE and produces a vulnerability report") - -@register_function(config_type=CVEResearcherConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_researcher(config: CVEResearcherConfig, 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) - - async def _arun(inp: CVEResearcherInput) -> CVEResearcherResult: - messages = [SystemMessage(content=SYSTEM_PROMPT), HumanMessage(content=build_user_message(inp))] - content, tool_calls, llm_calls = await run_agentic_loop( - llm=llm, messages=messages, tools=tools, - max_iterations=config.max_iterations, - context_window_tokens=config.context_window_tokens, - wrap_up_fraction=config.wrap_up_fraction, - loop_threshold=config.loop_detection_threshold, - phase_label="CVE Researcher", - ) - return CVEResearcherResult(identifier=inp.identifier, report_content=content, success=True, - tool_call_count=tool_calls, llm_call_count=llm_calls) - - def _str_to_input(s: str) -> CVEResearcherInput: - return CVEResearcherInput.model_validate_json(s) - def _result_to_str(r: CVEResearcherResult) -> str: - return r.model_dump_json() - - yield FunctionInfo.from_fn(_arun, input_schema=CVEResearcherInput, description=config.description, - converters=[_str_to_input, _result_to_str]) -``` - -**`functions/container_analyzer.py`** -- two-phase approach: - -- Phase 1 (web research): runs only for images matching `public_registry_prefixes`; uses `phase1_llm_name` and `web_tool_names` -- Phase 2 (filesystem analysis): always runs; uses `phase2_llm_name` and `fs_tool_names` -- Calls `configure_allowed_paths(roots=[inp.filesystem_path])` before Phase 2 -- Port `WebResearchSubAgent` into `utils/web_researcher.py` - -**`functions/exploitability_analyzer.py`** -- two-phase approach: - -- Phase 1: Deterministic pre-triage (no LLM). Uses `discovery_probe_paths` for filesystem probing. -- If pre-triage doesn't early-exit, Phase 2 runs LLM agentic loop with all 18 filesystem/binary/package tools -- Calls `configure_allowed_paths(roots=[...], files=[...], filesystem_root=...)` before Phase 2 -- Early-exit generates template report via `report_generator.py` - -**`functions/vex_categorizer.py`** -- LLM-only agent using `reason` tool (scratchpad). Reads exploitability and container reports, produces VEX status/justification. - ---- - -## 6. Workflow Function (Orchestrator) - -The main workflow uses `@register_function` with LangGraph `StateGraph` inline. - -> **CRITICAL -- Bare yield only:** Do NOT wrap the `yield FunctionInfo.from_fn(...)` in `try/except GeneratorExit` or `try/finally`. This violates the async generator protocol and causes `RuntimeError: generator didn't stop`. - -**`register.py`:** - -```python -class CVEPipelineWorkflowConfig(FunctionBaseConfig, name="cve_pipeline"): - cve_researcher_name: FunctionRef - container_analyzer_name: FunctionRef - exploitability_analyzer_name: FunctionRef - vex_categorizer_name: FunctionRef - stage_timeout: int = Field(default=600) - stage_max_retries: int = Field(default=1) - 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): - cve_researcher_fn = await builder.get_function(name=config.cve_researcher_name) - container_analyzer_fn = await builder.get_function(name=config.container_analyzer_name) - exploitability_fn = await builder.get_function(name=config.exploitability_analyzer_name) - vex_fn = await builder.get_function(name=config.vex_categorizer_name) - - # --- LangGraph node functions --- - async def stage1_parallel(state: PipelineState) -> dict: - results = await asyncio.gather( - _run_with_retry(cve_researcher_fn, make_cve_input(state), config), - _run_with_retry(container_analyzer_fn, make_container_input(state), config), - return_exceptions=True, - ) - cve_result = _result_or_failure(results[0], "cve_researcher") - container_result = _result_or_failure(results[1], "container_analyzer") - return {"cve_report": cve_result, "container_report": container_result} - - # ... exploitability_node, vex_node, fail_safe_node, output_results_node ... - - # --- Build StateGraph --- - graph_builder = StateGraph(PipelineState) - # ... add nodes and edges ... - graph = graph_builder.compile() - - async def _response_fn(inp: PipelineInput) -> PipelineResult: - initial_state = build_initial_state(inp) - result = await graph.ainvoke(initial_state) - return PipelineResult(**result) - - # Bare yield -- no try/finally - yield FunctionInfo.from_fn( - _response_fn, input_schema=PipelineInput, description=config.description, - converters=[str_to_pipeline_input, pipeline_result_to_str] - ) -``` - -**`register.py` imports** (triggers `@register_function` decorators): - -```python -from vuln_analysis.functions import cve_researcher, container_analyzer, exploitability_analyzer, vex_categorizer -from vuln_analysis.tools import web_tools, osv_tools, filesystem_tools, binary_tools, package_tools -``` - ---- - -## 7. Configuration Files - -All default values are sourced from the cve-agent-v3 config files. - -> **CRITICAL -- No YAML anchors under `functions:`:** NAT treats every key under `functions:` as a function definition and requires a `_type` discriminator. YAML anchors placed there become phantom entries that NAT can't parse. Omit config values and let Pydantic defaults apply, or repeat values explicitly. - -See the actual YAML files in `src/vuln_analysis/configs/` for the full configuration. Key points: - -- 5 separate LLM configs (different temperature/max_tokens per agent) -- Each tool listed individually under `functions:` with just its `_type` -- Individual agent configs for standalone `nat run` (workflow section references the agent directly) - -Add `data/examples/` with sample JSON inputs for validation and onboarding: - -- `pipeline_input.json` -- full pipeline input -- `cve_researcher_input.json` -- standalone CVE researcher -- `container_analyzer_input.json` -- standalone container analyzer -- `exploitability_analyzer_input.json` -- standalone exploitability analyzer -- `vex_categorizer_input.json` -- standalone VEX categorizer - -```bash -# Full pipeline -nat run --config_file=configs/config.yml --input_file=data/examples/pipeline_input.json - -# Individual agent -nat run --config_file=configs/config-cve-researcher.yml --input='{"cve_id":"CVE-2025-47273"}' - -# Serve as HTTP API -nat serve --config_file=configs/config.yml --host 0.0.0.0 --port 8000 -``` - ---- - -## Migration Sequence - -Implement in this order so each step can be validated independently: - -1. **Remove old code** -- delete old workflow-specific files from `src/vuln_analysis/` -2. **Scaffold** -- update pyproject.toml, create new directory structure, write new register.py skeleton -3. **Data models** -- port all schemas to Pydantic under `src/vuln_analysis/data_models/` -4. **Utils** -- keep reusable utils, add agentic_loop.py, helpers.py, report_parser.py, pre_triage.py, report_generator.py, web_researcher.py -5. **Tools** -- register all 21 tool functions under `src/vuln_analysis/tools/` (one `@register_function` per tool) -6. **Agent: cve_researcher** -- simplest agent (web tools only), validate with `nat run` -7. **Agent: container_analyzer** -- two-phase, validate individually -8. **Agent: exploitability_analyzer** -- pre-triage + LLM with all 18 filesystem/binary/package tools, validate individually -9. **Agent: vex_categorizer** -- LLM-only, validate individually -10. **Workflow** -- LangGraph orchestrator with parallel Stage 1, validate full pipeline -11. **Serve** -- test `nat serve` with HTTP API - -After any fix to tool/function files, clear stale bytecode: - -```bash -find src/vuln_analysis -type d -name __pycache__ -exec rm -rf {} + -``` From 51d7c358972f275b08323ff77d4900b1a3dcd9c0 Mon Sep 17 00:00:00 2001 From: shawn-davis <12801620+shawn-davis@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:32:53 -0400 Subject: [PATCH 46/48] Updated eval workflows to work out of the box. --- README.md | 13 +- .../configs/config-cve-report-eval.yml | 2 +- src/vuln_analysis/configs/config-eval.yml | 2 +- .../cve_report_benchmark/CVE-2024-5187.md | 166 ++++++++++++++++++ .../eval_datasets/morpheus_benchmark_2506.csv | 60 +++---- .../data/examples/cve_researcher_input.json | 6 +- src/vuln_analysis/eval/parse_eval_input.py | 48 +++-- src/vuln_analysis/eval/sample_pb_eval.py | 12 +- tests/eval/test_parse_eval_input.py | 27 ++- 9 files changed, 261 insertions(+), 75 deletions(-) create mode 100644 src/vuln_analysis/data/eval_datasets/cve_report_benchmark/CVE-2024-5187.md diff --git a/README.md b/README.md index c2e78469a..6320c0169 100644 --- a/README.md +++ b/README.md @@ -159,13 +159,13 @@ This project uses **[uv](https://docs.astral.sh/uv/)** as its package manager. # Install uv (if not already installed) curl -LsSf https://astral.sh/uv/install.sh | sh +# Initialize repository submodules +git submodule update --init --recursive + # 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 - -# Initialize repository submodules -git submodule update --init --recursive ``` 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. @@ -522,9 +522,10 @@ nat eval --config_file=configs/config-eval.yml \ | Column | Description | |--------|-------------| | `vuln_id` | CVE or GHSA identifier | -| `image` | Full container image reference including tag; the parser splits this into `image_name` and `image_tag` | -| `filesystem_path` | Path to extracted filesystem | -| `image_config_path` | Path to image config JSON | +| `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 | 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. diff --git a/src/vuln_analysis/configs/config-cve-report-eval.yml b/src/vuln_analysis/configs/config-cve-report-eval.yml index a4142bd19..31cb56bfb 100644 --- a/src/vuln_analysis/configs/config-cve-report-eval.yml +++ b/src/vuln_analysis/configs/config-cve-report-eval.yml @@ -55,7 +55,7 @@ eval: max_concurrency: 2 dataset: _type: custom - file_path: path/to/cve_report_eval_dataset_directory # Directory containing generated and gold report markdown files. + file_path: data/eval_datasets/cve_report_benchmark function: vuln_analysis.eval.parse_cve_report_directory.parse_input evaluators: diff --git a/src/vuln_analysis/configs/config-eval.yml b/src/vuln_analysis/configs/config-eval.yml index a721d32ec..510149fbe 100644 --- a/src/vuln_analysis/configs/config-eval.yml +++ b/src/vuln_analysis/configs/config-eval.yml @@ -222,7 +222,7 @@ workflow: eval: general: - output_dir: ./.tmp/evaluators + output_dir: .tmp/evaluators max_concurrency: 2 dataset: _type: custom 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/morpheus_benchmark_2506.csv b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv index 868d8ef4a..184aee536 100644 --- a/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv +++ b/src/vuln_analysis/data/eval_datasets/morpheus_benchmark_2506.csv @@ -1,30 +1,30 @@ -vuln_id,image,filesystem_path,image_config_path,ground_truth_label -GHSA-53q9-r3pm-6pq6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -CVE-2025-4517,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -CVE-2025-13836,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-xv5p-fjw5-vrj6,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-wf7f-8fxf-xfxc,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-pgqp-8h46-6x4j,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-gm62-xv2j-4w53,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-8qvm-5x2c-j2w7,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-7f5h-v6xp-fcq8,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-6mq8-rvhq-8wgg,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-63vm-454h-vhhq,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-5rjg-fvgr-3xxf,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -GHSA-58pv-8j8x-9vj2,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-38jv-5279-wg99,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-2xpw-w6gg-jr37,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -GHSA-2qfp-q593-8484,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_present -GHSA-ph84-rcj2-fxxm,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -CVE-2024-9287,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -CVE-2025-4138,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -CVE-2025-4330,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -CVE-2025-4435,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,code_not_reachable -CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -CVE-2025-8194,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -CVE-2025-9230,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/path/to/container,/path/to/container,vulnerable -CVE-2025-48384,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/path/to/container,/path/to/container,code_not_reachable -CVE-2025-68973,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/path/to/container,/path/to/container,code_not_reachable -GHSA-58pv-8j8x-9vj2,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/path/to/container,/path/to/container,code_not_reachable -GHSA-5rjg-fvgr-3xxf,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/path/to/container,/path/to/container,code_not_reachable -GHSA-rqc4-2hc7-8c8v,nvcr.io/nvidia/morpheus/morpheus-tritonserver-models:25.06,/path/to/container,/path/to/container,code_not_reachable +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/cve_researcher_input.json b/src/vuln_analysis/data/examples/cve_researcher_input.json index d49baf1a6..331bf0f35 100644 --- a/src/vuln_analysis/data/examples/cve_researcher_input.json +++ b/src/vuln_analysis/data/examples/cve_researcher_input.json @@ -1,5 +1,5 @@ { - "cve_id": "CVE-2025-47273", - "package_hint": "setuptools", - "version_hint": "75.8.0" + "cve_id": "CVE-2024-5187", + "package_hint": "onnx", + "version_hint": "1.16.0" } diff --git a/src/vuln_analysis/eval/parse_eval_input.py b/src/vuln_analysis/eval/parse_eval_input.py index 929804df3..08ebbaba9 100644 --- a/src/vuln_analysis/eval/parse_eval_input.py +++ b/src/vuln_analysis/eval/parse_eval_input.py @@ -16,12 +16,11 @@ """Custom NAT dataset parser for benchmark CSVs. Reads a CSV with columns: - vuln_id, image, filesystem_path, image_config_path, ground_truth_label + vuln_id, image_name, image_tag, filesystem_path, image_config_path, + ground_truth_label -where ``image`` is a full ``name:tag`` reference (e.g. -``nvcr.io/nvidia/morpheus/morpheus:25.06-runtime``). The parser splits the -reference into the ``image_name`` and ``image_tag`` fields required by -``PipelineInput``. +``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:: @@ -44,32 +43,18 @@ _REQUIRED_COLUMNS = frozenset({ "vuln_id", - "image", - "filesystem_path", - "image_config_path", + "image_name", + "image_tag", "ground_truth_label", }) -def _split_image(image: str) -> tuple[str, str]: - """Split a ``name:tag`` image reference into its components. - - Uses ``rsplit`` so registry ports (``localhost:5000/foo:tag``) parse - correctly. Raises ``ValueError`` if no tag is present. - """ - final_component = image.rsplit("/", 1)[-1] - if ":" not in final_component: - raise ValueError(f"image reference must include a tag: {image!r}") - name, tag = image.rsplit(":", 1) - return name, tag - - def parse_input(file_path: Path, **kwargs) -> EvalInput: """Parse a benchmark CSV into NAT ``EvalInput``. Each CSV row maps to one ``EvalInputItem``: - * **id** — ``"{cve_id}__{image}"`` (unique per case) + * **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 @@ -80,15 +65,18 @@ def parse_input(file_path: Path, **kwargs) -> EvalInput: eval_items: list[EvalInputItem] = [] for row in rows: vuln_id_raw = row["vuln_id"] - pull_ref = row["image"] - image_name, image_tag = _split_image(pull_ref) + 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": row["filesystem_path"], - "image_config_path": row["image_config_path"] or None, + "filesystem_path": filesystem_path, + "image_config_path": image_config_path, } item_id = f"{vuln_id_raw}__{pull_ref}" @@ -101,6 +89,8 @@ def parse_input(file_path: Path, **kwargs) -> EvalInput: 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, }, @@ -123,5 +113,9 @@ def _load_csv(csv_path: Path) -> list[dict[str, str]]: f"CSV {csv_path} is missing required columns: {sorted(missing)}" ) for row in reader: - rows.append({k: v.strip() for k, v in row.items()}) + 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 index ef36f981d..adb7fced9 100644 --- a/src/vuln_analysis/eval/sample_pb_eval.py +++ b/src/vuln_analysis/eval/sample_pb_eval.py @@ -40,7 +40,8 @@ _CSV_COLUMNS = [ "vuln_id", - "image", + "image_name", + "image_tag", "filesystem_path", "image_config_path", "ground_truth_label", @@ -78,11 +79,12 @@ def _dir_name_from_image(pull_ref: str) -> str: return name_tag -def _require_tag(pull_ref: str) -> None: - """Raise if the image reference is missing a 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: @@ -139,7 +141,7 @@ def load_cases(xlsx_path: str, sheet_name: str, data_root: str) -> list[dict]: continue dir_name = _dir_name_from_image(pull_ref) - _require_tag(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") @@ -152,6 +154,8 @@ def load_cases(xlsx_path: str, sheet_name: str, data_root: str) -> list[dict]: 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, diff --git a/tests/eval/test_parse_eval_input.py b/tests/eval/test_parse_eval_input.py index 06a233b78..75fa8b746 100644 --- a/tests/eval/test_parse_eval_input.py +++ b/tests/eval/test_parse_eval_input.py @@ -18,13 +18,13 @@ from vuln_analysis.eval.parse_eval_input import parse_input -def test_parse_input_splits_combined_image_into_name_and_tag(tmp_path): +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,filesystem_path,image_config_path,ground_truth_label", - "CVE-2025-0001,nvcr.io/nvidia/morpheus/morpheus:25.06-runtime,/tmp/rootfs,,true", + "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", @@ -38,3 +38,24 @@ def test_parse_input_splits_combined_image_into_name_and_tag(tmp_path): 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"] == "" From 88ac1dc55294721b9467c21121d1292a504e2e4c Mon Sep 17 00:00:00 2001 From: shawn-davis <12801620+shawn-davis@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:23:04 -0400 Subject: [PATCH 47/48] Move CVE researcher into local NAT plugin Add a first-party vuln_analysis.plugins.cve_researcher plugin with the agent, prompts, state models, NAT registration, and compatibility exports from the previous AIQ bridge. Handle AIQ runtime API drift and NAT 1.7 annotation resolution so the cve_researcher function registers cleanly, then cover the plugin with focused registration and model tests. Update the aiq submodule to track NVIDIA-AI-Blueprints/aiq and refresh the pinned revision. --- .gitmodules | 2 +- aiq | 2 +- pyproject.toml | 1 + src/vuln_analysis/functions/cve_researcher.py | 236 +--------- src/vuln_analysis/plugins/__init__.py | 2 + .../plugins/cve_researcher/__init__.py | 22 + .../plugins/cve_researcher/agent.py | 382 ++++++++++++++++ .../plugins/cve_researcher/models/__init__.py | 167 +++++++ .../plugins/cve_researcher/models/state.py | 127 ++++++ .../plugins/cve_researcher/prompts.py | 416 ++++++++++++++++++ .../plugins/cve_researcher/register.py | 357 +++++++++++++++ tests/plugins/test_cve_researcher_plugin.py | 208 +++++++++ 12 files changed, 1691 insertions(+), 231 deletions(-) create mode 100644 src/vuln_analysis/plugins/__init__.py create mode 100644 src/vuln_analysis/plugins/cve_researcher/__init__.py create mode 100644 src/vuln_analysis/plugins/cve_researcher/agent.py create mode 100644 src/vuln_analysis/plugins/cve_researcher/models/__init__.py create mode 100644 src/vuln_analysis/plugins/cve_researcher/models/state.py create mode 100644 src/vuln_analysis/plugins/cve_researcher/prompts.py create mode 100644 src/vuln_analysis/plugins/cve_researcher/register.py create mode 100644 tests/plugins/test_cve_researcher_plugin.py diff --git a/.gitmodules b/.gitmodules index 569049cc8..f745ba698 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "aiq"] path = aiq - url = https://github.com/shawn-davis/aiq.git + url = https://github.com/NVIDIA-AI-Blueprints/aiq.git branch = develop diff --git a/aiq b/aiq index 49f9a1fda..444996351 160000 --- a/aiq +++ b/aiq @@ -1 +1 @@ -Subproject commit 49f9a1fdabbeae564b89f10a657a2f219fcb5507 +Subproject commit 444996351eca33cf1fb8b2fe70d0a446300510a4 diff --git a/pyproject.toml b/pyproject.toml index 24b097245..489348e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ classifiers = ["Programming Language :: Python"] [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. diff --git a/src/vuln_analysis/functions/cve_researcher.py b/src/vuln_analysis/functions/cve_researcher.py index 56b0ab931..c3f537211 100644 --- a/src/vuln_analysis/functions/cve_researcher.py +++ b/src/vuln_analysis/functions/cve_researcher.py @@ -1,231 +1,9 @@ -"""AI-Q CVE Researcher function for NAT. +"""Compatibility exports for the local CVE researcher NAT plugin.""" -This adapter keeps the vulnerability-analysis workflow contract while delegating -execution to the dedicated ``aiq_agent.agents.cve_researcher`` implementation. -""" +from vuln_analysis.plugins.cve_researcher.register import CVEResearcherConfig +from vuln_analysis.plugins.cve_researcher.register import cve_researcher -import logging -from typing import Any - -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 FunctionRef -from nat.data_models.function import FunctionBaseConfig -from pydantic import Field - -from vuln_analysis.data_models.cve_researcher import CVEResearcherInput -from vuln_analysis.data_models.cve_researcher import CVEResearcherResult - -logger = logging.getLogger(__name__) - - -class CVEResearcherConfig(FunctionBaseConfig, name="cve_researcher"): - """Configuration for the vulnerability-analysis AI-Q CVE researcher bridge.""" - - llm_name: str = Field(description="LLM for AI-Q CVE research") - tool_names: list[FunctionRef] = 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 AI-Q deep researcher wrapper.", - ) - researcher_llm_name: str | None = Field( - default=None, - description="Deprecated compatibility field. The dedicated AI-Q CVE researcher uses llm_name.", - ) - planner_llm_name: str | None = Field( - default=None, - description="Deprecated compatibility field. The dedicated AI-Q CVE researcher uses llm_name.", - ) - data_sources: list[str] | None = Field( - default=None, - description="Optional AI-Q data source IDs. Null uses all configured tools.", - ) - include_middleware: bool = Field( - default=True, - description="Whether to include the AI-Q CVE researcher's default DeepAgents middleware stack.", - ) - skills: dict[str, Any] | None = Field(default=None, description="Optional AI-Q DeepAgents skills config") - sandbox: dict[str, Any] | None = Field(default=None, description="Optional AI-Q DeepAgents sandbox config") - verbose: bool = Field(default=True) - output_subdir: str = Field(default="vulnerability_reports") - description: str = Field(default="Researches a CVE through NVIDIA AI-Q's dedicated CVE researcher") - - -def _load_aiq_cve_api() -> dict[str, Any]: - try: - from aiq_agent.agents.cve_researcher.agent import CVEResearcherAgent - from aiq_agent.agents.cve_researcher.models import CVEResearcherAgentState as AIQCVEResearcherAgentState - 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 - except ModuleNotFoundError as exc: - raise RuntimeError( - "The AI-Q CVE researcher requires the `aiq-agent` package with " - "`aiq_agent.agents.cve_researcher`. Install or update the AI-Q " - "submodule package before running `cve_researcher`." - ) from exc - - return { - "AIQCVEResearcherAgentState": AIQCVEResearcherAgentState, - "CVEResearcherAgent": CVEResearcherAgent, - "LLMProvider": LLMProvider, - "LLMRole": LLMRole, - "SandboxConfig": SandboxConfig, - "SkillsConfig": SkillsConfig, - "VerboseTraceCallback": VerboseTraceCallback, - "all_mapped_tools_filtered_out": all_mapped_tools_filtered_out, - "filter_tools_by_sources": filter_tools_by_sources, - "format_user_facing_tool_error": format_user_facing_tool_error, - "is_verbose": is_verbose, - "validate_tool_availability": validate_tool_availability, - } - - -def _to_aiq_state(inp: CVEResearcherInput, aiq_state_type: type[Any]) -> Any: - data = dict( - cve_id=inp.cve_id, - ghsa_id=inp.ghsa_id, - package_hint=inp.package_hint, - version_hint=inp.version_hint, - ) - if hasattr(aiq_state_type, "model_fields"): - data = { - key: value - for key, value in data.items() - if key in aiq_state_type.model_fields - } - return aiq_state_type.model_validate(data) - - -def _to_pipeline_result(identifier: str, result: Any) -> CVEResearcherResult: - if isinstance(result, CVEResearcherResult): - return result - - return CVEResearcherResult( - identifier=getattr(result, "identifier", identifier) or identifier, - success=getattr(result, "success", True), - error=getattr(result, "error", None), - report_content=getattr(result, "report_content", None), - report_path=getattr(result, "report_path", None), - tool_call_count=getattr(result, "tool_call_count", 0), - llm_call_count=getattr(result, "llm_call_count", 0), - ) - - -def _select_tools(tools: list[BaseTool], data_sources: list[str] | None, aiq: dict[str, Any]) -> list[BaseTool]: - if data_sources is None: - return tools - - selected_tools = aiq["filter_tools_by_sources"](tools, data_sources) - if aiq["all_mapped_tools_filtered_out"](tools, selected_tools, data_sources): - logger.warning("AI-Q CVE research received data_sources with no matching tools") - return selected_tools - - -def _runtime_config(config: CVEResearcherConfig, aiq: dict[str, Any]) -> tuple[Any, Any]: - skills = None - sandbox = None - - if config.skills is not None: - skills = aiq["SkillsConfig"].model_validate(config.skills) - if config.sandbox is not None: - sandbox = aiq["SandboxConfig"].model_validate(config.sandbox) - - return skills, sandbox - - -@register_function(config_type=CVEResearcherConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) -async def cve_researcher(config: CVEResearcherConfig, builder: Builder): - 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) - - async def _arun(inp: CVEResearcherInput) -> CVEResearcherResult: - identifier = inp.identifier - logger.info("Starting AI-Q CVE research for %s", identifier) - - try: - aiq = _load_aiq_cve_api() - - 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 AI-Q role LLM overrides for dedicated CVE researcher") - - selected_tools = _select_tools(tools, config.data_sources, aiq) - is_valid, _, unavailable_tools = aiq["validate_tool_availability"]( - selected_tools, - research_type="CVE research", - ) - if not is_valid: - error_msg = aiq["format_user_facing_tool_error"]("CVE research", unavailable_tools) - return CVEResearcherResult(identifier=identifier, success=False, error=error_msg) - - verbose = aiq["is_verbose"](config.verbose) - callbacks = [aiq["VerboseTraceCallback"]()] if verbose else [] - skills, sandbox = _runtime_config(config, aiq) - provider = aiq["LLMProvider"]() - provider.set_default(llm) - provider.configure(aiq["LLMRole"].ORCHESTRATOR, llm) - provider.configure(aiq["LLMRole"].RESEARCHER, llm) - - agent = aiq["CVEResearcherAgent"]( - llm_provider=provider, - tools=selected_tools, - max_iterations=config.max_iterations, - verbose=verbose, - callbacks=callbacks, - skills=skills, - sandbox=sandbox, - include_middleware=config.include_middleware, - ) - result = await agent.run(_to_aiq_state(inp, aiq["AIQCVEResearcherAgentState"])) - except Exception as exc: - logger.error("AI-Q CVE Researcher failed: %s", exc, exc_info=True) - return CVEResearcherResult(identifier=identifier, success=False, error=str(exc)) - - converted = _to_pipeline_result(identifier, result) - if converted.success and not converted.report_content: - return CVEResearcherResult( - identifier=identifier, - success=False, - error="No report content produced by AI-Q CVE researcher.", - tool_call_count=converted.tool_call_count, - llm_call_count=converted.llm_call_count, - ) - - logger.info( - "AI-Q 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() - - yield FunctionInfo.from_fn( - _arun, - input_schema=CVEResearcherInput, - description=config.description, - converters=[_str_to_input, _result_to_str], - ) +__all__ = [ + "CVEResearcherConfig", + "cve_researcher", +] 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..42dc5e7ed --- /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 = 30, + 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/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 From 9bca53283e37351a1ef9313d52bc9e1898023f17 Mon Sep 17 00:00:00 2001 From: shawn-davis <12801620+shawn-davis@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:30:10 -0400 Subject: [PATCH 48/48] Update NIM deployment defaults and VEX example docs Use explicit Docker Compose config/env-file commands in the deployment notebook, switch the default NIM LLM image to Nemotron, reduce CVE researcher default iterations, and document the CVE-2025-47273 VEX result for the automatic extraction example. --- README.md | 2 ++ deploy/1_Deploy_CVE.ipynb | 16 ++++++++++------ deploy/docker-compose.nim.yml | 2 +- .../plugins/cve_researcher/agent.py | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6320c0169..f2ee51941 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,8 @@ Supply `image_name` and `image_tag` in your `PipelineInput`: } ``` +For this CVE/image example, the generated VEX result is status `not_affected` with justification `false_positive`. + 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`. ### Option B — Pre-extract with the standalone helper diff --git a/deploy/1_Deploy_CVE.ipynb b/deploy/1_Deploy_CVE.ipynb index 335cb45ce..5e95e064f 100644 --- a/deploy/1_Deploy_CVE.ipynb +++ b/deploy/1_Deploy_CVE.ipynb @@ -246,9 +246,11 @@ "%%bash\n", "set -euo pipefail\n", "\n", - "docker compose build vuln-analysis\n", - "docker compose up -d vuln-analysis nginx-cache\n", - "docker compose ps\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" ] }, { @@ -271,10 +273,12 @@ "%%bash\n", "set -euo pipefail\n", "\n", - "if docker compose exec -T vuln-analysis pgrep -af '[n]at serve' >/dev/null; then\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", - " docker 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", + " \"${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" ] @@ -446,7 +450,7 @@ "## 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 exec vuln-analysis tail -n 100 /var/tmp/nat-serve.log`.\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" diff --git a/deploy/docker-compose.nim.yml b/deploy/docker-compose.nim.yml index fc033f09a..7cddd1bfe 100755 --- a/deploy/docker-compose.nim.yml +++ b/deploy/docker-compose.nim.yml @@ -25,7 +25,7 @@ services: - nim-llm 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 diff --git a/src/vuln_analysis/plugins/cve_researcher/agent.py b/src/vuln_analysis/plugins/cve_researcher/agent.py index 42dc5e7ed..ada5da4c3 100644 --- a/src/vuln_analysis/plugins/cve_researcher/agent.py +++ b/src/vuln_analysis/plugins/cve_researcher/agent.py @@ -204,7 +204,7 @@ def __init__( tools: Sequence[BaseTool] | None = None, *, llm: Any | None = None, - max_iterations: int = 30, + max_iterations: int = 2, verbose: bool = True, callbacks: list[Any] | None = None, skills: SkillsConfig | None = None,