aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLouis Burda <dev@sinitax.com>2026-01-27 03:31:54 +0100
committerLouis Burda <dev@sinitax.com>2026-01-27 03:31:54 +0100
commitc6625238403d8ea2957bc91ea3e0f04090cbd5aa (patch)
treeae85a763ac1b2bdaa3897f4ecf3252489839dd72
downloadvulners-py-main.tar.gz
vulners-py-main.zip
Add initial versionHEADmain
-rw-r--r--.gitignore10
-rw-r--r--.python-version1
-rw-r--r--README.md0
-rw-r--r--pyproject.toml18
-rw-r--r--src/vulners/__init__.py3
-rw-r--r--src/vulners/api.py74
-rw-r--r--src/vulners/cli.py282
-rw-r--r--uv.lock161
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()
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..eb3573d
--- /dev/null
+++ b/uv.lock
@@ -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" },
+]