patchradar 2026.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchradar-2026.8.1/.env.example +0 -0
- patchradar-2026.8.1/.github/workflows/publish.yml +33 -0
- patchradar-2026.8.1/.github/workflows/test.yml +30 -0
- patchradar-2026.8.1/LICENSE +21 -0
- patchradar-2026.8.1/PKG-INFO +135 -0
- patchradar-2026.8.1/README.md +102 -0
- patchradar-2026.8.1/pyproject.toml +49 -0
- patchradar-2026.8.1/src/patchradar/__init__.py +0 -0
- patchradar-2026.8.1/src/patchradar/api/__init__.py +0 -0
- patchradar-2026.8.1/src/patchradar/api/main.py +78 -0
- patchradar-2026.8.1/src/patchradar/api/templates/index.html +334 -0
- patchradar-2026.8.1/src/patchradar/cli.py +173 -0
- patchradar-2026.8.1/src/patchradar/collectors/__init__.py +0 -0
- patchradar-2026.8.1/src/patchradar/collectors/debian.py +0 -0
- patchradar-2026.8.1/src/patchradar/collectors/msrc.py +0 -0
- patchradar-2026.8.1/src/patchradar/collectors/nvd.py +66 -0
- patchradar-2026.8.1/src/patchradar/collectors/snyk.py +0 -0
- patchradar-2026.8.1/src/patchradar/db/__init__.py +0 -0
- patchradar-2026.8.1/src/patchradar/db/database.py +92 -0
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-and-publish:
|
|
9
|
+
name: Build and publish to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/patchradar
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.13"
|
|
25
|
+
|
|
26
|
+
- name: Install build dependencies
|
|
27
|
+
run: pip install build hatchling
|
|
28
|
+
|
|
29
|
+
- name: Build package
|
|
30
|
+
run: python -m build
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install -e .
|
|
26
|
+
|
|
27
|
+
- name: Test CLI
|
|
28
|
+
run: |
|
|
29
|
+
patchradar --help
|
|
30
|
+
patchradar list
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 maksimtech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: patchradar
|
|
3
|
+
Version: 2026.8.1
|
|
4
|
+
Summary: Realtime CVE intelligence for your software stack
|
|
5
|
+
Project-URL: Homepage, https://github.com/maksimtech/patchradar
|
|
6
|
+
Project-URL: Repository, https://github.com/maksimtech/patchradar
|
|
7
|
+
Project-URL: Issues, https://github.com/maksimtech/patchradar/issues
|
|
8
|
+
Author-email: maksimtech <github@maksimtech.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cve,monitoring,patch-tuesday,security,vulnerability
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
25
|
+
Requires-Dist: apscheduler>=3.10.0
|
|
26
|
+
Requires-Dist: fastapi>=0.115.0
|
|
27
|
+
Requires-Dist: httpx>=0.28.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0.0
|
|
29
|
+
Requires-Dist: rich>=13.0.0
|
|
30
|
+
Requires-Dist: typer>=0.12.0
|
|
31
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# PatchRadar 🛡️
|
|
35
|
+
|
|
36
|
+
> Know when your software is vulnerable — before attackers do.
|
|
37
|
+
|
|
38
|
+
PatchRadar monitors CVE feeds in realtime and alerts you when a new vulnerability affects your software stack. No more manually checking NVD, MSRC, or Snyk — just add your software and let PatchRadar watch for you.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ✨ Features
|
|
48
|
+
|
|
49
|
+
- 🔍 **Realtime CVE monitoring** — scans NVD and other sources for new vulnerabilities
|
|
50
|
+
- 📋 **Personal watchlist** — add any software you want to monitor
|
|
51
|
+
- 🎨 **Beautiful web UI** — dark theme dashboard with charts and filters
|
|
52
|
+
- 💻 **CLI first** — full command line interface for automation
|
|
53
|
+
- 📊 **CVSS scoring** — color-coded severity (Critical / High / Medium / Low)
|
|
54
|
+
- 💾 **Local SQLite** — all data stored locally, no cloud, no account needed
|
|
55
|
+
- 🐍 **Python 3.11+** — modern async architecture with httpx and FastAPI
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 🚀 Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install patchradar
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 📖 Usage
|
|
68
|
+
|
|
69
|
+
### CLI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Add software to your watchlist
|
|
73
|
+
patchradar add proxmox
|
|
74
|
+
patchradar add bitwarden
|
|
75
|
+
patchradar add "windows 10"
|
|
76
|
+
|
|
77
|
+
# Show your watchlist
|
|
78
|
+
patchradar list
|
|
79
|
+
|
|
80
|
+
# Scan for CVEs (last 30 days)
|
|
81
|
+
patchradar scan --days 30
|
|
82
|
+
|
|
83
|
+
# Show latest CVEs in terminal
|
|
84
|
+
patchradar status
|
|
85
|
+
|
|
86
|
+
# Remove software
|
|
87
|
+
patchradar remove proxmox
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Web UI
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
patchradar serve
|
|
94
|
+
# Open http://localhost:8000
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 📡 Sources
|
|
100
|
+
|
|
101
|
+
| Source | Type | Status |
|
|
102
|
+
|--------|------|--------|
|
|
103
|
+
| [NVD](https://nvd.nist.gov) | CVE Database | ✅ Active |
|
|
104
|
+
| MSRC | Microsoft Security | 🔜 Coming soon |
|
|
105
|
+
| Snyk | Package vulnerabilities | 🔜 Coming soon |
|
|
106
|
+
| Debian Security | Linux packages | 🔜 Coming soon |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 🗓️ Versioning
|
|
111
|
+
|
|
112
|
+
PatchRadar uses [CalVer](https://calver.org) — `YYYY.MM.PATCH`.
|
|
113
|
+
|
|
114
|
+
| Version | Date | Notes |
|
|
115
|
+
|---------|------|-------|
|
|
116
|
+
| 2026.8.2 | 2026-08-14 | Web UI added |
|
|
117
|
+
| 2026.8.1 | 2026-08-14 | Initial release |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🤝 Contributing
|
|
122
|
+
|
|
123
|
+
Contributions are welcome! Feel free to open issues or pull requests.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 📄 License
|
|
128
|
+
|
|
129
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
<div align="center">
|
|
134
|
+
Built with ❤️ by <a href="https://github.com/maksimtech">maksimtech</a>
|
|
135
|
+
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# PatchRadar 🛡️
|
|
2
|
+
|
|
3
|
+
> Know when your software is vulnerable — before attackers do.
|
|
4
|
+
|
|
5
|
+
PatchRadar monitors CVE feeds in realtime and alerts you when a new vulnerability affects your software stack. No more manually checking NVD, MSRC, or Snyk — just add your software and let PatchRadar watch for you.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- 🔍 **Realtime CVE monitoring** — scans NVD and other sources for new vulnerabilities
|
|
17
|
+
- 📋 **Personal watchlist** — add any software you want to monitor
|
|
18
|
+
- 🎨 **Beautiful web UI** — dark theme dashboard with charts and filters
|
|
19
|
+
- 💻 **CLI first** — full command line interface for automation
|
|
20
|
+
- 📊 **CVSS scoring** — color-coded severity (Critical / High / Medium / Low)
|
|
21
|
+
- 💾 **Local SQLite** — all data stored locally, no cloud, no account needed
|
|
22
|
+
- 🐍 **Python 3.11+** — modern async architecture with httpx and FastAPI
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🚀 Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install patchradar
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📖 Usage
|
|
35
|
+
|
|
36
|
+
### CLI
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Add software to your watchlist
|
|
40
|
+
patchradar add proxmox
|
|
41
|
+
patchradar add bitwarden
|
|
42
|
+
patchradar add "windows 10"
|
|
43
|
+
|
|
44
|
+
# Show your watchlist
|
|
45
|
+
patchradar list
|
|
46
|
+
|
|
47
|
+
# Scan for CVEs (last 30 days)
|
|
48
|
+
patchradar scan --days 30
|
|
49
|
+
|
|
50
|
+
# Show latest CVEs in terminal
|
|
51
|
+
patchradar status
|
|
52
|
+
|
|
53
|
+
# Remove software
|
|
54
|
+
patchradar remove proxmox
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Web UI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
patchradar serve
|
|
61
|
+
# Open http://localhost:8000
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 📡 Sources
|
|
67
|
+
|
|
68
|
+
| Source | Type | Status |
|
|
69
|
+
|--------|------|--------|
|
|
70
|
+
| [NVD](https://nvd.nist.gov) | CVE Database | ✅ Active |
|
|
71
|
+
| MSRC | Microsoft Security | 🔜 Coming soon |
|
|
72
|
+
| Snyk | Package vulnerabilities | 🔜 Coming soon |
|
|
73
|
+
| Debian Security | Linux packages | 🔜 Coming soon |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🗓️ Versioning
|
|
78
|
+
|
|
79
|
+
PatchRadar uses [CalVer](https://calver.org) — `YYYY.MM.PATCH`.
|
|
80
|
+
|
|
81
|
+
| Version | Date | Notes |
|
|
82
|
+
|---------|------|-------|
|
|
83
|
+
| 2026.8.2 | 2026-08-14 | Web UI added |
|
|
84
|
+
| 2026.8.1 | 2026-08-14 | Initial release |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🤝 Contributing
|
|
89
|
+
|
|
90
|
+
Contributions are welcome! Feel free to open issues or pull requests.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 📄 License
|
|
95
|
+
|
|
96
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
<div align="center">
|
|
101
|
+
Built with ❤️ by <a href="https://github.com/maksimtech">maksimtech</a>
|
|
102
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "patchradar"
|
|
7
|
+
version = "2026.8.1"
|
|
8
|
+
description = "Realtime CVE intelligence for your software stack"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "maksimtech", email = "github@maksimtech.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["cve", "security", "vulnerability", "monitoring", "patch-tuesday"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: System Administrators",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Security",
|
|
27
|
+
"Topic :: System :: Monitoring",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.28.0",
|
|
31
|
+
"fastapi>=0.115.0",
|
|
32
|
+
"uvicorn>=0.30.0",
|
|
33
|
+
"typer>=0.12.0",
|
|
34
|
+
"rich>=13.0.0",
|
|
35
|
+
"pydantic>=2.0.0",
|
|
36
|
+
"apscheduler>=3.10.0",
|
|
37
|
+
"aiosqlite>=0.20.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
patchradar = "patchradar.cli:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/maksimtech/patchradar"
|
|
45
|
+
Repository = "https://github.com/maksimtech/patchradar"
|
|
46
|
+
Issues = "https://github.com/maksimtech/patchradar/issues"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/patchradar"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.staticfiles import StaticFiles
|
|
3
|
+
from fastapi.responses import HTMLResponse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from patchradar.db.database import (
|
|
6
|
+
init_db, get_watchlist, add_to_watchlist,
|
|
7
|
+
remove_from_watchlist, get_cves
|
|
8
|
+
)
|
|
9
|
+
from patchradar.collectors.nvd import fetch_cves as nvd_fetch
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
app = FastAPI(title="PatchRadar", version="2026.8.1")
|
|
13
|
+
|
|
14
|
+
@app.on_event("startup")
|
|
15
|
+
async def startup():
|
|
16
|
+
await init_db()
|
|
17
|
+
|
|
18
|
+
@app.get("/api/watchlist")
|
|
19
|
+
async def api_watchlist():
|
|
20
|
+
items = await get_watchlist()
|
|
21
|
+
return {"watchlist": items}
|
|
22
|
+
|
|
23
|
+
@app.post("/api/watchlist/{software}")
|
|
24
|
+
async def api_add(software: str):
|
|
25
|
+
added = await add_to_watchlist(software)
|
|
26
|
+
return {"added": added, "software": software}
|
|
27
|
+
|
|
28
|
+
@app.delete("/api/watchlist/{software}")
|
|
29
|
+
async def api_remove(software: str):
|
|
30
|
+
removed = await remove_from_watchlist(software)
|
|
31
|
+
return {"removed": removed, "software": software}
|
|
32
|
+
|
|
33
|
+
@app.get("/api/cves")
|
|
34
|
+
async def api_cves(software: str = None, limit: int = 50):
|
|
35
|
+
cves = await get_cves(software=software, limit=limit)
|
|
36
|
+
return {"cves": cves, "total": len(cves)}
|
|
37
|
+
|
|
38
|
+
@app.post("/api/scan")
|
|
39
|
+
async def api_scan(days: int = 7):
|
|
40
|
+
from patchradar.db.database import save_cve
|
|
41
|
+
watchlist = await get_watchlist()
|
|
42
|
+
total = 0
|
|
43
|
+
results = {}
|
|
44
|
+
for sw in watchlist:
|
|
45
|
+
cves = await nvd_fetch(sw, days_back=days)
|
|
46
|
+
for cve in cves:
|
|
47
|
+
await save_cve(cve)
|
|
48
|
+
results[sw] = len(cves)
|
|
49
|
+
total += len(cves)
|
|
50
|
+
return {"total": total, "by_software": results}
|
|
51
|
+
|
|
52
|
+
@app.get("/api/stats")
|
|
53
|
+
async def api_stats():
|
|
54
|
+
from patchradar.db.database import DB_PATH
|
|
55
|
+
import aiosqlite
|
|
56
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
57
|
+
async with db.execute("SELECT COUNT(*) FROM cves") as cur:
|
|
58
|
+
total = (await cur.fetchone())[0]
|
|
59
|
+
async with db.execute(
|
|
60
|
+
"SELECT severity, COUNT(*) FROM cves GROUP BY severity"
|
|
61
|
+
) as cur:
|
|
62
|
+
by_severity = dict(await cur.fetchall())
|
|
63
|
+
async with db.execute(
|
|
64
|
+
"SELECT software, COUNT(*) FROM cves GROUP BY software ORDER BY COUNT(*) DESC"
|
|
65
|
+
) as cur:
|
|
66
|
+
by_software = dict(await cur.fetchall())
|
|
67
|
+
watchlist = await get_watchlist()
|
|
68
|
+
return {
|
|
69
|
+
"total_cves": total,
|
|
70
|
+
"watched": len(watchlist),
|
|
71
|
+
"by_severity": by_severity,
|
|
72
|
+
"by_software": by_software,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@app.get("/", response_class=HTMLResponse)
|
|
76
|
+
async def index():
|
|
77
|
+
html_path = Path(__file__).parent / "templates" / "index.html"
|
|
78
|
+
return HTMLResponse(content=html_path.read_text())
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>PatchRadar 🛡️</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #e6edf3; min-height: 100vh; }
|
|
10
|
+
header { background: #161b22; border-bottom: 1px solid #30363d; padding: 1rem 2rem; display: flex; align-items: center; gap: 1rem; }
|
|
11
|
+
header h1 { font-size: 1.4rem; font-weight: 600; }
|
|
12
|
+
header span { font-size: 1.5rem; }
|
|
13
|
+
.badge { background: #238636; color: #fff; font-size: 0.7rem; padding: 2px 8px; border-radius: 20px; font-weight: 600; }
|
|
14
|
+
main { padding: 2rem; max-width: 1400px; margin: 0 auto; }
|
|
15
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
16
|
+
.stat-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.2rem; }
|
|
17
|
+
.stat-card .label { font-size: 0.75rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
|
18
|
+
.stat-card .value { font-size: 2rem; font-weight: 700; }
|
|
19
|
+
.stat-card.critical .value { color: #f85149; }
|
|
20
|
+
.stat-card.high .value { color: #e3b341; }
|
|
21
|
+
.stat-card.medium .value { color: #d29922; }
|
|
22
|
+
.stat-card.low .value { color: #3fb950; }
|
|
23
|
+
.stat-card.total .value { color: #58a6ff; }
|
|
24
|
+
.grid { display: grid; grid-template-columns: 300px 1fr; gap: 1.5rem; }
|
|
25
|
+
.panel { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; }
|
|
26
|
+
.panel h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: #e6edf3; }
|
|
27
|
+
.watchlist-item { display: flex; align-items: center; justify-content: space-between; padding: 0.6rem 0.8rem; border-radius: 6px; margin-bottom: 0.4rem; background: #0d1117; border: 1px solid #30363d; }
|
|
28
|
+
.watchlist-item span { font-size: 0.9rem; }
|
|
29
|
+
.btn-remove { background: none; border: none; color: #f85149; cursor: pointer; font-size: 1rem; padding: 2px 6px; border-radius: 4px; }
|
|
30
|
+
.btn-remove:hover { background: #2d1316; }
|
|
31
|
+
.add-form { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
|
32
|
+
.add-form input { flex: 1; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 0.5rem 0.8rem; color: #e6edf3; font-size: 0.9rem; outline: none; }
|
|
33
|
+
.add-form input:focus { border-color: #58a6ff; }
|
|
34
|
+
.btn { padding: 0.5rem 1rem; border-radius: 6px; border: none; cursor: pointer; font-size: 0.85rem; font-weight: 600; transition: opacity 0.2s; }
|
|
35
|
+
.btn:hover { opacity: 0.85; }
|
|
36
|
+
.btn-primary { background: #238636; color: #fff; }
|
|
37
|
+
.btn-scan { background: #1f6feb; color: #fff; width: 100%; margin-top: 1rem; padding: 0.7rem; }
|
|
38
|
+
.filters { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
39
|
+
.filter-btn { padding: 0.3rem 0.8rem; border-radius: 20px; border: 1px solid #30363d; background: #0d1117; color: #8b949e; cursor: pointer; font-size: 0.8rem; }
|
|
40
|
+
.filter-btn.active { border-color: #58a6ff; color: #58a6ff; background: #0c2d6b; }
|
|
41
|
+
.filter-btn.critical.active { border-color: #f85149; color: #f85149; background: #2d1316; }
|
|
42
|
+
.filter-btn.high.active { border-color: #e3b341; color: #e3b341; background: #2d2005; }
|
|
43
|
+
.filter-btn.medium.active { border-color: #d29922; color: #d29922; background: #2d1f00; }
|
|
44
|
+
.filter-btn.low.active { border-color: #3fb950; color: #3fb950; background: #0d2b1a; }
|
|
45
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
46
|
+
th { text-align: left; padding: 0.6rem 0.8rem; color: #8b949e; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; border-bottom: 1px solid #30363d; }
|
|
47
|
+
td { padding: 0.7rem 0.8rem; border-bottom: 1px solid #21262d; vertical-align: top; }
|
|
48
|
+
tr:hover td { background: #1c2128; }
|
|
49
|
+
.severity-badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 0.72rem; font-weight: 700; }
|
|
50
|
+
.sev-CRITICAL { background: #2d1316; color: #f85149; }
|
|
51
|
+
.sev-HIGH { background: #2d2005; color: #e3b341; }
|
|
52
|
+
.sev-MEDIUM { background: #2d1f00; color: #d29922; }
|
|
53
|
+
.sev-LOW { background: #0d2b1a; color: #3fb950; }
|
|
54
|
+
.sev-UNKNOWN { background: #21262d; color: #8b949e; }
|
|
55
|
+
.score { font-weight: 700; }
|
|
56
|
+
.score.critical { color: #f85149; }
|
|
57
|
+
.score.high { color: #e3b341; }
|
|
58
|
+
.score.medium { color: #d29922; }
|
|
59
|
+
.score.low { color: #3fb950; }
|
|
60
|
+
.cve-link { color: #58a6ff; text-decoration: none; font-family: monospace; }
|
|
61
|
+
.cve-link:hover { text-decoration: underline; }
|
|
62
|
+
.desc { color: #8b949e; max-width: 400px; }
|
|
63
|
+
.source-badge { font-size: 0.7rem; background: #21262d; padding: 2px 6px; border-radius: 4px; color: #8b949e; }
|
|
64
|
+
.scanning { opacity: 0.6; }
|
|
65
|
+
.toast { position: fixed; bottom: 2rem; right: 2rem; background: #238636; color: #fff; padding: 0.8rem 1.2rem; border-radius: 8px; font-size: 0.9rem; display: none; z-index: 100; }
|
|
66
|
+
.empty { text-align: center; padding: 3rem; color: #8b949e; }
|
|
67
|
+
.chart-container { margin-top: 1rem; }
|
|
68
|
+
.chart-bar { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 0.6rem; }
|
|
69
|
+
.chart-bar .label { width: 80px; font-size: 0.8rem; color: #8b949e; text-align: right; }
|
|
70
|
+
.chart-bar .bar-wrap { flex: 1; background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; }
|
|
71
|
+
.chart-bar .bar { height: 100%; border-radius: 4px; transition: width 0.5s ease; display: flex; align-items: center; padding-left: 8px; font-size: 0.75rem; font-weight: 600; }
|
|
72
|
+
.bar-critical { background: #f85149; }
|
|
73
|
+
.bar-high { background: #e3b341; }
|
|
74
|
+
.bar-medium { background: #d29922; }
|
|
75
|
+
.bar-low { background: #3fb950; }
|
|
76
|
+
.bar-unknown { background: #8b949e; }
|
|
77
|
+
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid #fff3; border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 6px; }
|
|
78
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<header>
|
|
83
|
+
<span>🛡️</span>
|
|
84
|
+
<h1>PatchRadar</h1>
|
|
85
|
+
<span class="badge">2026.8.1</span>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
<main>
|
|
89
|
+
<!-- Stats -->
|
|
90
|
+
<div class="stats-grid" id="stats-grid">
|
|
91
|
+
<div class="stat-card total"><div class="label">Total CVEs</div><div class="value" id="stat-total">—</div></div>
|
|
92
|
+
<div class="stat-card critical"><div class="label">Critical</div><div class="value" id="stat-critical">—</div></div>
|
|
93
|
+
<div class="stat-card high"><div class="label">High</div><div class="value" id="stat-high">—</div></div>
|
|
94
|
+
<div class="stat-card medium"><div class="label">Medium</div><div class="value" id="stat-medium">—</div></div>
|
|
95
|
+
<div class="stat-card low"><div class="label">Low</div><div class="value" id="stat-low">—</div></div>
|
|
96
|
+
<div class="stat-card total"><div class="label">Watching</div><div class="value" id="stat-watched">—</div></div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="grid">
|
|
100
|
+
<!-- Watchlist -->
|
|
101
|
+
<div>
|
|
102
|
+
<div class="panel">
|
|
103
|
+
<h2>📋 Watchlist</h2>
|
|
104
|
+
<div id="watchlist-items"></div>
|
|
105
|
+
<div class="add-form">
|
|
106
|
+
<input type="text" id="add-input" placeholder="Add software..." onkeydown="if(event.key==='Enter')addSoftware()">
|
|
107
|
+
<button class="btn btn-primary" onclick="addSoftware()">Add</button>
|
|
108
|
+
</div>
|
|
109
|
+
<button class="btn btn-scan" id="scan-btn" onclick="scanAll()">🔍 Scan All</button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Chart -->
|
|
113
|
+
<div class="panel" style="margin-top: 1rem;">
|
|
114
|
+
<h2>📊 By Severity</h2>
|
|
115
|
+
<div class="chart-container" id="severity-chart"></div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- By Software -->
|
|
119
|
+
<div class="panel" style="margin-top: 1rem;">
|
|
120
|
+
<h2>📦 By Software</h2>
|
|
121
|
+
<div class="chart-container" id="software-chart"></div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- CVE Table -->
|
|
126
|
+
<div class="panel">
|
|
127
|
+
<h2>🚨 CVEs</h2>
|
|
128
|
+
<div class="filters">
|
|
129
|
+
<button class="filter-btn active" onclick="setFilter('ALL', this)">All</button>
|
|
130
|
+
<button class="filter-btn critical" onclick="setFilter('CRITICAL', this)">Critical</button>
|
|
131
|
+
<button class="filter-btn high" onclick="setFilter('HIGH', this)">High</button>
|
|
132
|
+
<button class="filter-btn medium" onclick="setFilter('MEDIUM', this)">Medium</button>
|
|
133
|
+
<button class="filter-btn low" onclick="setFilter('LOW', this)">Low</button>
|
|
134
|
+
</div>
|
|
135
|
+
<div id="cve-table-wrap">
|
|
136
|
+
<div class="empty">Run a scan to see CVEs</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</main>
|
|
141
|
+
|
|
142
|
+
<div class="toast" id="toast"></div>
|
|
143
|
+
|
|
144
|
+
<script>
|
|
145
|
+
let allCves = [];
|
|
146
|
+
let currentFilter = 'ALL';
|
|
147
|
+
|
|
148
|
+
async function api(path, method='GET', body=null) {
|
|
149
|
+
const opts = { method, headers: {'Content-Type': 'application/json'} };
|
|
150
|
+
if (body) opts.body = JSON.stringify(body);
|
|
151
|
+
const r = await fetch(path, opts);
|
|
152
|
+
return r.json();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toast(msg, color='#238636') {
|
|
156
|
+
const t = document.getElementById('toast');
|
|
157
|
+
t.textContent = msg;
|
|
158
|
+
t.style.background = color;
|
|
159
|
+
t.style.display = 'block';
|
|
160
|
+
setTimeout(() => t.style.display = 'none', 3000);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function loadWatchlist() {
|
|
164
|
+
const data = await api('/api/watchlist');
|
|
165
|
+
const el = document.getElementById('watchlist-items');
|
|
166
|
+
if (!data.watchlist.length) {
|
|
167
|
+
el.innerHTML = '<div style="color:#8b949e;font-size:0.85rem;padding:0.5rem 0">No software added yet</div>';
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
el.innerHTML = data.watchlist.map(sw => `
|
|
171
|
+
<div class="watchlist-item">
|
|
172
|
+
<span>📦 ${sw}</span>
|
|
173
|
+
<button class="btn-remove" onclick="removeSoftware('${sw}')" title="Remove">✕</button>
|
|
174
|
+
</div>
|
|
175
|
+
`).join('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function addSoftware() {
|
|
179
|
+
const input = document.getElementById('add-input');
|
|
180
|
+
const sw = input.value.trim();
|
|
181
|
+
if (!sw) return;
|
|
182
|
+
const data = await api(`/api/watchlist/${encodeURIComponent(sw)}`, 'POST');
|
|
183
|
+
if (data.added) {
|
|
184
|
+
toast(`✅ Added ${sw}`);
|
|
185
|
+
input.value = '';
|
|
186
|
+
await loadWatchlist();
|
|
187
|
+
await loadStats();
|
|
188
|
+
} else {
|
|
189
|
+
toast(`⚠️ ${sw} already in watchlist`, '#d29922');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function removeSoftware(sw) {
|
|
194
|
+
await api(`/api/watchlist/${encodeURIComponent(sw)}`, 'DELETE');
|
|
195
|
+
toast(`🗑️ Removed ${sw}`, '#f85149');
|
|
196
|
+
await loadWatchlist();
|
|
197
|
+
await loadStats();
|
|
198
|
+
await loadCves();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function scanAll() {
|
|
202
|
+
const btn = document.getElementById('scan-btn');
|
|
203
|
+
btn.innerHTML = '<span class="spinner"></span>Scanning...';
|
|
204
|
+
btn.disabled = true;
|
|
205
|
+
try {
|
|
206
|
+
const data = await api('/api/scan?days=30', 'POST');
|
|
207
|
+
toast(`✅ Found ${data.total} CVEs`);
|
|
208
|
+
await loadCves();
|
|
209
|
+
await loadStats();
|
|
210
|
+
} finally {
|
|
211
|
+
btn.innerHTML = '🔍 Scan All';
|
|
212
|
+
btn.disabled = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function loadCves() {
|
|
217
|
+
const data = await api('/api/cves?limit=200');
|
|
218
|
+
allCves = data.cves;
|
|
219
|
+
renderTable();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function setFilter(severity, btn) {
|
|
223
|
+
currentFilter = severity;
|
|
224
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
225
|
+
btn.classList.add('active');
|
|
226
|
+
renderTable();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function severityClass(s) {
|
|
230
|
+
const m = {'CRITICAL':'critical','HIGH':'high','MEDIUM':'medium','LOW':'low'};
|
|
231
|
+
return m[(s||'').toUpperCase()] || '';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function scoreClass(score) {
|
|
235
|
+
if (!score) return '';
|
|
236
|
+
if (score >= 9.0) return 'critical';
|
|
237
|
+
if (score >= 7.0) return 'high';
|
|
238
|
+
if (score >= 4.0) return 'medium';
|
|
239
|
+
return 'low';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderTable() {
|
|
243
|
+
const filtered = currentFilter === 'ALL'
|
|
244
|
+
? allCves
|
|
245
|
+
: allCves.filter(c => (c.severity||'').toUpperCase() === currentFilter);
|
|
246
|
+
|
|
247
|
+
const wrap = document.getElementById('cve-table-wrap');
|
|
248
|
+
if (!filtered.length) {
|
|
249
|
+
wrap.innerHTML = '<div class="empty">No CVEs found. Run a scan first.</div>';
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
wrap.innerHTML = `
|
|
254
|
+
<table>
|
|
255
|
+
<thead>
|
|
256
|
+
<tr>
|
|
257
|
+
<th>CVE ID</th>
|
|
258
|
+
<th>Software</th>
|
|
259
|
+
<th>Score</th>
|
|
260
|
+
<th>Severity</th>
|
|
261
|
+
<th>Description</th>
|
|
262
|
+
<th>Source</th>
|
|
263
|
+
<th>Published</th>
|
|
264
|
+
</tr>
|
|
265
|
+
</thead>
|
|
266
|
+
<tbody>
|
|
267
|
+
${filtered.map(cve => `
|
|
268
|
+
<tr>
|
|
269
|
+
<td><a class="cve-link" href="${cve.url||'#'}" target="_blank">${cve.id}</a></td>
|
|
270
|
+
<td><span class="source-badge">${cve.software||''}</span></td>
|
|
271
|
+
<td><span class="score ${scoreClass(cve.cvss_score)}">${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'}</span></td>
|
|
272
|
+
<td><span class="severity-badge sev-${(cve.severity||'UNKNOWN').toUpperCase()}">${cve.severity||'UNKNOWN'}</span></td>
|
|
273
|
+
<td class="desc">${(cve.description||'').substring(0, 120)}${(cve.description||'').length > 120 ? '...' : ''}</td>
|
|
274
|
+
<td><span class="source-badge">${cve.source||''}</span></td>
|
|
275
|
+
<td style="color:#8b949e;font-size:0.8rem">${cve.published_at ? cve.published_at.substring(0,10) : ''}</td>
|
|
276
|
+
</tr>
|
|
277
|
+
`).join('')}
|
|
278
|
+
</tbody>
|
|
279
|
+
</table>
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function loadStats() {
|
|
284
|
+
const data = await api('/api/stats');
|
|
285
|
+
document.getElementById('stat-total').textContent = data.total_cves;
|
|
286
|
+
document.getElementById('stat-watched').textContent = data.watched;
|
|
287
|
+
const sev = data.by_severity || {};
|
|
288
|
+
document.getElementById('stat-critical').textContent = sev['CRITICAL'] || 0;
|
|
289
|
+
document.getElementById('stat-high').textContent = sev['HIGH'] || 0;
|
|
290
|
+
document.getElementById('stat-medium').textContent = sev['MEDIUM'] || 0;
|
|
291
|
+
document.getElementById('stat-low').textContent = sev['LOW'] || 0;
|
|
292
|
+
|
|
293
|
+
const total = data.total_cves || 1;
|
|
294
|
+
const sevOrder = ['CRITICAL','HIGH','MEDIUM','LOW','UNKNOWN'];
|
|
295
|
+
const sevColors = {'CRITICAL':'critical','HIGH':'high','MEDIUM':'medium','LOW':'low','UNKNOWN':'unknown'};
|
|
296
|
+
document.getElementById('severity-chart').innerHTML = sevOrder
|
|
297
|
+
.filter(s => sev[s])
|
|
298
|
+
.map(s => `
|
|
299
|
+
<div class="chart-bar">
|
|
300
|
+
<div class="label">${s}</div>
|
|
301
|
+
<div class="bar-wrap">
|
|
302
|
+
<div class="bar bar-${sevColors[s]}" style="width:${Math.round((sev[s]/total)*100)}%">
|
|
303
|
+
${sev[s]}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
`).join('');
|
|
308
|
+
|
|
309
|
+
const bySw = data.by_software || {};
|
|
310
|
+
const maxSw = Math.max(...Object.values(bySw), 1);
|
|
311
|
+
document.getElementById('software-chart').innerHTML = Object.entries(bySw)
|
|
312
|
+
.sort((a,b) => b[1]-a[1])
|
|
313
|
+
.map(([sw, count]) => `
|
|
314
|
+
<div class="chart-bar">
|
|
315
|
+
<div class="label">${sw}</div>
|
|
316
|
+
<div class="bar-wrap">
|
|
317
|
+
<div class="bar bar-high" style="width:${Math.round((count/maxSw)*100)}%">
|
|
318
|
+
${count}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
`).join('');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function init() {
|
|
326
|
+
await loadWatchlist();
|
|
327
|
+
await loadStats();
|
|
328
|
+
await loadCves();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
init();
|
|
332
|
+
</script>
|
|
333
|
+
</body>
|
|
334
|
+
</html>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import asyncio
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from patchradar.db.database import (
|
|
8
|
+
init_db, add_to_watchlist, get_watchlist,
|
|
9
|
+
remove_from_watchlist, get_cves, save_cve
|
|
10
|
+
)
|
|
11
|
+
from patchradar.collectors.nvd import fetch_cves
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="patchradar",
|
|
15
|
+
help="Realtime CVE intelligence for your software stack 🛡️",
|
|
16
|
+
add_completion=False,
|
|
17
|
+
)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
def run(coro):
|
|
21
|
+
return asyncio.run(coro)
|
|
22
|
+
|
|
23
|
+
@app.callback()
|
|
24
|
+
def startup():
|
|
25
|
+
run(init_db())
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def add(software: str = typer.Argument(..., help="Software to monitor")):
|
|
29
|
+
"""Add software to your watchlist."""
|
|
30
|
+
added = run(add_to_watchlist(software))
|
|
31
|
+
if added:
|
|
32
|
+
console.print(f"✅ [green]Added[/green] [bold]{software}[/bold] to watchlist")
|
|
33
|
+
else:
|
|
34
|
+
console.print(f"⚠️ [yellow]{software}[/yellow] is already in your watchlist")
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def remove(software: str = typer.Argument(..., help="Software to remove")):
|
|
38
|
+
"""Remove software from your watchlist."""
|
|
39
|
+
removed = run(remove_from_watchlist(software))
|
|
40
|
+
if removed:
|
|
41
|
+
console.print(f"🗑️ [red]Removed[/red] [bold]{software}[/bold] from watchlist")
|
|
42
|
+
else:
|
|
43
|
+
console.print(f"❌ [red]{software}[/red] not found in watchlist")
|
|
44
|
+
|
|
45
|
+
@app.command(name="list")
|
|
46
|
+
def list_watchlist():
|
|
47
|
+
"""Show your current watchlist."""
|
|
48
|
+
items = run(get_watchlist())
|
|
49
|
+
if not items:
|
|
50
|
+
console.print("📭 Your watchlist is empty. Use [bold]patchradar add <software>[/bold]")
|
|
51
|
+
return
|
|
52
|
+
table = Table(title="🛡️ PatchRadar Watchlist", box=box.ROUNDED)
|
|
53
|
+
table.add_column("Software", style="cyan bold")
|
|
54
|
+
for item in items:
|
|
55
|
+
table.add_row(item)
|
|
56
|
+
console.print(table)
|
|
57
|
+
|
|
58
|
+
@app.command()
|
|
59
|
+
def scan(
|
|
60
|
+
software: str = typer.Argument(None, help="Software to scan (or all watchlist)"),
|
|
61
|
+
days: int = typer.Option(7, "--days", "-d", help="Days back to search"),
|
|
62
|
+
):
|
|
63
|
+
"""Scan for CVEs affecting your software."""
|
|
64
|
+
async def _scan():
|
|
65
|
+
targets = [software] if software else await get_watchlist()
|
|
66
|
+
if not targets:
|
|
67
|
+
console.print("📭 Nothing to scan. Add software with [bold]patchradar add[/bold]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
total = 0
|
|
71
|
+
for target in targets:
|
|
72
|
+
with console.status(f"[cyan]Scanning {target}...[/cyan]"):
|
|
73
|
+
cves = await fetch_cves(target, days_back=days)
|
|
74
|
+
|
|
75
|
+
for cve in cves:
|
|
76
|
+
await save_cve(cve)
|
|
77
|
+
|
|
78
|
+
total += len(cves)
|
|
79
|
+
if cves:
|
|
80
|
+
_print_cves(target, cves)
|
|
81
|
+
else:
|
|
82
|
+
console.print(f"✅ [green]{target}[/green] — no CVEs found in last {days} days")
|
|
83
|
+
|
|
84
|
+
console.print(f"\n📊 Total: [bold]{total}[/bold] CVEs found")
|
|
85
|
+
|
|
86
|
+
run(_scan())
|
|
87
|
+
|
|
88
|
+
@app.command()
|
|
89
|
+
def status():
|
|
90
|
+
"""Show latest CVEs from your watchlist."""
|
|
91
|
+
async def _status():
|
|
92
|
+
cves = await get_cves(limit=20)
|
|
93
|
+
if not cves:
|
|
94
|
+
console.print("📭 No CVEs in database yet. Run [bold]patchradar scan[/bold]")
|
|
95
|
+
return
|
|
96
|
+
_print_cves_table(cves)
|
|
97
|
+
run(_status())
|
|
98
|
+
|
|
99
|
+
def _print_cves(software: str, cves: list):
|
|
100
|
+
table = Table(
|
|
101
|
+
title=f"🚨 CVEs for [bold]{software}[/bold]",
|
|
102
|
+
box=box.ROUNDED,
|
|
103
|
+
show_lines=True
|
|
104
|
+
)
|
|
105
|
+
table.add_column("CVE ID", style="bold cyan", no_wrap=True)
|
|
106
|
+
table.add_column("Score", justify="center", width=6)
|
|
107
|
+
table.add_column("Severity", justify="center", width=10)
|
|
108
|
+
table.add_column("Description", max_width=60)
|
|
109
|
+
|
|
110
|
+
for cve in cves:
|
|
111
|
+
score = cve.get("cvss_score")
|
|
112
|
+
severity = cve.get("severity", "UNKNOWN")
|
|
113
|
+
score_str = f"{score:.1f}" if score else "N/A"
|
|
114
|
+
severity_color = {
|
|
115
|
+
"CRITICAL": "red bold",
|
|
116
|
+
"HIGH": "red",
|
|
117
|
+
"MEDIUM": "yellow",
|
|
118
|
+
"LOW": "green",
|
|
119
|
+
}.get(severity.upper(), "white")
|
|
120
|
+
|
|
121
|
+
table.add_row(
|
|
122
|
+
cve["id"],
|
|
123
|
+
score_str,
|
|
124
|
+
Text(severity, style=severity_color),
|
|
125
|
+
cve.get("description", "")[:120] + "..." if len(cve.get("description", "")) > 120 else cve.get("description", ""),
|
|
126
|
+
)
|
|
127
|
+
console.print(table)
|
|
128
|
+
|
|
129
|
+
def _print_cves_table(cves: list):
|
|
130
|
+
table = Table(title="🛡️ PatchRadar — Latest CVEs", box=box.ROUNDED, show_lines=True)
|
|
131
|
+
table.add_column("CVE ID", style="bold cyan", no_wrap=True)
|
|
132
|
+
table.add_column("Software", style="blue")
|
|
133
|
+
table.add_column("Score", justify="center", width=6)
|
|
134
|
+
table.add_column("Severity", justify="center", width=10)
|
|
135
|
+
table.add_column("Source", width=6)
|
|
136
|
+
|
|
137
|
+
for cve in cves:
|
|
138
|
+
score = cve.get("cvss_score")
|
|
139
|
+
severity = cve.get("severity", "UNKNOWN")
|
|
140
|
+
score_str = f"{score:.1f}" if score else "N/A"
|
|
141
|
+
severity_color = {
|
|
142
|
+
"CRITICAL": "red bold",
|
|
143
|
+
"HIGH": "red",
|
|
144
|
+
"MEDIUM": "yellow",
|
|
145
|
+
"LOW": "green",
|
|
146
|
+
}.get(severity.upper(), "white")
|
|
147
|
+
|
|
148
|
+
table.add_row(
|
|
149
|
+
cve["id"],
|
|
150
|
+
cve.get("software", ""),
|
|
151
|
+
score_str,
|
|
152
|
+
Text(severity, style=severity_color),
|
|
153
|
+
cve.get("source", ""),
|
|
154
|
+
)
|
|
155
|
+
console.print(table)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command()
|
|
159
|
+
def serve(
|
|
160
|
+
host: str = typer.Option("127.0.0.1", "--host", "-h"),
|
|
161
|
+
port: int = typer.Option(8000, "--port", "-p"),
|
|
162
|
+
):
|
|
163
|
+
"""Launch the PatchRadar web UI."""
|
|
164
|
+
import uvicorn
|
|
165
|
+
console.print(f"🛡️ [bold]PatchRadar[/bold] UI → [cyan]http://{host}:{port}[/cyan]")
|
|
166
|
+
uvicorn.run("patchradar.api.main:app", host=host, port=port, reload=False)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main():
|
|
170
|
+
app()
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
|
|
4
|
+
NVD_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
|
5
|
+
|
|
6
|
+
async def fetch_cves(keyword: str, days_back: int = 7) -> list[dict]:
|
|
7
|
+
"""Fetch CVEs from NVD for a given keyword."""
|
|
8
|
+
start = (datetime.utcnow() - timedelta(days=days_back)).strftime(
|
|
9
|
+
"%Y-%m-%dT00:00:00.000"
|
|
10
|
+
)
|
|
11
|
+
end = datetime.utcnow().strftime("%Y-%m-%dT23:59:59.999")
|
|
12
|
+
|
|
13
|
+
params = {
|
|
14
|
+
"keywordSearch": keyword,
|
|
15
|
+
"pubStartDate": start,
|
|
16
|
+
"pubEndDate": end,
|
|
17
|
+
"resultsPerPage": 50,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
21
|
+
try:
|
|
22
|
+
response = await client.get(NVD_API, params=params)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
data = response.json()
|
|
25
|
+
except Exception as e:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
results = []
|
|
29
|
+
for item in data.get("vulnerabilities", []):
|
|
30
|
+
cve = item.get("cve", {})
|
|
31
|
+
cve_id = cve.get("id", "")
|
|
32
|
+
|
|
33
|
+
# Descrizione inglese
|
|
34
|
+
descriptions = cve.get("descriptions", [])
|
|
35
|
+
description = next(
|
|
36
|
+
(d["value"] for d in descriptions if d["lang"] == "en"), ""
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# CVSS score
|
|
40
|
+
cvss_score = None
|
|
41
|
+
cvss_version = None
|
|
42
|
+
severity = "UNKNOWN"
|
|
43
|
+
metrics = cve.get("metrics", {})
|
|
44
|
+
|
|
45
|
+
for version in ["cvssMetricV40", "cvssMetricV31", "cvssMetricV30", "cvssMetricV2"]:
|
|
46
|
+
if version in metrics and metrics[version]:
|
|
47
|
+
m = metrics[version][0]
|
|
48
|
+
cvss_data = m.get("cvssData", {})
|
|
49
|
+
cvss_score = cvss_data.get("baseScore")
|
|
50
|
+
cvss_version = cvss_data.get("version")
|
|
51
|
+
severity = m.get("baseSeverity", cvss_data.get("baseSeverity", "UNKNOWN"))
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
results.append({
|
|
55
|
+
"id": cve_id,
|
|
56
|
+
"software": keyword.lower(),
|
|
57
|
+
"description": description,
|
|
58
|
+
"cvss_score": cvss_score,
|
|
59
|
+
"cvss_version": cvss_version,
|
|
60
|
+
"severity": severity,
|
|
61
|
+
"published_at": cve.get("published"),
|
|
62
|
+
"source": "NVD",
|
|
63
|
+
"url": f"https://nvd.nist.gov/vuln/detail/{cve_id}",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return results
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import aiosqlite
|
|
2
|
+
import asyncio
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
DB_PATH = Path.home() / ".patchradar" / "patchradar.db"
|
|
6
|
+
|
|
7
|
+
async def init_db():
|
|
8
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
9
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
10
|
+
await db.execute("""
|
|
11
|
+
CREATE TABLE IF NOT EXISTS watchlist (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
name TEXT UNIQUE NOT NULL,
|
|
14
|
+
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
15
|
+
)
|
|
16
|
+
""")
|
|
17
|
+
await db.execute("""
|
|
18
|
+
CREATE TABLE IF NOT EXISTS cves (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
software TEXT NOT NULL,
|
|
21
|
+
description TEXT,
|
|
22
|
+
cvss_score REAL,
|
|
23
|
+
cvss_version TEXT,
|
|
24
|
+
severity TEXT,
|
|
25
|
+
published_at TIMESTAMP,
|
|
26
|
+
source TEXT,
|
|
27
|
+
url TEXT,
|
|
28
|
+
seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
29
|
+
)
|
|
30
|
+
""")
|
|
31
|
+
await db.commit()
|
|
32
|
+
|
|
33
|
+
async def add_to_watchlist(name: str) -> bool:
|
|
34
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
35
|
+
try:
|
|
36
|
+
await db.execute(
|
|
37
|
+
"INSERT INTO watchlist (name) VALUES (?)", (name.lower(),)
|
|
38
|
+
)
|
|
39
|
+
await db.commit()
|
|
40
|
+
return True
|
|
41
|
+
except aiosqlite.IntegrityError:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
async def get_watchlist() -> list[str]:
|
|
45
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
46
|
+
async with db.execute("SELECT name FROM watchlist ORDER BY name") as cursor:
|
|
47
|
+
rows = await cursor.fetchall()
|
|
48
|
+
return [row[0] for row in rows]
|
|
49
|
+
|
|
50
|
+
async def remove_from_watchlist(name: str) -> bool:
|
|
51
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
52
|
+
cursor = await db.execute(
|
|
53
|
+
"DELETE FROM watchlist WHERE name = ?", (name.lower(),)
|
|
54
|
+
)
|
|
55
|
+
await db.commit()
|
|
56
|
+
return cursor.rowcount > 0
|
|
57
|
+
|
|
58
|
+
async def save_cve(cve: dict) -> bool:
|
|
59
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
60
|
+
try:
|
|
61
|
+
await db.execute("""
|
|
62
|
+
INSERT OR IGNORE INTO cves
|
|
63
|
+
(id, software, description, cvss_score, cvss_version,
|
|
64
|
+
severity, published_at, source, url)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
66
|
+
""", (
|
|
67
|
+
cve["id"], cve["software"], cve["description"],
|
|
68
|
+
cve.get("cvss_score"), cve.get("cvss_version"),
|
|
69
|
+
cve.get("severity"), cve.get("published_at"),
|
|
70
|
+
cve.get("source"), cve.get("url")
|
|
71
|
+
))
|
|
72
|
+
await db.commit()
|
|
73
|
+
return cursor.rowcount > 0
|
|
74
|
+
except Exception:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
async def get_cves(software: str = None, limit: int = 50) -> list[dict]:
|
|
78
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
79
|
+
db.row_factory = aiosqlite.Row
|
|
80
|
+
if software:
|
|
81
|
+
async with db.execute(
|
|
82
|
+
"SELECT * FROM cves WHERE software = ? ORDER BY published_at DESC LIMIT ?",
|
|
83
|
+
(software.lower(), limit)
|
|
84
|
+
) as cursor:
|
|
85
|
+
rows = await cursor.fetchall()
|
|
86
|
+
else:
|
|
87
|
+
async with db.execute(
|
|
88
|
+
"SELECT * FROM cves ORDER BY published_at DESC LIMIT ?",
|
|
89
|
+
(limit,)
|
|
90
|
+
) as cursor:
|
|
91
|
+
rows = await cursor.fetchall()
|
|
92
|
+
return [dict(row) for row in rows]
|