diff options
| author | Louis Burda <dev@sinitax.com> | 2026-01-27 03:31:54 +0100 |
|---|---|---|
| committer | Louis Burda <dev@sinitax.com> | 2026-01-27 03:31:54 +0100 |
| commit | c6625238403d8ea2957bc91ea3e0f04090cbd5aa (patch) | |
| tree | ae85a763ac1b2bdaa3897f4ecf3252489839dd72 | |
| download | vulners-py-main.tar.gz vulners-py-main.zip | |
| -rw-r--r-- | .gitignore | 10 | ||||
| -rw-r--r-- | .python-version | 1 | ||||
| -rw-r--r-- | README.md | 0 | ||||
| -rw-r--r-- | pyproject.toml | 18 | ||||
| -rw-r--r-- | src/vulners/__init__.py | 3 | ||||
| -rw-r--r-- | src/vulners/api.py | 74 | ||||
| -rw-r--r-- | src/vulners/cli.py | 282 | ||||
| -rw-r--r-- | uv.lock | 161 |
8 files changed, 549 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/README.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dba56a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "vulners" +version = "0.1.0" +description = "CLI tool for querying CVEs and CPEs from vulners.com" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "click>=8.1", + "httpx>=0.27", + "rich>=13.7", +] + +[project.scripts] +vulners = "vulners.cli:main" + +[build-system] +requires = ["uv_build>=0.9.25,<0.10.0"] +build-backend = "uv_build" diff --git a/src/vulners/__init__.py b/src/vulners/__init__.py new file mode 100644 index 0000000..eee1892 --- /dev/null +++ b/src/vulners/__init__.py @@ -0,0 +1,3 @@ +"""Vulners CLI - Query CVEs and CPEs from vulners.com""" + +__version__ = "0.1.0" diff --git a/src/vulners/api.py b/src/vulners/api.py new file mode 100644 index 0000000..2c0fdbd --- /dev/null +++ b/src/vulners/api.py @@ -0,0 +1,74 @@ +import os +from dataclasses import dataclass +from typing import Any + +import httpx + +BASE_URL = "https://vulners.com/api/v3" + + +@dataclass +class VulnersClient: + api_key: str | None = None + timeout: float = 30.0 + + def __post_init__(self): + if not self.api_key: + self.api_key = os.environ.get("VULNERS_API_KEY") + + def _request(self, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]: + headers = {"Content-Type": "application/json"} + if self.api_key: + headers["X-Api-Key"] = self.api_key + + with httpx.Client(timeout=self.timeout) as client: + resp = client.post(f"{BASE_URL}{endpoint}", headers=headers, json=payload) + resp.raise_for_status() + return resp.json() + + def search( + self, + query: str, + size: int = 10, + skip: int = 0, + fields: list[str] | None = None, + ) -> dict[str, Any]: + payload = {"query": query, "size": size, "skip": skip} + if fields: + payload["fields"] = fields + return self._request("/search/lucene/", payload) + + def get_cve(self, cve_id: str) -> dict[str, Any]: + return self.search(f"id:{cve_id}", size=1) + + def search_cpe( + self, + vendor: str | None = None, + product: str | None = None, + version: str | None = None, + size: int = 10, + ) -> dict[str, Any]: + parts = [] + if vendor: + parts.append(f"cpe.vendor:{vendor}") + if product: + parts.append(f"cpe.product:{product}") + if version: + parts.append(f"cpe.version:{version}") + query = " AND ".join(parts) if parts else "*" + return self.search(query, size=size) + + def software_vulns( + self, + software: str, + version: str | None = None, + max_vulns: int = 20, + ) -> dict[str, Any]: + payload = { + "software": software, + "type": "software", + "maxVulnerabilities": max_vulns, + } + if version: + payload["version"] = version + return self._request("/burp/softwareapi/", payload) diff --git a/src/vulners/cli.py b/src/vulners/cli.py new file mode 100644 index 0000000..a5d954a --- /dev/null +++ b/src/vulners/cli.py @@ -0,0 +1,282 @@ +import json +import sys + +import click +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich import print as rprint + +from .api import VulnersClient + +console = Console() + + +def get_client(api_key: str | None) -> VulnersClient: + client = VulnersClient(api_key=api_key) + if not client.api_key: + console.print("[yellow]Warning: No API key set. Set VULNERS_API_KEY or use --api-key[/yellow]") + return client + + +def print_error(msg: str): + console.print(f"[red]Error:[/red] {msg}") + sys.exit(1) + + +@click.group(context_settings={"help_option_names": ["-h", "--help"]}) +@click.option("--api-key", envvar="VULNERS_API_KEY", help="Vulners API key") +@click.pass_context +def main(ctx, api_key): + """Query CVEs and CPEs from vulners.com""" + ctx.ensure_object(dict) + ctx.obj["api_key"] = api_key + + +def build_query( + base: str, + type_: str | None, + cvss_min: float | None, + cvss_max: float | None, + after: str | None, + before: str | None, + vendor: str | None, + product: str | None, + version: str | None, + order: str | None, +) -> str: + parts = [base] if base else [] + if type_: + parts.append(f"type:{type_}") + if cvss_min is not None and cvss_max is not None: + parts.append(f"cvss.score:[{cvss_min} TO {cvss_max}]") + elif cvss_min is not None: + parts.append(f"cvss.score:[{cvss_min} TO 10]") + elif cvss_max is not None: + parts.append(f"cvss.score:[0 TO {cvss_max}]") + if after: + parts.append(f"published:[{after} TO *]") + if before: + parts.append(f"published:[* TO {before}]") + if vendor: + parts.append(f"affectedSoftware.vendor:{vendor}") + if product: + parts.append(f"affectedSoftware.name:{product}") + if version: + parts.append(f"affectedSoftware.version:{version}") + query = " AND ".join(parts) if parts else "*" + if order: + query += f" order:{order}" + return query + + +@main.command() +@click.argument("query", default="") +@click.option("-n", "--limit", default=10, help="Number of results") +@click.option("--skip", default=0, help="Skip first N results") +@click.option("-t", "--type", "type_", help="Filter by type (cve, exploit, nessus, etc.)") +@click.option("--cvss-min", type=float, help="Minimum CVSS score") +@click.option("--cvss-max", type=float, help="Maximum CVSS score") +@click.option("--after", help="Published after date (YYYY-MM-DD)") +@click.option("--before", help="Published before date (YYYY-MM-DD)") +@click.option("--vendor", help="Filter by software vendor") +@click.option("--product", help="Filter by product name") +@click.option("--version", help="Filter by version") +@click.option("--order", type=click.Choice(["published", "cvss"]), help="Order results") +@click.option("--full", "-f", is_flag=True, help="Show full descriptions") +@click.option("--json", "as_json", is_flag=True, help="Output as JSON") +@click.pass_context +def search(ctx, query, limit, skip, type_, cvss_min, cvss_max, after, before, vendor, product, version, order, full, as_json): + """Search vulnerabilities using Lucene query syntax""" + client = get_client(ctx.obj["api_key"]) + final_query = build_query(query, type_, cvss_min, cvss_max, after, before, vendor, product, version, order) + try: + result = client.search(final_query, size=limit, skip=skip) + except Exception as e: + print_error(str(e)) + + if as_json: + click.echo(json.dumps(result, indent=2)) + return + + data = result.get("data", {}) + if data.get("total", 0) == 0: + console.print("[yellow]No results found[/yellow]") + return + + console.print(f"[green]Found {data['total']} results[/green]\n") + + for doc in data.get("search", []): + src = doc.get("_source", {}) + title = src.get("title", src.get("id", "Unknown")) + doc_type = src.get("type", "unknown") + cvss = src.get("cvss", {}).get("score", "-") + published = src.get("published", "-")[:10] if src.get("published") else "-" + raw_desc = src.get("description", "") + truncated = not full and len(raw_desc) > 200 + desc = raw_desc if full else raw_desc[:200] + + panel = Panel( + f"{desc}..." if truncated else (desc or "No description"), + title=f"[bold]{title}[/bold]", + subtitle=f"Type: {doc_type} | CVSS: {cvss} | Published: {published}", + border_style="dim", + ) + console.print(panel) + console.print() + + +@main.command() +@click.argument("cve_id") +@click.option("--json", "as_json", is_flag=True, help="Output as JSON") +@click.pass_context +def cve(ctx, cve_id, as_json): + """Get details for a specific CVE (e.g., CVE-2021-44228)""" + client = get_client(ctx.obj["api_key"]) + cve_id = cve_id.upper() + + try: + result = client.get_cve(cve_id) + except Exception as e: + print_error(str(e)) + + if as_json: + click.echo(json.dumps(result, indent=2)) + return + + docs = result.get("data", {}).get("search", []) + if not docs: + console.print(f"[yellow]CVE {cve_id} not found[/yellow]") + return + + src = docs[0].get("_source", {}) + title = src.get("title", cve_id) + cvss = src.get("cvss", {}) + desc = src.get("description", "No description available") + published = src.get("published", "Unknown")[:10] if src.get("published") else "Unknown" + refs = src.get("references", []) + cpes = src.get("cpe", []) + + console.print(Panel(f"[bold cyan]{title}[/bold cyan]", expand=False, border_style="dim")) + console.print(f"\n[bold]CVSS Score:[/bold] {cvss.get('score', 'N/A')} ({cvss.get('vector', 'N/A')})") + console.print(f"[bold]Published:[/bold] {published}") + console.print(f"\n[bold]Description:[/bold]\n{desc}") + + if cpes: + console.print(f"\n[bold]Affected CPEs:[/bold]") + for cpe in cpes[:10]: + console.print(f" • {cpe}") + if len(cpes) > 10: + console.print(f" [dim]... and {len(cpes) - 10} more[/dim]") + + if refs: + console.print(f"\n[bold]References:[/bold]") + for ref in refs[:5]: + console.print(f" • {ref}") + if len(refs) > 5: + console.print(f" [dim]... and {len(refs) - 5} more[/dim]") + + +@main.command() +@click.option("--vendor", "-v", help="CPE vendor name") +@click.option("--product", "-p", help="CPE product name") +@click.option("--version", help="CPE version") +@click.option("-n", "--limit", default=10, help="Number of results") +@click.option("--json", "as_json", is_flag=True, help="Output as JSON") +@click.pass_context +def cpe(ctx, vendor, product, version, limit, as_json): + """Search vulnerabilities by CPE attributes""" + if not any([vendor, product, version]): + print_error("At least one of --vendor, --product, or --version required") + + client = get_client(ctx.obj["api_key"]) + try: + result = client.search_cpe(vendor=vendor, product=product, version=version, size=limit) + except Exception as e: + print_error(str(e)) + + if as_json: + click.echo(json.dumps(result, indent=2)) + return + + data = result.get("data", {}) + total = data.get("total", 0) + if total == 0: + console.print("[yellow]No results found[/yellow]") + return + + console.print(f"[green]Found {total} vulnerabilities[/green]\n") + + table = Table(show_header=True, border_style="dim") + table.add_column("ID", style="cyan") + table.add_column("Title", max_width=50) + table.add_column("CVSS", justify="right") + table.add_column("Published") + + for doc in data.get("search", []): + src = doc.get("_source", {}) + table.add_row( + src.get("id", "-"), + (src.get("title", "-")[:47] + "...") if len(src.get("title", "")) > 50 else src.get("title", "-"), + str(src.get("cvss", {}).get("score", "-")), + src.get("published", "-")[:10] if src.get("published") else "-", + ) + + console.print(table) + + +@main.command() +@click.argument("software") +@click.argument("version", required=False) +@click.option("-n", "--limit", default=20, help="Max vulnerabilities") +@click.option("--json", "as_json", is_flag=True, help="Output as JSON") +@click.pass_context +def software(ctx, software, version, limit, as_json): + """Find vulnerabilities for a software (e.g., apache 2.4.49)""" + client = get_client(ctx.obj["api_key"]) + try: + result = client.software_vulns(software, version, max_vulns=limit) + except Exception as e: + print_error(str(e)) + + if as_json: + click.echo(json.dumps(result, indent=2)) + return + + data = result.get("data", {}) + vulns = data.get("search", []) or data.get("vulnerabilities", []) + + name = f"{software} {version}" if version else software + if not vulns: + console.print(f"[yellow]No vulnerabilities found for {name}[/yellow]") + return + + console.print(f"[green]Found {len(vulns)} vulnerabilities for {name}[/green]\n") + + table = Table(show_header=True, border_style="dim") + table.add_column("CVE", style="cyan") + table.add_column("Title", max_width=50) + table.add_column("CVSS", justify="right") + + for vuln in vulns: + if isinstance(vuln, dict): + src = vuln.get("_source", vuln) + cve_id = src.get("id", "-") + title = src.get("title", "-") + cvss = src.get("cvss", {}).get("score", "-") if isinstance(src.get("cvss"), dict) else "-" + else: + cve_id = str(vuln) + title = "-" + cvss = "-" + + table.add_row( + cve_id, + (title[:47] + "...") if len(title) > 50 else title, + str(cvss), + ) + + console.print(table) + + +if __name__ == "__main__": + main() @@ -0,0 +1,161 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +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 = "rich" +version = "14.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "vulners" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "httpx" }, + { name = "rich" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "rich", specifier = ">=13.7" }, +] |
