diff --git a/README.md b/README.md index 89ff153..29444ab 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,11 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branc [--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME] [--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] - [--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] [--version] + [--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] + [--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT] + [--reach-analysis-memory-limit REACH_ANALYSIS_MEMORY_LIMIT] [--reach-ecosystems REACH_ECOSYSTEMS] [--reach-exclude-paths REACH_EXCLUDE_PATHS] + [--reach-min-severity {low,medium,high,critical}] [--reach-skip-cache] [--reach-disable-analytics] [--reach-output-file REACH_OUTPUT_FILE] + [--only-facts-file] [--version] ```` If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY` @@ -160,6 +164,28 @@ If you don't want to provide the Socket API Token every time then you can use th | --allow-unverified | False | False | Allow unverified packages | | --disable-security-issue | False | False | Disable security issue checks | +#### Reachability Analysis +| Parameter | Required | Default | Description | +|:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------| +| --reach | False | False | Enable reachability analysis to identify which vulnerable functions are actually called by your code | +| --reach-version | False | latest | Version of @coana-tech/cli to use for analysis | +| --reach-analysis-timeout | False | 1200 | Timeout in seconds for the reachability analysis (default: 1200 seconds / 20 minutes) | +| --reach-analysis-memory-limit | False | 4096 | Memory limit in MB for the reachability analysis (default: 4096 MB / 4 GB) | +| --reach-ecosystems | False | | Comma-separated list of ecosystems to analyze (e.g., "npm,pypi"). If not specified, all supported ecosystems are analyzed | +| --reach-exclude-paths | False | | Comma-separated list of file paths or patterns to exclude from reachability analysis | +| --reach-min-severity | False | | Minimum severity level for reporting reachability results (low, medium, high, critical) | +| --reach-skip-cache | False | False | Skip cache and force fresh reachability analysis | +| --reach-disable-analytics | False | False | Disable analytics collection during reachability analysis | +| --reach-output-file | False | .socket.facts.json | Path where reachability analysis results should be saved | +| --only-facts-file | False | False | Submit only the .socket.facts.json file to an existing scan (requires --reach and a prior scan) | + +**Reachability Analysis Requirements:** +- `npm` - Required to install and run @coana-tech/cli +- `npx` - Required to execute @coana-tech/cli +- `uv` - Required for Python environment management + +The CLI will automatically install @coana-tech/cli if not present. Use `--reach` to enable reachability analysis during a full scan, or use `--only-facts-file` with `--reach` to submit reachability results to an existing scan. + #### Advanced Configuration | Parameter | Required | Default | Description | |:-------------------------|:---------|:--------|:----------------------------------------------------------------------| diff --git a/pyproject.toml b/pyproject.toml index 6b2ea18..f326d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.15" +version = "2.2.18" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ @@ -16,7 +16,8 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - 'socketdev>=3.0.6,<4.0.0' + 'socketdev>=3.0.6,<4.0.0', + "bs4>=0.0.2", ] readme = "README.md" description = "Socket Security CLI for CI/CD" @@ -158,4 +159,4 @@ docstring-code-format = false docstring-code-line-length = "dynamic" [tool.hatch.build.targets.wheel] -include = ["socketsecurity", "LICENSE"] \ No newline at end of file +include = ["socketsecurity", "LICENSE"] diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index c4b1ae5..a61cd78 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.15' +__version__ = '2.2.18' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index bbfb4bc..0fc5351 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -62,7 +62,19 @@ class CliConfig: save_manifest_tar: Optional[str] = None sub_paths: List[str] = field(default_factory=list) workspace_name: Optional[str] = None - + # Reachability Flags + reach: bool = False + reach_version: Optional[str] = None + reach_analysis_memory_limit: Optional[int] = None + reach_analysis_timeout: Optional[int] = None + reach_disable_analytics: bool = False + reach_ecosystems: Optional[List[str]] = None + reach_exclude_paths: Optional[List[str]] = None + reach_skip_cache: bool = False + reach_min_severity: Optional[str] = None + reach_output_file: Optional[str] = None + only_facts_file: bool = False + @classmethod def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': parser = create_argument_parser() @@ -110,6 +122,17 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'save_manifest_tar': args.save_manifest_tar, 'sub_paths': args.sub_paths or [], 'workspace_name': args.workspace_name, + 'reach': args.reach, + 'reach_version': args.reach_version, + 'reach_analysis_timeout': args.reach_analysis_timeout, + 'reach_analysis_memory_limit': args.reach_analysis_memory_limit, + 'reach_disable_analytics': args.reach_disable_analytics, + 'reach_ecosystems': args.reach_ecosystems.split(',') if args.reach_ecosystems else None, + 'reach_exclude_paths': args.reach_exclude_paths.split(',') if args.reach_exclude_paths else None, + 'reach_skip_cache': args.reach_skip_cache, + 'reach_min_severity': args.reach_min_severity, + 'reach_output_file': args.reach_output_file, + 'only_facts_file': args.only_facts_file, 'version': __version__ } try: @@ -141,6 +164,11 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': logging.error("--workspace-name requires --sub-path to be specified") exit(1) + # Validate that only_facts_file requires reach + if args.only_facts_file and not args.reach: + logging.error("--only-facts-file requires --reach to be specified") + exit(1) + return cls(**config_args) def to_dict(self) -> dict: @@ -474,6 +502,78 @@ def create_argument_parser() -> argparse.ArgumentParser: help="Enabling including module folders like node_modules" ) + # Reachability Configuration + reachability_group = parser.add_argument_group('Reachability Analysis') + reachability_group.add_argument( + "--reach", + dest="reach", + action="store_true", + help="Enable reachability analysis" + ) + reachability_group.add_argument( + "--reach-version", + dest="reach_version", + metavar="", + help="Specific version of @coana-tech/cli to use (e.g., '1.2.3')" + ) + reachability_group.add_argument( + "--reach-timeout", + dest="reach_analysis_timeout", + type=int, + metavar="", + help="Timeout for reachability analysis in seconds" + ) + reachability_group.add_argument( + "--reach-memory-limit", + dest="reach_analysis_memory_limit", + type=int, + metavar="", + help="Memory limit for reachability analysis in MB" + ) + reachability_group.add_argument( + "--reach-ecosystems", + dest="reach_ecosystems", + metavar="", + help="Ecosystems to analyze for reachability (comma-separated, e.g., 'npm,pypi')" + ) + reachability_group.add_argument( + "--reach-exclude-paths", + dest="reach_exclude_paths", + metavar="", + help="Paths to exclude from reachability analysis (comma-separated)" + ) + reachability_group.add_argument( + "--reach-min-severity", + dest="reach_min_severity", + metavar="", + help="Minimum severity level for reachability analysis (info, low, moderate, high, critical)" + ) + reachability_group.add_argument( + "--reach-skip-cache", + dest="reach_skip_cache", + action="store_true", + help="Skip cache usage for reachability analysis" + ) + reachability_group.add_argument( + "--reach-disable-analytics", + dest="reach_disable_analytics", + action="store_true", + help="Disable analytics sharing for reachability analysis" + ) + reachability_group.add_argument( + "--reach-output-file", + dest="reach_output_file", + metavar="", + default=".socket.facts.json", + help="Output file path for reachability analysis results (default: .socket.facts.json)" + ) + reachability_group.add_argument( + "--only-facts-file", + dest="only_facts_file", + action="store_true", + help="Submit only the .socket.facts.json file when creating full scan (requires --reach)" + ) + parser.add_argument( '--version', action='version', diff --git a/socketsecurity/core/helper/__init__.py b/socketsecurity/core/helper/__init__.py index f10cb6e..ab7d06f 100644 --- a/socketsecurity/core/helper/__init__.py +++ b/socketsecurity/core/helper/__init__.py @@ -1,5 +1,6 @@ import markdown -from bs4 import BeautifulSoup, NavigableString, Tag +from bs4 import BeautifulSoup, Tag +from bs4.element import NavigableString import string diff --git a/socketsecurity/core/tools/reachability.py b/socketsecurity/core/tools/reachability.py new file mode 100644 index 0000000..064b699 --- /dev/null +++ b/socketsecurity/core/tools/reachability.py @@ -0,0 +1,234 @@ +from socketdev import socketdev +from typing import List, Optional, Dict, Any +import os +import subprocess +import json +import pathlib +import logging +import sys + +log = logging.getLogger(__name__) + + +class ReachabilityAnalyzer: + def __init__(self, sdk: socketdev, api_token: str): + self.sdk = sdk + self.api_token = api_token + + def _ensure_coana_cli_installed(self, version: Optional[str] = None) -> str: + """ + Check if @coana-tech/cli is installed, and install it if not present. + + Args: + version: Specific version to install (e.g., '1.2.3') + + Returns: + str: The package specifier to use with npx + """ + # Determine the package specifier + package_spec = f"@coana-tech/cli@{version}" if version else "@coana-tech/cli" + + # Check if the package is already available + try: + check_cmd = ["npm", "list", "-g", "@coana-tech/cli", "--depth=0"] + result = subprocess.run( + check_cmd, + capture_output=True, + text=True, + timeout=10 + ) + + # If npm list succeeds and mentions the package, it's installed + if result.returncode == 0 and "@coana-tech/cli" in result.stdout: + log.debug(f"@coana-tech/cli is already installed globally") + return package_spec + + except Exception as e: + log.debug(f"Could not check for existing @coana-tech/cli installation: {e}") + + # Package not found or check failed - install it + log.info("Downloading reachability analysis plugin (@coana-tech/cli)...") + log.info("This may take a moment on first run...") + + try: + install_cmd = ["npm", "install", "-g", package_spec] + log.debug(f"Installing with command: {' '.join(install_cmd)}") + + result = subprocess.run( + install_cmd, + capture_output=True, + text=True, + timeout=300 # 5 minute timeout for installation + ) + + if result.returncode != 0: + log.warning(f"Global installation failed, npx will download on demand") + log.debug(f"Install stderr: {result.stderr}") + else: + log.info("Reachability analysis plugin installed successfully") + + except subprocess.TimeoutExpired: + log.warning("Installation timed out, npx will download on demand") + except Exception as e: + log.warning(f"Could not install globally: {e}, npx will download on demand") + + return package_spec + + + def run_reachability_analysis( + self, + org_slug: str, + target_directory: str, + tar_hash: Optional[str] = None, + output_path: str = ".socket.facts.json", + timeout: Optional[int] = None, + memory_limit: Optional[int] = None, + ecosystems: Optional[List[str]] = None, + exclude_paths: Optional[List[str]] = None, + min_severity: Optional[str] = None, + skip_cache: bool = False, + disable_analytics: bool = False, + repo_name: Optional[str] = None, + branch_name: Optional[str] = None, + version: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Run reachability analysis. + + Args: + org_slug: Socket organization slug + target_directory: Directory to analyze + tar_hash: Tar hash from manifest upload or existing scan (optional) + output_path: Output file path for results + timeout: Analysis timeout in seconds + memory_limit: Memory limit in MB + ecosystems: List of ecosystems to analyze (e.g., ['npm', 'pypi']) + exclude_paths: Paths to exclude from analysis + min_severity: Minimum severity level (info, low, moderate, high, critical) + skip_cache: Skip cache usage + disable_analytics: Disable analytics sharing + repo_name: Repository name + branch_name: Branch name + version: Specific version of @coana-tech/cli to use + + Returns: + Dict containing scan_id and report_path + """ + # Ensure @coana-tech/cli is installed + cli_package = self._ensure_coana_cli_installed(version) + + # Build CLI command arguments + cmd = ["npx", cli_package, "run", target_directory] + + # Add required arguments + output_dir = str(pathlib.Path(output_path).parent) + cmd.extend([ + "--output-dir", output_dir, + "--socket-mode", output_path, + "--disable-report-submission" + ]) + + # Add conditional arguments + if timeout: + cmd.extend(["--analysis-timeout", str(timeout)]) + + if memory_limit: + cmd.extend(["--memory-limit", str(memory_limit)]) + + if disable_analytics: + cmd.append("--disable-analytics-sharing") + + # KEY POINT: Only add manifest tar hash if we have one + if tar_hash: + cmd.extend(["--run-without-docker", "--manifests-tar-hash", tar_hash]) + + if ecosystems: + cmd.extend(["--purl-types"] + ecosystems) + + if exclude_paths: + cmd.extend(["--exclude-dirs"] + exclude_paths) + + if min_severity: + cmd.extend(["--min-severity", min_severity]) + + if skip_cache: + cmd.append("--skip-cache-usage") + + # Set up environment variables + env = os.environ.copy() + + # Required environment variables for Coana CLI + env["SOCKET_ORG_SLUG"] = org_slug + env["SOCKET_CLI_API_TOKEN"] = self.api_token + + # Optional environment variables + if repo_name: + env["SOCKET_REPO_NAME"] = repo_name + + if branch_name: + env["SOCKET_BRANCH_NAME"] = branch_name + + # Execute CLI + log.info("Running reachability analysis...") + log.debug(f"Reachability command: {' '.join(cmd)}") + log.debug(f"Environment: SOCKET_ORG_SLUG={org_slug}, SOCKET_REPO_NAME={repo_name or 'not set'}, SOCKET_BRANCH_NAME={branch_name or 'not set'}") + + try: + # Run with output streaming to stderr (don't capture output) + result = subprocess.run( + cmd, + env=env, + cwd=os.getcwd(), + stdout=sys.stderr, # Send stdout to stderr so user sees it + stderr=sys.stderr, # Send stderr to stderr + timeout=timeout + 60 if timeout else None # Add buffer to subprocess timeout + ) + + if result.returncode != 0: + log.error(f"Reachability analysis failed with exit code {result.returncode}") + raise Exception(f"Reachability analysis failed with exit code {result.returncode}") + + # Extract scan ID from output file + scan_id = self._extract_scan_id(output_path) + + log.info(f"Reachability analysis completed successfully") + if scan_id: + log.info(f"Scan ID: {scan_id}") + + return { + "scan_id": scan_id, + "report_path": output_path, + "tar_hash_used": tar_hash + } + + except subprocess.TimeoutExpired: + log.error(f"Reachability analysis timed out after {timeout} seconds") + raise Exception(f"Reachability analysis timed out after {timeout} seconds") + except Exception as e: + log.error(f"Failed to run reachability analysis: {str(e)}") + raise Exception(f"Failed to run reachability analysis: {str(e)}") + + def _extract_scan_id(self, facts_file_path: str) -> Optional[str]: + """ + Extract tier1ReachabilityScanId from the socket facts JSON file. + + Args: + facts_file_path: Path to the .socket.facts.json file + + Returns: + Optional[str]: The scan ID if found, None otherwise + """ + try: + if not os.path.exists(facts_file_path): + log.warning(f"Facts file not found: {facts_file_path}") + return None + + with open(facts_file_path, 'r') as f: + facts = json.load(f) + + scan_id = facts.get('tier1ReachabilityScanId') + return scan_id.strip() if scan_id else None + + except (json.JSONDecodeError, IOError) as e: + log.warning(f"Failed to extract scan ID from {facts_file_path}: {e}") + return None diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 79b8374..ed3ebc0 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -1,6 +1,7 @@ import json import sys import traceback +import shutil from dotenv import load_dotenv from git import InvalidGitRepositoryError, NoSuchPathError @@ -75,6 +76,50 @@ def main_code(): log.debug("loaded client") core = Core(socket_config, sdk) log.debug("loaded core") + + # Check for required dependencies if reachability analysis is enabled + if config.reach: + log.info("Reachability analysis enabled, checking for required dependencies...") + required_deps = ["npm", "uv", "npx"] + missing_deps = [] + found_deps = [] + + for dep in required_deps: + if shutil.which(dep): + found_deps.append(dep) + log.debug(f"Found required dependency: {dep}") + else: + missing_deps.append(dep) + + if missing_deps: + log.error(f"Reachability analysis requires the following dependencies: {', '.join(required_deps)}") + log.error(f"Missing dependencies: {', '.join(missing_deps)}") + log.error("Please install the missing dependencies and try again.") + sys.exit(3) + + log.info(f"All required dependencies found: {', '.join(found_deps)}") + + # Check if organization has an enterprise plan + log.info("Checking organization plan for reachability analysis eligibility...") + org_response = sdk.org.get(use_types=True) + organizations = org_response.get("organizations", {}) + + if organizations: + org_id = next(iter(organizations)) + org_plan = organizations[org_id].get('plan', '') + + # Check if plan matches enterprise* pattern (enterprise, enterprise_trial, etc.) + if not org_plan.startswith('enterprise'): + log.error(f"Reachability analysis is only available for enterprise plans.") + log.error(f"Your organization plan is: {org_plan}") + log.error("Please upgrade to an enterprise plan to use reachability analysis.") + sys.exit(3) + + log.info(f"Organization plan verified: {org_plan}") + else: + log.error("Unable to retrieve organization information for plan verification.") + sys.exit(3) + # Parse files argument try: if isinstance(config.files, list): @@ -112,6 +157,9 @@ def main_code(): # Determine if files were explicitly specified files_explicitly_specified = config.files != "[]" and len(specified_files) > 0 + # Variable to track if we need to override files with facts file + facts_file_to_submit = None + # Git setup is_repo = False git_repo: Git @@ -172,6 +220,85 @@ def main_code(): # If no repo name was set but workspace_name is provided, we'll use it later log.debug(f"Workspace name provided: {config.workspace_name}") + # Run reachability analysis if enabled + if config.reach: + from socketsecurity.core.tools.reachability import ReachabilityAnalyzer + + log.info("Starting reachability analysis...") + + # Find manifest files in scan paths (excluding .socket.facts.json to avoid circular dependency) + log.info("Finding manifest files for reachability analysis...") + manifest_files = [] + for scan_path in scan_paths: + scan_manifests = core.find_files(scan_path) + # Filter out .socket.facts.json files from manifest upload + scan_manifests = [f for f in scan_manifests if not f.endswith('.socket.facts.json')] + manifest_files.extend(scan_manifests) + + if not manifest_files: + log.warning("No manifest files found for reachability analysis") + else: + log.info(f"Found {len(manifest_files)} manifest files for reachability upload") + + # Upload manifests and get tar hash + log.info("Uploading manifest files...") + try: + # Get org_slug early (we'll need it) + org_slug = core.config.org_slug + + # Upload manifest files + tar_hash = sdk.uploadmanifests.upload_manifest_files( + org_slug=org_slug, + file_paths=manifest_files, + workspace=config.repo or "default-workspace", + base_path=None, + base_paths=base_paths, + use_lazy_loading=False + ) + log.info(f"Manifest upload successful, tar hash: {tar_hash}") + + # Initialize and run reachability analyzer + analyzer = ReachabilityAnalyzer(sdk, config.api_token) + + # Determine output path + output_path = config.reach_output_file or ".socket.facts.json" + + # Run the analysis + result = analyzer.run_reachability_analysis( + org_slug=org_slug, + target_directory=config.target_path, + tar_hash=tar_hash, + output_path=output_path, + timeout=config.reach_analysis_timeout, + memory_limit=config.reach_analysis_memory_limit, + ecosystems=config.reach_ecosystems, + exclude_paths=config.reach_exclude_paths, + min_severity=config.reach_min_severity, + skip_cache=config.reach_skip_cache or False, + disable_analytics=config.reach_disable_analytics or False, + repo_name=config.repo, + branch_name=config.branch, + version=config.reach_version + ) + + log.info(f"Reachability analysis completed successfully") + log.info(f"Results written to: {result['report_path']}") + if result.get('scan_id'): + log.info(f"Reachability scan ID: {result['scan_id']}") + + # If only-facts-file mode, mark the facts file for submission + if config.only_facts_file: + import os + facts_file_to_submit = os.path.abspath(output_path) + log.info(f"Only-facts-file mode: will submit only {facts_file_to_submit}") + + except Exception as e: + log.error(f"Reachability analysis failed: {str(e)}") + if not config.disable_blocking: + sys.exit(3) + + log.info("Continuing with normal scan flow...") + scm = None if config.scm == "github": from socketsecurity.core.scm.github import Github, GithubConfig @@ -188,6 +315,12 @@ def main_code(): if scm is not None and not config.default_branch: config.default_branch = scm.config.is_default_branch + # Override files if only-facts-file mode is active + if facts_file_to_submit: + specified_files = [facts_file_to_submit] + files_explicitly_specified = True + log.debug(f"Overriding files to only submit facts file: {facts_file_to_submit}") + # Determine files to check based on the new logic files_to_check = [] force_api_mode = False @@ -282,7 +415,8 @@ def main_code(): pull_request=pr_number, committers=config.committers, make_default_branch=is_default_branch, - set_as_pending_head=is_default_branch + set_as_pending_head=is_default_branch, + tmp=False ) params.include_license_details = not config.exclude_license_details diff --git a/uv.lock b/uv.lock index 3375542..ad16f82 100644 --- a/uv.lock +++ b/uv.lock @@ -35,6 +35,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } +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 = "bs4" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -1027,22 +1052,23 @@ wheels = [ [[package]] name = "socketdev" -version = "3.0.5" +version = "3.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/b7/fe90d55105df76e9ff3af025f64b2d2b515c30ac0866a9973a093f25c5ed/socketdev-3.0.5.tar.gz", hash = "sha256:58cbe8613c3c892cdbae4941cb53f065051f8e991500d9d61618b214acf4ffc2", size = 129576, upload-time = "2025-09-09T07:15:48.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/e8/362072e5a8b94aa550d91ec0d7ef9ee63120284ceaedc9c8e1889a32abcf/socketdev-3.0.14.tar.gz", hash = "sha256:bcd1c548ac93f91ecc504f8a42be0ad59e457baa9ab17d02fcd2ccd9f10ace5e", size = 131919, upload-time = "2025-10-17T01:53:04.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/05/c3fc7d0418c2598302ad4b0baf111fa492b31a8fa14acfa394af6f55b373/socketdev-3.0.5-py3-none-any.whl", hash = "sha256:e050f50d2c6b4447107edd3368b56b053e1df62056d424cc1616e898303638ef", size = 55083, upload-time = "2025-09-09T07:15:46.52Z" }, + { url = "https://files.pythonhosted.org/packages/80/ac/aa54c296ecfff89d32974396517eb67bec17737cb863ef1f41bfe1ef83f1/socketdev-3.0.14-py3-none-any.whl", hash = "sha256:189d3e717f774b402eee55d933ddc13e41b52fc9e6410ab4362d5198ff57c723", size = 57338, upload-time = "2025-10-17T01:53:02.356Z" }, ] [[package]] name = "socketsecurity" -version = "2.2.7" +version = "2.2.15" source = { editable = "." } dependencies = [ + { name = "bs4" }, { name = "gitpython" }, { name = "mdutils" }, { name = "packaging" }, @@ -1070,6 +1096,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "bs4", specifier = ">=0.0.2" }, { name = "gitpython" }, { name = "hatch", marker = "extra == 'dev'" }, { name = "mdutils" }, @@ -1084,12 +1111,21 @@ requires-dist = [ { name = "python-dotenv" }, { name = "requests" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, - { name = "socketdev", specifier = ">=3.0.5,<4.0.0" }, + { name = "socketdev", specifier = ">=3.0.6,<4.0.0" }, { name = "twine", marker = "extra == 'dev'" }, { name = "uv", marker = "extra == 'dev'", specifier = ">=0.1.0" }, ] provides-extras = ["test", "dev"] +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + [[package]] name = "tomli" version = "2.2.1"