diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5288ece..8b776bf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ This document provides guidance for GitHub Copilot when working with the Insight ## Project Overview -InsightVM-Python is a modern Python client library for Rapid7 InsightVM and Palo Alto Cortex XDR APIs. The project follows industry-standard patterns with comprehensive type hints and clean, intuitive interfaces. +InsightVM-Python is a modern Python client library for Rapid7 InsightVM API. The project follows industry-standard patterns with comprehensive type hints and clean, intuitive interfaces. **Version:** 2.0.0 **Python Support:** 3.8+ @@ -42,7 +42,6 @@ insightvm-python/ │ │ ├── scan_templates.py # Template management │ │ └── sonar_queries.py # Sonar integration │ └── tools/ # Standalone utility scripts -├── src/paloalto/ # Palo Alto Cortex XDR package ├── docs/ # Documentation ├── tests/ # Test suite │ ├── conftest.py # Shared pytest fixtures diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b90df1..6bb31e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to InsightVM-Python will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Removed +- 🔥 **BREAKING**: Removed Palo Alto Cortex XDR integration from main codebase + - Moved to dedicated development branch for independent development + - Main repository now focuses exclusively on Rapid7 InsightVM functionality + - Palo Alto XDR code available in git history (commit 559a63e and earlier) + ## [2.0.0] - 2025-10-07 ### 🎉 Major Release - Complete Architecture Refactoring @@ -166,7 +174,6 @@ This is a **major breaking release** with significant improvements to the codeba - Asset retrieval capabilities - Asset group creation - Database storage support (PostgreSQL) -- Palo Alto Cortex XDR integration - Agent installation tools - Manual Base64 authentication diff --git a/README.md b/README.md index 1fdf3d1..91032f7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![License](https://img.shields.io/badge/license-MIT-orange.svg) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/8676b480eff04517b65bc3bfcfeaea9a)](https://app.codacy.com/gh/talltechy/insightvm-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) -A modern Python client library for Rapid7 InsightVM and Palo Alto Cortex XDR APIs. Built with industry-standard patterns, comprehensive type hints, and a clean, intuitive interface. +A modern Python client library for Rapid7 InsightVM API. Built with industry-standard patterns, comprehensive type hints, and a clean, intuitive interface. ## ✨ Features @@ -471,7 +471,6 @@ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for gu - [Official Rapid7 InsightVM API](https://help.rapid7.com/insightvm/en-us/api/index.html) - [Official Rapid7 InsightVM API Examples](https://github.com/rapid7/insightvm-api-examples) -- [Palo Alto Cortex XDR API](https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR/Cortex-XDR-API-Reference/APIs-Overview) ## 📝 License diff --git a/docs/PALOALTO_XDR.md b/docs/PALOALTO_XDR.md new file mode 100644 index 0000000..5d76b61 --- /dev/null +++ b/docs/PALOALTO_XDR.md @@ -0,0 +1,76 @@ +# Palo Alto Cortex XDR Integration + +## Status: Moved to Separate Development Branch + +As of version 2.1, the Palo Alto Cortex XDR integration has been moved to a dedicated development branch to keep the main repository focused on Rapid7 InsightVM functionality. + +## Accessing the Palo Alto XDR Code + +### Option 1: Check Out from Git History + +The Palo Alto XDR code is available in the git history. You can access it by checking out a commit before the removal: + +```bash +# View the last commit that included Palo Alto code +git log --all --full-history -- src/paloalto/ + +# Check out the specific commit (replace with actual commit hash) +git checkout 559a63e -- src/paloalto/ +``` + +### Option 2: Browse on GitHub + +You can view the Palo Alto XDR code in the GitHub repository history: + +1. Go to the repository: https://github.com/talltechy/insightvm-python +2. Navigate to `src/paloalto/` in any commit before this change +3. View commit `559a63e` or earlier + +### Files Included + +The Palo Alto Cortex XDR integration consisted of: + +- `src/paloalto/__init__.py` - Package initialization +- `src/paloalto/api_pa_xdr.py` - Main API client with functions for: + - Incident management + - Endpoint management + - Alert management + - Endpoint isolation/quarantine operations +- `src/paloalto/api_pa_xdr_auth.py` - Authentication helper functions + +### Environment Variables Required + +If you restore the Palo Alto XDR code, you'll need these environment variables: + +```bash +XDR_API_KEY=your_xdr_api_key +XDR_API_KEY_ID=your_xdr_api_key_id +XDR_BASE_URL=https://api-your-region.xdr.us.paloaltonetworks.com +``` + +## Why Was This Moved? + +The Palo Alto Cortex XDR integration was moved to a separate development branch for the following reasons: + +1. **Focus**: Keep the main repository focused on Rapid7 InsightVM +2. **Maintainability**: Separate development cycles for different products +3. **Clarity**: Clear separation of concerns between different API integrations +4. **Independence**: Allow each integration to evolve independently + +## Future Plans + +The Palo Alto Cortex XDR integration may be: +- Developed as a separate package (`paloalto-cortex-xdr-python`) +- Maintained on a long-lived feature branch +- Released as a separate project + +## Questions? + +For questions about the Palo Alto XDR integration, please: +1. Check the git history for implementation details +2. Open an issue in the repository for discussion +3. Contact the maintainers if you're interested in continuing XDR development + +## References + +- [Palo Alto Cortex XDR API Documentation](https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR/Cortex-XDR-API-Reference/APIs-Overview) diff --git a/src/paloalto/__init__.py b/src/paloalto/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/paloalto/api_pa_xdr.py b/src/paloalto/api_pa_xdr.py deleted file mode 100644 index 73478fb..0000000 --- a/src/paloalto/api_pa_xdr.py +++ /dev/null @@ -1,455 +0,0 @@ -""" -This module provides functions to interact with the Cortex XDR API. - -Functions: -- get_incidents(query: Optional[str] = None) -> List[dict]: Retrieves a list of incidents from the Cortex XDR API. -- get_endpoints(query: Optional[str] = None) -> List[dict]: Retrieves a list of endpoints from the Cortex XDR API. -- get_alerts(query: Optional[str] = None) -> List[dict]: Retrieves a list of alerts from the Cortex XDR API. -- get_endpoint_details(endpoint_id: str) -> dict: Retrieves details for a specific endpoint from the Cortex XDR API. -- get_incident_details(incident_id: str) -> dict: Retrieves details for a specific incident from the Cortex XDR API. -- get_alert_details(alert_id: str) -> dict: Retrieves details for a specific alert from the Cortex XDR API. -- isolate_endpoint(endpoint_id: str) -> dict: Isolates an endpoint from the Cortex XDR API. -- unisolate_endpoint(endpoint_id: str) -> dict: Unisolates an endpoint from the Cortex XDR API. -- get_endpoint_quarantine_status(endpoint_id: str) -> dict: Retrieves the quarantine status for a specific endpoint from the Cortex XDR API. -- quarantine_endpoint(endpoint_id: str) -> dict: Quarantines an endpoint from the Cortex XDR API. -- unquarantine_endpoint(endpoint_id: str) -> dict: Unquarantines an endpoint from the Cortex XDR API. -- get_endpoint_network_details(endpoint_id: str) -> dict: Retrieves network details for a specific endpoint from the Cortex XDR API. -""" - -import json -import logging -from typing import Optional, List -import requests -from dotenv import load_dotenv -from src.paloalto.cortex_xdr.api_pa_xdr_auth import generate_advanced_authentication, load_xdr_api_credentials - -# Load environment variables from .env file -load_dotenv(".env") - -# Load the XDR API credentials -xdr_api_key, xdr_api_key_id, xdr_base_url = load_xdr_api_credentials() - -# Call the `generate_advanced_authentication()` function with the loaded credentials -auth_headers: dict[str, str] = generate_advanced_authentication( - api_key=xdr_api_key, api_key_id=xdr_api_key_id -) - -# Define the base URL for the Cortex XDR API -BASE_URL = xdr_base_url - -# Set up logging -logging.basicConfig(filename='api_pa_xdr.log', level=logging.ERROR) - -def get_incidents(query: Optional[str] = None) -> List[dict]: - """ - Retrieves a list of incidents from the Cortex XDR API. - - Args: - query: Optional string containing the query to filter the incidents. - - Returns: - List of incidents. - """ - # Define the endpoint URL - endpoint_url: str = f"{BASE_URL}/incidents" - - # Define the query parameters - params = {} - if query: - params["query"] = query - - try: - # Send the API request with a timeout of 10 seconds - response = requests.get( - endpoint_url, headers=auth_headers, params=params, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the list of incidents - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting incidents: %s", str(error)) - return [] - -def get_endpoints(query: Optional[str] = None) -> List[dict]: - """ - Retrieves a list of endpoints from the Cortex XDR API. - - Args: - query: Optional string containing the query to filter the endpoints. - - Returns: - List of endpoints. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints" - - # Define the query parameters - params = {} - if query: - params["query"] = query - - # Send the API request with a timeout of 10 seconds - response = requests.get( - endpoint_url, headers=auth_headers, params=params, timeout=10 - ) - - # Raise an exception if the request fails - response.raise_for_status() - - # Parse the response JSON - response_json = response.json() - - # Return the list of endpoints - return response_json["reply"] - -def get_alerts(query: Optional[str] = None) -> List[dict]: - """ - Retrieves a list of alerts from the Cortex XDR API. - - Args: - query: Optional string containing the query to filter the alerts. - - Returns: - List of alerts. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/alerts" - - # Define the query parameters - params = {} - if query: - params["query"] = query - - try: - # Send the API request with a timeout of 10 seconds - response = requests.get( - endpoint_url, headers=auth_headers, params=params, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the list of alerts - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting alerts: %s", error) - return [] - -def get_endpoint_details(endpoint_id: str) -> dict: - """ - Retrieves details for a specific endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to retrieve details for. - - Returns: - Dictionary containing the endpoint details. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/{endpoint_id}" - - try: - # Send the API request with a timeout of 10 seconds - response = requests.get( - endpoint_url, headers=auth_headers, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the endpoint details - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting endpoint details: %s", error) - return {} - -def get_incident_details(incident_id: str) -> dict: - """ - Retrieves details for a specific incident from the Cortex XDR API. - - Args: - incident_id: The ID of the incident to retrieve details for. - - Returns: - Dictionary containing the incident details. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/incidents/{incident_id}" - - try: - # Send the API request with a timeout of 10 seconds - response = requests.get( - endpoint_url, headers=auth_headers, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the incident details - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting incident details: %s", error) - return {} - -def get_alert_details(alert_id: str) -> dict: - """ - Retrieves details for a specific alert from the Cortex XDR API. - - Args: - alert_id: The ID of the alert to retrieve details for. - - Returns: - Dictionary containing the alert details. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/alerts/{alert_id}" - - try: - # Send the API request with a timeout of 10 seconds - response = requests.get( - endpoint_url, headers=auth_headers, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the alert details - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting alert details: %s", error) - return {} - -def isolate_endpoint(endpoint_id: str) -> dict: - """ - Isolates an endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to isolate. - - Returns: - Dictionary containing the response from the Cortex XDR API. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/isolate" - - # Define the request body - request_body = { - "request_data": { - "endpoint_id": endpoint_id - } - } - - try: - # Send the API request with a timeout of 10 seconds - response = requests.post( - endpoint_url, headers=auth_headers, json=request_body, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the response from the Cortex XDR API - return response_json - - except requests.exceptions.RequestException as error: - logging.error("Error isolating endpoint: %s", error) - return {} - -def unisolate_endpoint(endpoint_id: str) -> dict: - """ - Unisolates an endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to unisolate. - - Returns: - Dictionary containing the response from the Cortex XDR API. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/unisolate" - - # Define the request body - request_body = { - "request_data": { - "endpoint_id": endpoint_id - } - } - - try: - # Send the API request with a timeout of 10 seconds - response = requests.post( - endpoint_url, headers=auth_headers, json=request_body, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the response from the Cortex XDR API - return response_json - - except requests.exceptions.RequestException as error: - logging.error("Error unisolating endpoint: %s", error) - return {} - -def get_endpoint_quarantine_status(endpoint_id: str) -> dict: - """ - Retrieves the quarantine status for a specific endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to retrieve the quarantine status for. - - Returns: - Dictionary containing the quarantine status for the endpoint. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/quarantine-status" - - # Define the request body - request_body = { - "request_data": { - "endpoint_id": endpoint_id - } - } - - try: - # Send the API request with a timeout of 10 seconds - response = requests.post( - endpoint_url, headers=auth_headers, json=request_body, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the quarantine status for the endpoint - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting endpoint quarantine status: %s", error) - return {} - -def quarantine_endpoint(endpoint_id: str) -> dict: - """ - Quarantines an endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to quarantine. - - Returns: - Dictionary containing the response from the Cortex XDR API. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/quarantine" - - # Define the request body - request_body = { - "request_data": { - "endpoint_id": endpoint_id - } - } - - try: - # Send the API request with a timeout of 10 seconds - response = requests.post( - endpoint_url, headers=auth_headers, json=request_body, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the response from the Cortex XDR API - return response_json - - except requests.exceptions.RequestException as error: - logging.error("Error quarantining endpoint: %s", error) - return {} - -def unquarantine_endpoint(endpoint_id: str) -> dict: - """ - Unquarantines an endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to unquarantine. - - Returns: - Dictionary containing the response from the Cortex XDR API. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/unquarantine" - - # Define the request body - request_body = { - "request_data": { - "endpoint_id": endpoint_id - } - } - - try: - # Send the API request with a timeout of 10 seconds - response = requests.post( - endpoint_url, headers=auth_headers, json=request_body, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the response from the Cortex XDR API - return response_json - - except requests.exceptions.RequestException as error: - logging.error("Error unquarantining endpoint: %s", error) - return {} - -def get_endpoint_network_details(endpoint_id: str) -> dict: - """ - Retrieves network details for a specific endpoint from the Cortex XDR API. - - Args: - endpoint_id: The ID of the endpoint to retrieve network details for. - - Returns: - Dictionary containing the network details for the endpoint. - """ - # Define the endpoint URL - endpoint_url = f"{BASE_URL}/endpoints/network" - - # Define the request body - request_body = { - "request_data": { - "endpoint_id": endpoint_id - } - } - - try: - # Send the API request with a timeout of 10 seconds - response = requests.post( - endpoint_url, headers=auth_headers, json=request_body, timeout=10 - ) - response.raise_for_status() # Raise an exception if the response status code is not 200 - - # Parse the response JSON - response_json = json.loads(response.text) - - # Return the network details for the endpoint - return response_json["reply"] - - except requests.exceptions.RequestException as error: - logging.error("Error getting endpoint network details: %s", error) - return {} diff --git a/src/paloalto/api_pa_xdr_auth.py b/src/paloalto/api_pa_xdr_auth.py deleted file mode 100644 index cdb0400..0000000 --- a/src/paloalto/api_pa_xdr_auth.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -This module provides functions for generating advanced authentication headers -for Cortex XDR API requests. -""" - -import os -from datetime import datetime, timezone -import secrets -import hashlib -from typing import Optional -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv(".env") - - -def load_xdr_api_credentials(): - """ - Loads the XDR API credentials from environment variables. - - Returns: - Tuple containing the XDR API key and API key ID. - """ - xdr_api_key = os.getenv("XDR_API_KEY") - xdr_api_key_id = os.getenv("XDR_API_KEY_ID") - xdr_base_url = os.getenv("XDR_BASE_URL") - - if not xdr_api_key or not xdr_api_key_id: - raise ValueError("Missing XDR API credentials. Please check .env file.") - - return xdr_api_key, xdr_api_key_id, xdr_base_url - - -def generate_advanced_authentication( - api_key: str, api_key_id: str, payload: Optional[dict] = None -): - """ - Generates advanced authentication headers for Cortex XDR API requests. - - Args: - api_key: The XDR API key. - api_key_id: The XDR API key ID. - payload: Optional dictionary containing the request payload. - - Returns: - Dictionary containing the authentication headers. - """ - # Use empty dictionary as payload if payload is None - payload = payload or {} - - # Generate nonce value - nonce = secrets.token_urlsafe(64) - - # Generate timestamp string - timestamp_ms = int(datetime.now(timezone.utc).timestamp()) * 1000 - timestamp_str = f"{timestamp_ms}" - - # Generate authentication headers - auth_string = (api_key + nonce + timestamp_str).encode("utf-8") - auth_key = hashlib.sha256(auth_string).hexdigest() - headers = { - "Authorization": auth_key, - "x-xdr-nonce": nonce, - "x-xdr-timestamp": timestamp_str, - "x-xdr-auth-id": str(api_key_id), - } - return headers - - -# Load the XDR API credentials -# xdr_api_key, xdr_api_key_id, xdr_base_url = load_xdr_api_credentials() - -# Call the `generate_advanced_authentication()` function with the loaded credentials -# auth_headers: dict[str, str] = generate_advanced_authentication(api_key=xdr_api_key, api_key_id=xdr_api_key_id) - -# Print the authentication headers -# print(auth_headers) diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ac4d0b6 Binary files /dev/null and b/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc b/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000..1e06509 Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_api_standardization.cpython-312-pytest-8.4.2.pyc b/tests/__pycache__/test_api_standardization.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000..33da362 Binary files /dev/null and b/tests/__pycache__/test_api_standardization.cpython-312-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-8.4.2.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000..46442b7 Binary files /dev/null and b/tests/__pycache__/test_auth.cpython-312-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_client.cpython-312-pytest-8.4.2.pyc b/tests/__pycache__/test_client.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000..0bca8f6 Binary files /dev/null and b/tests/__pycache__/test_client.cpython-312-pytest-8.4.2.pyc differ diff --git a/tests/conftest.py b/tests/conftest.py index 021f460..aada7d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,9 +17,6 @@ "INSIGHTVM_VERIFY_SSL": "true", "INSIGHT_PLATFORM_API_KEY": "test_platform_key", "INSIGHT_PLATFORM_BASE_URL": "https://us.api.insight.rapid7.com", - "XDR_API_KEY": "test_xdr_key", - "XDR_API_KEY_ID": "test_xdr_key_id", - "XDR_BASE_URL": "https://api-test.xdr.us.paloaltonetworks.com", } diff --git a/tests/test_rapid7/__pycache__/__init__.cpython-312.pyc b/tests/test_rapid7/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7a9c8f8 Binary files /dev/null and b/tests/test_rapid7/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/test_rapid7/__pycache__/test_assets.cpython-312-pytest-8.4.2.pyc b/tests/test_rapid7/__pycache__/test_assets.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000..2d8a71b Binary files /dev/null and b/tests/test_rapid7/__pycache__/test_assets.cpython-312-pytest-8.4.2.pyc differ diff --git a/tests/test_rapid7/__pycache__/test_base.cpython-312-pytest-8.4.2.pyc b/tests/test_rapid7/__pycache__/test_base.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000..a141622 Binary files /dev/null and b/tests/test_rapid7/__pycache__/test_base.cpython-312-pytest-8.4.2.pyc differ