first commit

This commit is contained in:
Mateusz Gruszczyński
2026-02-13 12:42:53 +01:00
commit bc1b4279de
21 changed files with 3835 additions and 0 deletions

20
.dockerignore Normal file
View File

@@ -0,0 +1,20 @@
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info/
dist/
build/
.git/
.gitignore
README.md
.env
*.db
*.db-journal
*.db-wal
*.log
.vscode/
.idea/

257
.env.example Normal file
View File

@@ -0,0 +1,257 @@
# ============================================================
# CVE MONITOR - CONFIGURATION FILE
# ============================================================
# Copy this file to .env and adjust values for your environment
# ============================================================
# APPLICATION SETTINGS
# ============================================================
APP_NAME=CVE Monitor
APP_VERSION=1.0.0
DEBUG=False
HOST=0.0.0.0
PORT=5000
# ============================================================
# DATABASE CONFIGURATION
# ============================================================
DATABASE_PATH=./cve_db/cve_cache.db
DATABASE_WAL_MODE=True
DATABASE_CACHE_SIZE=10000
# ============================================================
# LOGGING CONFIGURATION
# ============================================================
LOG_LEVEL=INFO
LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s
LOG_FILE=./logs/cve_monitor.log
LOG_MAX_BYTES=10485760
LOG_BACKUP_COUNT=5
# ============================================================
# AUTO-UPDATE CONFIGURATION
# ============================================================
# Enable automatic CVE updates in background
ENABLE_AUTO_UPDATE=True
# How often to check for new CVEs (in hours)
UPDATE_INTERVAL_HOURS=1
# How many days back to fetch CVEs on first run
INITIAL_LOOKBACK_DAYS=365
# Cache duration in hours before considering data stale
CACHE_HOURS=24
# ============================================================
# EXTERNAL API KEYS (Optional but Recommended)
# ============================================================
# NVD API Key - Get yours at: https://nvd.nist.gov/developers/request-an-api-key
# Without API key: 5 requests per 30 seconds
# With API key: 50 requests per 30 seconds
NVD_API_KEY=
# GitHub Personal Access Token - Get yours at: https://github.com/settings/tokens
# Increases rate limit from 60 to 5000 requests per hour
GITHUB_TOKEN=
# ============================================================
# API ENDPOINTS (Advanced - Don't change unless necessary)
# ============================================================
NVD_API_URL=https://services.nvd.nist.gov/rest/json/cves/2.0
GITHUB_ADVISORIES_URL=https://api.github.com/advisories
NVD_TIMEOUT=30
GITHUB_TIMEOUT=15
# ============================================================
# GUNICORN CONFIGURATION (Production)
# ============================================================
WORKERS=4
WORKER_TIMEOUT=120
WORKER_CLASS=sync
MAX_REQUESTS=1000
MAX_REQUESTS_JITTER=50
# ============================================================
# SECURITY SETTINGS
# ============================================================
# Enable/disable security headers
ENABLE_SECURITY_HEADERS=True
# Enable rate limiting to prevent abuse
ENABLE_RATE_LIMITING=True
# Rate limit: requests per minute per IP
RATE_LIMIT_PER_MINUTE=60
# Enable gzip compression
ENABLE_COMPRESSION=True
# Enable ETag for caching
ENABLE_ETAG=True
# Content Security Policy
CSP_DEFAULT_SRC='self'
CSP_SCRIPT_SRC='self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com
CSP_STYLE_SRC='self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com
CSP_FONT_SRC='self' cdnjs.cloudflare.com
CSP_IMG_SRC='self' data:
CSP_CONNECT_SRC='self' cdn.jsdelivr.net
# X-Frame-Options: DENY, SAMEORIGIN, or ALLOW-FROM uri
X_FRAME_OPTIONS=DENY
# HSTS max age in seconds (1 year = 31536000)
HSTS_MAX_AGE=31536000
# ============================================================
# FEATURE FLAGS
# ============================================================
# Enable/disable specific features
ENABLE_CHARTS=True
ENABLE_SEARCH=True
ENABLE_EXPORT=True
ENABLE_DARK_MODE=True
ENABLE_PAGINATION=True
# ============================================================
# UI CONFIGURATION
# ============================================================
# Items per page in CVE list
ITEMS_PER_PAGE=50
# Maximum search results to display
MAX_SEARCH_RESULTS=50
# ============================================================
# CDN URLS (for offline use, download and host locally)
# ============================================================
BOOTSTRAP_CSS_CDN=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css
BOOTSTRAP_JS_CDN=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
FONTAWESOME_CDN=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css
CHARTJS_CDN=https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js
# ============================================================
# DOCKER SPECIFIC SETTINGS
# ============================================================
# Set to True when running inside Docker
IS_DOCKER=False
# Timezone for Docker container
TZ=Europe/Warsaw
# ============================================================
# MONITORED VENDORS
# ============================================================
# Comma-separated list of vendor codes to monitor
# Available: microsoft,apple,fortinet,cisco,adobe,oracle,google,linux,vmware,paloalto,docker,kubernetes
MONITORED_VENDORS=microsoft,apple,cisco,fortinet,oracle,google,linux
# ============================================================
# NOTIFICATION SETTINGS (Future Feature)
# ============================================================
# Discord webhook for critical CVE notifications
DISCORD_WEBHOOK_URL=
# Slack webhook for notifications
SLACK_WEBHOOK_URL=
# Email notifications
SMTP_SERVER=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM=cve-monitor@example.com
SMTP_TO=admin@example.com
# Notification thresholds
NOTIFY_ON_CRITICAL=True
NOTIFY_ON_HIGH=False
NOTIFY_MIN_CVSS=9.0
# ============================================================
# BACKUP CONFIGURATION
# ============================================================
# Enable automatic database backups
ENABLE_BACKUPS=True
# Backup directory
BACKUP_DIR=./backups
# How many backup files to keep
BACKUP_RETENTION_DAYS=30
# Backup interval in hours
BACKUP_INTERVAL_HOURS=24
# ============================================================
# PROMETHEUS METRICS (Future Feature)
# ============================================================
ENABLE_METRICS=False
METRICS_PORT=9090
# ============================================================
# DEVELOPMENT SETTINGS
# ============================================================
# Enable Flask debug toolbar (development only)
FLASK_DEBUG_TOOLBAR=False
# Enable SQL query logging
SQL_DEBUG=False
# Disable security features for local development
# WARNING: Never use in production!
DEV_MODE=False
# ============================================================
# HEALTH CHECK
# ============================================================
# Health check endpoint timeout
HEALTH_CHECK_TIMEOUT=5
# ============================================================
# CORS SETTINGS (if using as API backend)
# ============================================================
ENABLE_CORS=False
CORS_ORIGINS=*
# Discord Bot Configuration
ENABLE_DISCORD_BOT=True
DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN_HERE
DISCORD_CHANNEL_ID=1234567890123456789
DISCORD_CHECK_INTERVAL_MINUTES=60
DISCORD_NOTIFY_CRITICAL=True
DISCORD_NOTIFY_HIGH=True
DISCORD_MIN_CVSS=7.0
# ============================================================
# NOTES
# ============================================================
# 1. Boolean values: True/False (case-sensitive)
# 2. Empty values will use defaults from config.py
# 3. Paths can be absolute or relative to project root
# 4. For production, always set DEBUG=False
# 5. Get NVD API key to avoid rate limits
# 6. Use strong CSP in production
# 7. Enable HTTPS in production (handled by reverse proxy)

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
__pycache__
logs/
venv
.env
cve_db

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM python:3.14-alpine
WORKDIR /app
RUN apk add --no-cache \
gcc \
musl-dev \
libffi-dev \
openssl-dev \
sqlite \
curl
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/cve_db /app/logs && \
chmod -R 777 /app/cve_db /app/logs
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["app"]

252
api.py Normal file
View File

@@ -0,0 +1,252 @@
from flask import Blueprint, jsonify, request, make_response
import logging
import csv
from io import StringIO
from datetime import datetime
import config
from cve_handler import CVEHandler
logger = logging.getLogger(__name__)
api_bp = Blueprint('api', __name__)
cve_handler = CVEHandler()
def api_response(data: any, status: int = 200) -> tuple:
return jsonify(data), status
def api_error(message: str, status: int = 400) -> tuple:
return jsonify({
'error': message,
'timestamp': datetime.utcnow().isoformat()
}), status
@api_bp.route('/vendors', methods=['GET'])
def get_vendors():
try:
summary = cve_handler.get_all_vendors_summary()
return api_response({
'vendors': summary,
'total_vendors': len(summary),
'timestamp': datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error in get_vendors: {e}", exc_info=True)
return api_error(str(e), 500)
@api_bp.route('/cve/<vendor_code>', methods=['GET'])
def get_vendor_cves(vendor_code: str):
try:
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
if not vendor:
return api_error(f"Unknown vendor: {vendor_code}", 404)
try:
limit = min(
int(request.args.get('limit', config.ITEMS_PER_PAGE)),
config.MAX_ITEMS_PER_PAGE
)
offset = max(int(request.args.get('offset', 0)), 0)
except ValueError:
return api_error("Invalid pagination parameters", 400)
cves = cve_handler.get_vendor_cves(
vendor_code,
limit=limit,
offset=offset
)
return api_response({
'vendor': {
'code': vendor_code,
'name': vendor['name']
},
'cves': cves,
'count': len(cves),
'limit': limit,
'offset': offset,
'timestamp': datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error in get_vendor_cves: {e}", exc_info=True)
return api_error(str(e), 500)
@api_bp.route('/stats/<vendor_code>', methods=['GET'])
def get_vendor_stats(vendor_code: str):
try:
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
if not vendor:
return api_error(f"Unknown vendor: {vendor_code}", 404)
stats = cve_handler.get_vendor_stats(vendor_code)
return api_response({
'vendor': {
'code': vendor_code,
'name': vendor['name']
},
'stats': stats,
'timestamp': datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error in get_vendor_stats: {e}", exc_info=True)
return api_error(str(e), 500)
@api_bp.route('/cve/<vendor_code>/filter', methods=['GET'])
def filter_vendor_cves(vendor_code: str):
try:
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
if not vendor:
return api_error(f"Unknown vendor: {vendor_code}", 404)
severity = request.args.get('severity', '').upper()
year = request.args.get('year', type=int)
try:
limit = min(
int(request.args.get('limit', config.ITEMS_PER_PAGE)),
config.MAX_ITEMS_PER_PAGE
)
offset = max(int(request.args.get('offset', 0)), 0)
except ValueError:
return api_error("Invalid pagination parameters", 400)
if severity and severity not in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
return api_error(f"Invalid severity: {severity}", 400)
cves = cve_handler.get_vendor_cves(
vendor_code,
limit=limit,
offset=offset,
severity=severity if severity else None,
year=year
)
return api_response({
'vendor': {
'code': vendor_code,
'name': vendor['name']
},
'filters': {
'severity': severity,
'year': year
},
'cves': cves,
'count': len(cves),
'timestamp': datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error in filter_vendor_cves: {e}", exc_info=True)
return api_error(str(e), 500)
@api_bp.route('/search', methods=['GET'])
def search_cves():
if not config.ENABLE_SEARCH:
return api_error("Search is disabled", 403)
try:
query = request.args.get('q', '').strip()
if not query:
return api_error("Missing search query", 400)
if len(query) < 3:
return api_error("Query too short (min 3 characters)", 400)
limit = min(
int(request.args.get('limit', config.ITEMS_PER_PAGE)),
config.MAX_ITEMS_PER_PAGE
)
results = cve_handler.search_cves(query, limit=limit)
return api_response({
'query': query,
'results': results,
'count': len(results),
'timestamp': datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error in search_cves: {e}", exc_info=True)
return api_error(str(e), 500)
@api_bp.route('/update/<vendor_code>', methods=['POST'])
def trigger_update(vendor_code: str):
try:
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
if not vendor:
return api_error(f"Unknown vendor: {vendor_code}", 404)
logger.info(f"Manual update triggered for {vendor_code}")
success = cve_handler.update_vendor_cache(vendor_code, force=True)
if success:
return api_response({
'message': f'Update triggered for {vendor_code}',
'vendor': vendor['name'],
'status': 'success',
'timestamp': datetime.utcnow().isoformat()
})
else:
return api_error('Update failed', 500)
except Exception as e:
logger.error(f"Error in trigger_update: {e}", exc_info=True)
return api_error(str(e), 500)
@api_bp.route('/export/<vendor_code>/<format>', methods=['GET'])
def export_cves(vendor_code: str, format: str):
if not config.ENABLE_EXPORT:
return api_error("Export is disabled", 403)
try:
if format not in config.EXPORT_FORMATS:
return api_error(f"Invalid format: {format}. Supported: {', '.join(config.EXPORT_FORMATS)}", 400)
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
if not vendor:
return api_error(f"Unknown vendor: {vendor_code}", 404)
cves = cve_handler.get_vendor_cves(
vendor_code,
limit=config.EXPORT_MAX_ITEMS
)
if format == 'json':
return api_response({
'vendor': {
'code': vendor_code,
'name': vendor['name']
},
'export_date': datetime.utcnow().isoformat(),
'cve_count': len(cves),
'cves': cves
})
elif format == 'csv':
output = StringIO()
writer = csv.DictWriter(output, fieldnames=[
'cve_id', 'severity', 'cvss_score', 'published_date',
'last_modified', 'description'
])
writer.writeheader()
for cve in cves:
writer.writerow({
'cve_id': cve.get('cve_id', ''),
'severity': cve.get('severity', ''),
'cvss_score': cve.get('cvss_score', ''),
'published_date': cve.get('published_date', ''),
'last_modified': cve.get('last_modified', ''),
'description': (cve.get('description', '') or '')[:500]
})
response = make_response(output.getvalue())
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
response.headers['Content-Disposition'] = f'attachment; filename={vendor_code}_cves_{datetime.utcnow().strftime("%Y%m%d")}.csv'
return response
except Exception as e:
logger.error(f"Error in export_cves: {e}", exc_info=True)
return api_error(str(e), 500)

303
app.py Normal file
View File

@@ -0,0 +1,303 @@
#!/usr/bin/env python3
from flask import Flask, render_template, make_response, request, jsonify
import gzip
import hashlib
import logging
from functools import wraps
from datetime import datetime
import threading
import time
import os
from logging.handlers import RotatingFileHandler
from dotenv import load_dotenv
load_dotenv()
import config
from cve_handler import CVEHandler, update_all_vendors
import api
def setup_logging():
log_level = getattr(logging, config.LOG_LEVEL.upper(), logging.INFO)
formatter = logging.Formatter(config.LOG_FORMAT)
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.setLevel(log_level)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setLevel(log_level)
root_logger.addHandler(console_handler)
if config.LOG_FILE:
os.makedirs(os.path.dirname(config.LOG_FILE), exist_ok=True)
file_handler = RotatingFileHandler(
config.LOG_FILE,
maxBytes=config.LOG_MAX_BYTES,
backupCount=config.LOG_BACKUP_COUNT
)
file_handler.setFormatter(formatter)
file_handler.setLevel(log_level)
root_logger.addHandler(file_handler)
logging.getLogger('werkzeug').handlers.clear()
logging.getLogger('werkzeug').setLevel(logging.WARNING if not config.DEBUG else logging.INFO)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('requests').setLevel(logging.WARNING)
setup_logging()
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.config['JSON_SORT_KEYS'] = False
if not config.DEBUG:
import logging as flask_logging
flask_logging.getLogger('werkzeug').disabled = True
app.register_blueprint(api.api_bp, url_prefix='/api')
cve_handler = CVEHandler()
@app.context_processor
def inject_config():
return {'config': config}
def add_security_headers(response):
if config.ENABLE_SECURITY_HEADERS:
for header, value in config.SECURITY_HEADERS.items():
response.headers[header] = value
return response
def gzip_response(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not config.ENABLE_COMPRESSION:
return f(*args, **kwargs)
response = make_response(f(*args, **kwargs))
accept_encoding = request.headers.get('Accept-Encoding', '')
if 'gzip' not in accept_encoding.lower():
return response
if response.status_code < 200 or response.status_code >= 300:
return response
if response.headers.get('Content-Encoding'):
return response
gzip_buffer = gzip.compress(response.get_data())
response.set_data(gzip_buffer)
response.headers['Content-Encoding'] = 'gzip'
response.headers['Content-Length'] = len(gzip_buffer)
response.headers['Vary'] = 'Accept-Encoding'
return response
return decorated_function
def etag_support(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not config.ENABLE_ETAG:
return f(*args, **kwargs)
response = make_response(f(*args, **kwargs))
content = response.get_data()
etag = hashlib.md5(content).hexdigest()
response.headers['ETag'] = f'"{etag}"'
response.headers['Cache-Control'] = f'public, max-age={config.CACHE_HOURS * 3600}'
if_none_match = request.headers.get('If-None-Match')
if if_none_match and if_none_match.strip('"') == etag:
return make_response('', 304)
return response
return decorated_function
@app.route('/')
@gzip_response
@etag_support
def index():
return render_template('index.html', vendors=config.VENDORS)
@app.route('/health')
def health():
try:
with cve_handler.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) as count FROM cve_cache")
cve_count = cursor.fetchone()['count']
cursor.execute("SELECT COUNT(*) as count FROM cve_metadata")
vendors_count = cursor.fetchone()['count']
health_data = {
'status': 'healthy',
'timestamp': datetime.utcnow().isoformat(),
'version': config.APP_VERSION,
'database': {
'status': 'connected',
'cve_count': cve_count,
'vendors_tracked': vendors_count
},
'features': {
'auto_update': config.ENABLE_AUTO_UPDATE,
'discord_bot': config.DISCORD_BOT_ENABLED,
'charts': config.ENABLE_CHARTS,
'search': config.ENABLE_SEARCH,
'export': config.ENABLE_EXPORT
}
}
return jsonify(health_data), 200
except Exception as e:
logger.error(f"Health check failed: {e}")
return jsonify({
'status': 'unhealthy',
'timestamp': datetime.utcnow().isoformat(),
'error': str(e)
}), 503
@app.route('/favicon.ico')
def favicon():
return '', 204
@app.route('/version')
def version():
return jsonify({
'app_name': config.APP_NAME,
'version': config.APP_VERSION,
'python_version': '3.14',
'flask_version': '3.0.2'
})
@app.errorhandler(404)
def not_found(error):
if request.path.startswith('/api/'):
return jsonify({'error': 'Endpoint not found'}), 404
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal error: {error}")
if request.path.startswith('/api/'):
return jsonify({'error': 'Internal server error'}), 500
return render_template('500.html'), 500
@app.after_request
def after_request(response):
response = add_security_headers(response)
if config.DEBUG and logger.isEnabledFor(logging.DEBUG):
logger.debug(
f"{request.method} {request.path} - "
f"{response.status_code} - {request.remote_addr}"
)
return response
def background_update_task():
logger.info("Background update task started")
time.sleep(60)
while True:
try:
if config.ENABLE_AUTO_UPDATE:
logger.info("Running scheduled CVE update...")
update_all_vendors()
logger.info("Scheduled update completed successfully")
else:
logger.debug("Auto-update disabled, skipping")
except Exception as e:
logger.error(f"Error in background update: {e}", exc_info=True)
sleep_seconds = config.UPDATE_INTERVAL_HOURS * 3600
logger.info(f"Next update in {config.UPDATE_INTERVAL_HOURS} hours")
time.sleep(sleep_seconds)
def start_background_tasks():
if config.ENABLE_AUTO_UPDATE:
update_thread = threading.Thread(
target=background_update_task,
daemon=True,
name="CVE-Update-Thread"
)
update_thread.start()
logger.info("Auto-update enabled: background task started")
else:
logger.info("Auto-update disabled")
if config.DISCORD_BOT_ENABLED:
try:
from discord_bot import start_discord_bot
discord_thread = threading.Thread(
target=start_discord_bot,
daemon=True,
name="Discord-Bot-Thread"
)
discord_thread.start()
logger.info("Discord bot started")
except ImportError:
logger.warning("discord.py not installed, Discord bot disabled")
except Exception as e:
logger.error(f"Failed to start Discord bot: {e}", exc_info=True)
def initialize_app():
logger.info(f"{'='*60}")
logger.info(f"{config.APP_NAME} v{config.APP_VERSION}")
logger.info(f"{'='*60}")
logger.info(f"Environment: {'DEBUG' if config.DEBUG else 'PRODUCTION'}")
logger.info(f"Database: {config.DATABASE_PATH}")
logger.info(f"Update interval: {config.UPDATE_INTERVAL_HOURS}h")
logger.info(f"Features:")
logger.info(f" - Security headers: {config.ENABLE_SECURITY_HEADERS}")
logger.info(f" - Compression: {config.ENABLE_COMPRESSION}")
logger.info(f" - Auto-update: {config.ENABLE_AUTO_UPDATE}")
logger.info(f" - Discord bot: {config.DISCORD_BOT_ENABLED}")
logger.info(f" - Charts: {config.ENABLE_CHARTS}")
logger.info(f" - Search: {config.ENABLE_SEARCH}")
logger.info(f" - Export: {config.ENABLE_EXPORT}")
if os.path.exists(config.DATABASE_PATH):
try:
with cve_handler.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) as count FROM cve_cache")
count = cursor.fetchone()['count']
logger.info(f"Database contains {count} CVEs")
except Exception as e:
logger.warning(f"Could not read database: {e}")
else:
logger.info("Database will be created on first update")
logger.info(f"{'='*60}")
start_background_tasks()
_initialized = False
if __name__ == '__main__':
if not _initialized:
initialize_app()
_initialized = True
logger.info(f"Starting Flask development server on {config.HOST}:{config.PORT}")
app.run(
host=config.HOST,
port=config.PORT,
debug=config.DEBUG,
threaded=True,
use_reloader=False
)
else:
if not _initialized:
initialize_app()
_initialized = True

478
config.py Normal file
View File

@@ -0,0 +1,478 @@
import os
from typing import List, Dict
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
def get_bool_env(key: str, default: bool = False) -> bool:
return os.getenv(key, str(default)).lower() in ('true', '1', 'yes')
def get_int_env(key: str, default: int) -> int:
try:
return int(os.getenv(key, str(default)))
except ValueError:
return default
def _is_docker():
return os.path.exists('/.dockerenv') or os.path.exists('/run/.containerenv')
IS_DOCKER = _is_docker()
# ============================================================
# VENDORS CONFIGURATION
# ============================================================
VENDORS: List[Dict[str, any]] = [
{
'code': 'microsoft',
'name': 'Microsoft',
'cpe_vendor': 'microsoft',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'microsoft', 'windows', 'office', 'azure', 'exchange', 'sharepoint',
'ms-', 'msft', 'outlook', 'teams', 'edge', 'internet explorer', 'ie',
'sql server', 'visual studio', 'dotnet', '.net', 'iis', 'hyper-v',
'active directory', 'powershell', 'windows server', 'defender',
'onedrive', 'dynamics', 'skype', 'surface', 'xbox'
],
'icon': 'fa-windows'
},
{
'code': 'apple',
'name': 'Apple',
'cpe_vendor': 'apple',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'apple', 'macos', 'ios', 'ipados', 'safari', 'webkit',
'iphone', 'ipad', 'mac os', 'watchos', 'tvos',
'xcode', 'swift', 'darwin', 'core foundation'
],
'icon': 'fa-apple'
},
{
'code': 'fortinet',
'name': 'Fortinet',
'cpe_vendor': 'fortinet',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'fortinet', 'fortigate', 'fortios', 'fortianalyzer', 'fortimanager',
'fortiweb', 'fortimail', 'fortisandbox', 'forticlient', 'fortiadc',
'fortiap', 'fortiswitch', 'fortiwan', 'fortiddos', 'fortiextender'
],
'icon': 'fa-shield-halved'
},
{
'code': 'cisco',
'name': 'Cisco',
'cpe_vendor': 'cisco',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'cisco', 'ios', 'nx-os', 'asa', 'webex', 'firepower',
'ios xe', 'ios xr', 'nexus', 'catalyst', 'meraki',
'duo', 'umbrella', 'anyconnect', 'jabber', 'telepresence'
],
'icon': 'fa-network-wired'
},
{
'code': 'oracle',
'name': 'Oracle',
'cpe_vendor': 'oracle',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'oracle', 'java', 'jdk', 'jre', 'mysql', 'weblogic', 'solaris',
'virtualbox', 'glassfish', 'peoplesoft',
'siebel', 'fusion', 'coherence', 'primavera'
],
'icon': 'fa-database'
},
{
'code': 'google',
'name': 'Google',
'cpe_vendor': 'google',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'google', 'chrome', 'android', 'chromium', 'chromeos',
'pixel', 'nest', 'workspace', 'cloud platform', 'gcp',
'firebase', 'tensorflow', 'kubernetes', 'golang'
],
'icon': 'fa-google'
},
{
'code': 'linux',
'name': 'Linux Kernel',
'cpe_vendor': 'linux',
'cpe_product': 'linux_kernel',
'use_cpe': True,
'strict_matching': True,
'require_cvss': True,
'keywords': [
'in the linux kernel',
'linux kernel vulnerability'
],
'exclude_keywords': [
'android', 'solaredge', 'solar edge', 'inverter',
'router', 'camera', 'nas device', 'synology', 'qnap',
'netgear', 'tp-link', 'asus router', 'd-link', 'linksys',
'firmware update', 'embedded system', 'iot',
'smart tv', 'television', 'printer'
],
'icon': 'fa-linux'
},
{
'code': 'vmware',
'name': 'VMware',
'cpe_vendor': 'vmware',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'vmware', 'vsphere', 'esxi', 'vcenter', 'workstation',
'fusion', 'horizon', 'nsx', 'vsan', 'vrealize',
'tanzu', 'aria', 'carbon black', 'workspace one'
],
'icon': 'fa-server'
},
{
'code': 'paloalto',
'name': 'Palo Alto Networks',
'cpe_vendor': 'paloaltonetworks',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'palo alto', 'pan-os', 'panorama', 'globalprotect',
'palo alto networks', 'wildfire', 'cortex', 'prisma',
'expedition', 'traps'
],
'icon': 'fa-fire'
},
{
'code': 'mikrotik',
'name': 'MikroTik',
'cpe_vendor': 'mikrotik',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'mikrotik', 'routeros', 'routerboard', 'winbox',
'capsman', 'the dude', 'user manager', 'swos',
'rb', 'crs', 'ccr', 'hap', 'hex', 'groove'
],
'icon': 'fa-router'
},
{
'code': 'proxmox',
'name': 'Proxmox',
'cpe_vendor': 'proxmox',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'proxmox', 'proxmox ve', 'proxmox virtual environment',
'pve', 'proxmox backup server', 'pbs', 'qemu-server',
'pve-manager', 'corosync', 'ceph proxmox'
],
'icon': 'fa-server'
},
{
'code': 'openssl',
'name': 'OpenSSL',
'cpe_vendor': 'openssl',
'cpe_product': 'openssl',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'openssl', 'openssl project', 'libssl', 'libcrypto',
'openssl library', 'tls implementation', 'ssl library'
],
'icon': 'fa-lock'
},
{
'code': 'php',
'name': 'PHP',
'cpe_vendor': 'php',
'cpe_product': 'php',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'php', 'php group', 'php language', 'php interpreter',
'php-fpm', 'php core', 'zend engine', 'php runtime'
],
'icon': 'fa-code'
},
{
'code': 'wordpress',
'name': 'WordPress',
'cpe_vendor': 'wordpress',
'cpe_product': 'wordpress',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'wordpress', 'wordpress core', 'wp-admin', 'wp-content',
'wordpress cms', 'automattic', 'woocommerce core',
'wordpress multisite', 'gutenberg'
],
'exclude_keywords': [
'plugin', 'theme', 'elementor', 'yoast', 'jetpack',
'contact form 7', 'akismet', 'wordfence'
],
'icon': 'fa-wordpress'
},
{
'code': 'f5',
'name': 'F5 Networks',
'cpe_vendor': 'f5',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'f5 networks', 'big-ip', 'bigip', 'f5 big-ip', 'tmos',
'nginx plus', 'f5 nginx', 'traffix', 'nginx controller',
'nginx ingress', 'f5os', 'icontrol', 'asm', 'afm', 'apm'
],
'icon': 'fa-network-wired'
},
{
'code': 'nginx',
'name': 'NGINX (OSS)',
'cpe_vendor': 'f5',
'cpe_product': 'nginx',
'use_cpe': True,
'require_cvss': True,
'keywords': [
'nginx', 'nginx web server', 'nginx http server',
'nginx open source', 'nginx reverse proxy',
'nginx inc', 'nginx software'
],
'exclude_keywords': [
'nginx plus', 'nginx controller', 'f5 nginx'
],
'icon': 'fa-server'
},
]
# ============================================================
# API SOURCES CONFIGURATION
# ============================================================
NVD_API_URL = os.getenv('NVD_API_URL', 'https://services.nvd.nist.gov/rest/json/cves/2.0')
NVD_API_KEY = os.getenv('NVD_API_KEY', '')
NVD_RATE_LIMIT = get_int_env('NVD_RATE_LIMIT', 5)
NVD_TIMEOUT = get_int_env('NVD_TIMEOUT', 30)
GITHUB_API_URL = os.getenv('GITHUB_API_URL', 'https://api.github.com/advisories')
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN', '')
GITHUB_RATE_LIMIT = get_int_env('GITHUB_RATE_LIMIT', 60)
GITHUB_TIMEOUT = get_int_env('GITHUB_TIMEOUT', 15)
CVE_SOURCES = {
'nvd': {
'url': NVD_API_URL,
'rate_limit': NVD_RATE_LIMIT,
'timeout': NVD_TIMEOUT
},
'github': {
'url': GITHUB_API_URL,
'rate_limit': GITHUB_RATE_LIMIT,
'timeout': GITHUB_TIMEOUT
}
}
# ============================================================
# DATABASE CONFIGURATION
# ============================================================
if IS_DOCKER:
DEFAULT_DB_PATH = '/app/cve_db/cve_cache.db'
else:
DEFAULT_DB_PATH = './cve_db/cve_cache.db'
DATABASE_PATH = os.getenv('DATABASE_PATH', DEFAULT_DB_PATH)
DATABASE_WAL_MODE = get_bool_env('DATABASE_WAL_MODE', True)
DATABASE_CACHE_SIZE = get_int_env('DATABASE_CACHE_SIZE', 64000)
# ============================================================
# CACHE SETTINGS
# ============================================================
CACHE_HOURS = get_int_env('CACHE_HOURS', 24)
UPDATE_INTERVAL_HOURS = get_int_env('UPDATE_INTERVAL_HOURS', 6)
INITIAL_LOOKBACK_DAYS = get_int_env('INITIAL_LOOKBACK_DAYS', 365)
# ============================================================
# APPLICATION SETTINGS
# ============================================================
DEBUG = get_bool_env('DEBUG', False)
HOST = os.getenv('HOST', '0.0.0.0')
PORT = get_int_env('PORT', 5000)
WORKERS = get_int_env('WORKERS', 4)
WORKER_TIMEOUT = get_int_env('WORKER_TIMEOUT', 120)
APP_NAME = os.getenv('APP_NAME', 'CVE Monitor')
APP_VERSION = os.getenv('APP_VERSION', '1.0.0')
# ============================================================
# SECURITY SETTINGS
# ============================================================
ENABLE_SECURITY_HEADERS = get_bool_env('ENABLE_SECURITY_HEADERS', not IS_DOCKER)
ENABLE_RATE_LIMITING = get_bool_env('ENABLE_RATE_LIMITING', True)
ENABLE_COMPRESSION = get_bool_env('ENABLE_COMPRESSION', True)
ENABLE_ETAG = get_bool_env('ENABLE_ETAG', True)
CSP_DEFAULT_SRC = os.getenv('CSP_DEFAULT_SRC', "'self'")
CSP_SCRIPT_SRC = os.getenv('CSP_SCRIPT_SRC', "'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com")
CSP_STYLE_SRC = os.getenv('CSP_STYLE_SRC', "'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com")
CSP_FONT_SRC = os.getenv('CSP_FONT_SRC', "'self' cdnjs.cloudflare.com")
CSP_IMG_SRC = os.getenv('CSP_IMG_SRC', "'self' data:")
CSP_CONNECT_SRC = os.getenv('CSP_CONNECT_SRC', "'self' cdn.jsdelivr.net")
SECURITY_HEADERS = {
'Content-Security-Policy': f"default-src {CSP_DEFAULT_SRC}; script-src {CSP_SCRIPT_SRC}; style-src {CSP_STYLE_SRC}; font-src {CSP_FONT_SRC}; img-src {CSP_IMG_SRC}; connect-src {CSP_CONNECT_SRC};",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': os.getenv('X_FRAME_OPTIONS', 'DENY'),
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': f"max-age={get_int_env('HSTS_MAX_AGE', 31536000)}; includeSubDomains"
}
# ============================================================
# DISCORD BOT CONFIGURATION
# ============================================================
ENABLE_DISCORD_BOT = get_bool_env('ENABLE_DISCORD_BOT', False)
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN', '')
DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID', '')
DISCORD_CHECK_INTERVAL_MINUTES = get_int_env('DISCORD_CHECK_INTERVAL_MINUTES', 60)
DISCORD_NOTIFY_CRITICAL = get_bool_env('DISCORD_NOTIFY_CRITICAL', True)
DISCORD_NOTIFY_HIGH = get_bool_env('DISCORD_NOTIFY_HIGH', True)
DISCORD_MIN_CVSS = float(os.getenv('DISCORD_MIN_CVSS', '7.0'))
DISCORD_BOT_ENABLED = ENABLE_DISCORD_BOT
DISCORD_UPDATE_INTERVAL = DISCORD_CHECK_INTERVAL_MINUTES * 60 # Convert to seconds
DISCORD_CVE_LOOKBACK_HOURS = DISCORD_CHECK_INTERVAL_MINUTES / 60
DISCORD_MIN_SEVERITY = os.getenv('DISCORD_MIN_SEVERITY', 'HIGH')
# ============================================================
# VALIDATION
# ============================================================
def validate_config():
"""Walidacja krytycznych ustawień"""
errors = []
if ENABLE_DISCORD_BOT:
if not DISCORD_BOT_TOKEN:
errors.append("ENABLE_DISCORD_BOT=True but DISCORD_BOT_TOKEN is empty")
if not DISCORD_CHANNEL_ID:
errors.append("ENABLE_DISCORD_BOT=True but DISCORD_CHANNEL_ID is empty")
try:
int(DISCORD_CHANNEL_ID)
except ValueError:
errors.append("DISCORD_CHANNEL_ID must be a valid integer")
if CACHE_HOURS < 1:
errors.append("CACHE_HOURS must be at least 1")
if UPDATE_INTERVAL_HOURS < 1:
errors.append("UPDATE_INTERVAL_HOURS must be at least 1")
if WORKERS < 1:
errors.append("WORKERS must be at least 1")
if errors:
raise ValueError("Configuration errors:\n" + "\n".join(errors))
# Run validation only if Discord bot enabled
if ENABLE_DISCORD_BOT:
validate_config()
# ============================================================
# PAGINATION SETTINGS
# ============================================================
ITEMS_PER_PAGE = get_int_env('ITEMS_PER_PAGE', 50)
MAX_ITEMS_PER_PAGE = get_int_env('MAX_ITEMS_PER_PAGE', 200)
# ============================================================
# EXPORT SETTINGS
# ============================================================
EXPORT_FORMATS = os.getenv('EXPORT_FORMATS', 'json,csv').split(',')
EXPORT_MAX_ITEMS = get_int_env('EXPORT_MAX_ITEMS', 1000)
# ============================================================
# LOGGING CONFIGURATION
# ============================================================
LOG_LEVEL = os.getenv('LOG_LEVEL', 'DEBUG' if not IS_DOCKER else 'INFO')
LOG_FORMAT = os.getenv('LOG_FORMAT', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
LOG_FILE = os.getenv('LOG_FILE', '')
LOG_MAX_BYTES = get_int_env('LOG_MAX_BYTES', 10485760)
LOG_BACKUP_COUNT = get_int_env('LOG_BACKUP_COUNT', 5)
# ============================================================
# SEVERITY CONFIGURATION
# ============================================================
SEVERITY_LEVELS = {
'CRITICAL': {
'color': 'danger',
'icon': 'fa-skull-crossbones',
'min_cvss': 9.0,
'badge_class': 'badge-critical'
},
'HIGH': {
'color': 'warning',
'icon': 'fa-exclamation-triangle',
'min_cvss': 7.0,
'badge_class': 'badge-high'
},
'MEDIUM': {
'color': 'info',
'icon': 'fa-exclamation-circle',
'min_cvss': 4.0,
'badge_class': 'badge-medium'
},
'LOW': {
'color': 'secondary',
'icon': 'fa-info-circle',
'min_cvss': 0.0,
'badge_class': 'badge-low'
}
}
# ============================================================
# CDN CONFIGURATION
# ============================================================
BOOTSTRAP_CSS_CDN = os.getenv('BOOTSTRAP_CSS_CDN', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css')
BOOTSTRAP_JS_CDN = os.getenv('BOOTSTRAP_JS_CDN', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js')
FONTAWESOME_CDN = os.getenv('FONTAWESOME_CDN', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css')
CHARTJS_CDN = os.getenv('CHARTJS_CDN', 'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js')
# ============================================================
# FEATURE FLAGS
# ============================================================
ENABLE_AUTO_UPDATE = get_bool_env('ENABLE_AUTO_UPDATE', True)
ENABLE_CHARTS = get_bool_env('ENABLE_CHARTS', True)
ENABLE_SEARCH = get_bool_env('ENABLE_SEARCH', True)
ENABLE_EXPORT = get_bool_env('ENABLE_EXPORT', True)
ENABLE_DARK_MODE = get_bool_env('ENABLE_DARK_MODE', True)
# ============================================================
# VALIDATION
# ============================================================
def validate_config():
errors = []
if DISCORD_BOT_ENABLED:
if not DISCORD_BOT_TOKEN:
errors.append("DISCORD_BOT_ENABLED=True but DISCORD_BOT_TOKEN is empty")
if not DISCORD_CHANNEL_ID:
errors.append("DISCORD_BOT_ENABLED=True but DISCORD_CHANNEL_ID is empty")
if CACHE_HOURS < 1:
errors.append("CACHE_HOURS must be at least 1")
if UPDATE_INTERVAL_HOURS < 1:
errors.append("UPDATE_INTERVAL_HOURS must be at least 1")
if WORKERS < 1:
errors.append("WORKERS must be at least 1")
if errors:
raise ValueError("Configuration errors:\n" + "\n".join(errors))
# Run validation
validate_config()

788
cve_handler.py Normal file
View File

@@ -0,0 +1,788 @@
import sqlite3
import json
import logging
import time
import os
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from contextlib import contextmanager
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import config
logging.basicConfig(level=config.LOG_LEVEL, format=config.LOG_FORMAT)
logger = logging.getLogger(__name__)
class CVEHandler:
def __init__(self, db_path: str = None):
self.db_path = db_path or config.DATABASE_PATH
db_dir = os.path.dirname(self.db_path)
if db_dir and not os.path.exists(db_dir):
os.makedirs(db_dir, exist_ok=True)
logger.info(f"Created database directory: {db_dir}")
self._init_database()
self.session = self._create_session()
def _create_session(self) -> requests.Session:
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
@contextmanager
def get_db_connection(self):
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
if config.DATABASE_WAL_MODE:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
conn.execute(f"PRAGMA cache_size=-{config.DATABASE_CACHE_SIZE}")
try:
yield conn
conn.commit()
except Exception as e:
conn.rollback()
logger.error(f"Database error: {e}")
raise
finally:
conn.close()
def _init_database(self):
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS cve_cache (
cve_id TEXT PRIMARY KEY,
vendor_code TEXT NOT NULL,
description TEXT,
published_date TEXT,
last_modified TEXT,
cvss_score REAL,
cvss_vector TEXT,
severity TEXT,
refs TEXT,
cwe_ids TEXT,
affected_products TEXT,
raw_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS cve_metadata (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vendor_code TEXT UNIQUE NOT NULL,
last_update TIMESTAMP,
total_cve_count INTEGER DEFAULT 0,
last_cve_id TEXT,
update_status TEXT,
error_message TEXT
)
""")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_vendor_code ON cve_cache(vendor_code)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_severity ON cve_cache(severity)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_published_date ON cve_cache(published_date DESC)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_cvss_score ON cve_cache(cvss_score DESC)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_updated_at ON cve_cache(updated_at DESC)")
logger.info(f"Database initialized at {self.db_path}")
def fetch_cve_from_nvd(self, keywords: List[str], vendor_config: Dict = None, days_back: int = None) -> List[Dict]:
if days_back is None:
days_back = config.INITIAL_LOOKBACK_DAYS
if not vendor_config:
logger.error("vendor_config is required!")
return []
cpe_vendor = vendor_config.get('cpe_vendor')
cpe_product = vendor_config.get('cpe_product')
if not cpe_vendor:
logger.error(f"cpe_vendor is required in vendor_config!")
return []
require_cvss = vendor_config.get('require_cvss', True)
min_cvss = vendor_config.get('min_cvss')
max_days_per_chunk = 120
all_results = []
unique_cve_ids = set()
num_chunks = (days_back // max_days_per_chunk) + (1 if days_back % max_days_per_chunk else 0)
logger.info(f"Fetching CVEs from NVD")
logger.info(f" CPE filter: vendor='{cpe_vendor}'")
if cpe_product:
logger.info(f" product='{cpe_product}'")
if require_cvss:
logger.info(f" CVSS required: yes")
if min_cvss:
logger.info(f" Minimum CVSS: {min_cvss}")
logger.info(f" Status filter: Analyzed/Modified only")
logger.info(f" Platform filter: Ignore OS-only CPE entries")
logger.info(f" Date range: {days_back} days in {num_chunks} chunks")
for chunk_idx in range(num_chunks):
chunk_end = datetime.utcnow() - timedelta(days=chunk_idx * max_days_per_chunk)
days_in_chunk = min(max_days_per_chunk, days_back - (chunk_idx * max_days_per_chunk))
chunk_start = chunk_end - timedelta(days=days_in_chunk)
start_str = chunk_start.strftime('%Y-%m-%dT%H:%M:%S.000')
end_str = chunk_end.strftime('%Y-%m-%dT%H:%M:%S.000')
logger.info(f"Chunk {chunk_idx+1}/{num_chunks}: {start_str[:10]} to {end_str[:10]}")
params = {
'pubStartDate': start_str,
'pubEndDate': end_str,
'resultsPerPage': 2000,
'startIndex': 0
}
headers = {}
if config.NVD_API_KEY:
headers['apiKey'] = config.NVD_API_KEY
page = 0
while True:
try:
params['startIndex'] = page * 2000
response = self.session.get(
config.CVE_SOURCES['nvd']['url'],
params=params,
headers=headers,
timeout=config.CVE_SOURCES['nvd']['timeout']
)
if response.status_code != 200:
if page == 0:
logger.warning(f"Chunk {chunk_idx+1} returned {response.status_code}")
break
data = response.json()
vulnerabilities = data.get('vulnerabilities', [])
total_results = data.get('totalResults', 0)
results_per_page = data.get('resultsPerPage', 2000)
if page == 0:
logger.info(f" Total CVEs in chunk: {total_results}")
if not vulnerabilities:
break
matched_count = 0
for vuln in vulnerabilities:
cve_data = vuln.get('cve', {})
cve_id = cve_data.get('id', '')
if cve_id in unique_cve_ids:
continue
vuln_status = cve_data.get('vulnStatus', '')
if vuln_status in ['Rejected', 'Awaiting Analysis', 'Undergoing Analysis']:
continue
cpe_match_found = False
configurations = cve_data.get('configurations', [])
if not configurations:
continue
for config_item in configurations:
for node in config_item.get('nodes', []):
for cpe_match in node.get('cpeMatch', []):
cpe_uri = cpe_match.get('criteria', '').lower()
# Format: cpe:2.3:part:vendor:product:version:update:edition:...
cpe_parts = cpe_uri.split(':')
if len(cpe_parts) >= 6:
cpe_part_type = cpe_parts[2]
cpe_vendor_part = cpe_parts[3]
cpe_product_part = cpe_parts[4]
cpe_version_part = cpe_parts[5]
vendor_match = (cpe_vendor_part == cpe_vendor.lower())
if vendor_match:
is_platform_only = False
if cpe_part_type == 'o':
if cpe_version_part in ['-', '*', 'any']:
is_platform_only = True
platform_products = [
'windows', 'windows_server', 'windows_10', 'windows_11',
'macos', 'mac_os_x', 'ios', 'ipados', 'tvos', 'watchos',
'linux', 'linux_kernel',
'android', 'chrome_os',
'ubuntu', 'debian', 'centos', 'rhel', 'fedora',
'freebsd', 'netbsd', 'openbsd'
]
if cpe_product_part in platform_products:
if cpe_version_part in ['-', '*', 'any']:
is_platform_only = True
if is_platform_only:
continue
if cpe_product:
product_match = (cpe_product_part == cpe_product.lower())
if product_match:
cpe_match_found = True
break
else:
cpe_match_found = True
break
if cpe_match_found:
break
if cpe_match_found:
break
if not cpe_match_found:
continue
cvss_score = None
cvss_vector = None
severity = 'UNKNOWN'
metrics = cve_data.get('metrics', {})
for version, key in [('4.0', 'cvssMetricV40'),
('3.1', 'cvssMetricV31'),
('3.0', 'cvssMetricV30'),
('2.0', 'cvssMetricV2')]:
if key in metrics and metrics[key]:
cvss_data = metrics[key][0].get('cvssData', {})
cvss_score = cvss_data.get('baseScore')
cvss_vector = cvss_data.get('vectorString')
severity = cvss_data.get('baseSeverity', 'UNKNOWN')
if (not severity or severity == 'UNKNOWN') and cvss_score:
if cvss_score >= 9.0:
severity = 'CRITICAL'
elif cvss_score >= 7.0:
severity = 'HIGH'
elif cvss_score >= 4.0:
severity = 'MEDIUM'
elif cvss_score > 0:
severity = 'LOW'
break
if require_cvss and not cvss_score:
continue
if min_cvss and cvss_score and cvss_score < min_cvss:
continue
matched_count += 1
unique_cve_ids.add(cve_id)
descriptions = cve_data.get('descriptions', [])
description_text = ' '.join([desc.get('value', '') for desc in descriptions])
refs = [ref.get('url', '') for ref in cve_data.get('references', [])]
cwe_ids = []
for weakness in cve_data.get('weaknesses', []):
for desc in weakness.get('description', []):
value = desc.get('value', '')
if value.startswith('CWE-'):
cwe_ids.append(value)
all_results.append({
'cve_id': cve_id,
'description': description_text[:2000],
'published_date': cve_data.get('published', ''),
'last_modified': cve_data.get('lastModified', ''),
'cvss_score': cvss_score,
'cvss_vector': cvss_vector,
'severity': severity,
'references': json.dumps(refs),
'cwe_ids': json.dumps(cwe_ids),
'raw_data': json.dumps(cve_data)
})
if matched_count > 0:
logger.info(f" Page {page+1}: Matched {matched_count} CVEs")
if len(vulnerabilities) < results_per_page or (page + 1) * results_per_page >= total_results:
break
page += 1
if not config.NVD_API_KEY:
time.sleep(6)
else:
time.sleep(0.6)
except Exception as e:
logger.error(f"Error in chunk {chunk_idx+1} page {page+1}: {e}", exc_info=True)
break
if chunk_idx < num_chunks - 1:
delay = 6 if not config.NVD_API_KEY else 1
time.sleep(delay)
logger.info(f"✓ Total unique CVEs matched: {len(all_results)} (CPE + CVSS + Status + Platform filters)")
return all_results
def fetch_cve_from_github(self, keywords: List[str]) -> List[Dict]:
results = []
unique_cve_ids = set()
headers = {}
if config.GITHUB_TOKEN:
headers['Authorization'] = f'token {config.GITHUB_TOKEN}'
logger.info(f"Fetching advisories from GitHub for keywords: {keywords[:5]}")
page = 1
max_pages = 10
while page <= max_pages:
params = {
'per_page': 100,
'page': page,
'sort': 'published',
'direction': 'desc'
}
try:
response = self.session.get(
config.CVE_SOURCES['github']['url'],
params=params,
headers=headers,
timeout=config.CVE_SOURCES['github']['timeout']
)
response.raise_for_status()
advisories = response.json()
if not advisories:
logger.info(f"GitHub page {page}: No more results")
break
logger.info(f"GitHub page {page}: Got {len(advisories)} advisories")
matched_count = 0
for advisory in advisories:
summary = advisory.get('summary', '')
description = advisory.get('description', '')
full_text = (summary + ' ' + description).lower()
if not any(keyword.lower() in full_text for keyword in keywords):
continue
cve_id = None
for identifier in advisory.get('identifiers', []):
if identifier.get('type') == 'CVE':
cve_id = identifier.get('value')
break
if not cve_id:
cve_id = advisory.get('ghsa_id', f"GHSA-{advisory.get('id', 'unknown')}")
if cve_id in unique_cve_ids:
continue
unique_cve_ids.add(cve_id)
matched_count += 1
cvss_data = advisory.get('cvss', {})
cvss_score = cvss_data.get('score')
cvss_vector = cvss_data.get('vector_string')
severity = advisory.get('severity', 'UNKNOWN').upper()
cwe_list = [cwe.get('cwe_id', '') for cwe in advisory.get('cwes', [])]
results.append({
'cve_id': cve_id,
'description': (summary + ' ' + description)[:1000],
'published_date': advisory.get('published_at', ''),
'last_modified': advisory.get('updated_at', ''),
'cvss_score': cvss_score,
'cvss_vector': cvss_vector,
'severity': severity,
'references': json.dumps([advisory.get('html_url', '')]),
'cwe_ids': json.dumps(cwe_list),
'raw_data': json.dumps(advisory)
})
logger.info(f"GitHub page {page}: Matched {matched_count} advisories")
page += 1
time.sleep(1)
except Exception as e:
logger.error(f"Error fetching GitHub page {page}: {e}", exc_info=True)
break
logger.info(f"✓ Total unique CVEs from GitHub: {len(results)}")
return results
def update_vendor_cache(self, vendor_code: str, force: bool = False) -> bool:
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
if not vendor:
logger.error(f"Unknown vendor code: {vendor_code}")
logger.error(f"Available vendors: {[v['code'] for v in config.VENDORS]}")
return False
if not force:
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT last_update FROM cve_metadata WHERE vendor_code = ?",
(vendor_code,)
)
row = cursor.fetchone()
if row and row['last_update']:
last_update = datetime.fromisoformat(row['last_update'])
time_since_update = datetime.utcnow() - last_update
cache_valid_hours = config.UPDATE_INTERVAL_HOURS
if time_since_update < timedelta(hours=cache_valid_hours):
logger.info(f"Cache for {vendor_code} is fresh (updated {time_since_update.seconds//3600}h ago)")
return True
logger.info(f"Updating cache for vendor: {vendor['name']} ({vendor_code})")
if vendor.get('cpe_vendor'):
logger.info(f"CPE vendor: {vendor['cpe_vendor']}")
if vendor.get('cpe_product'):
logger.info(f"CPE product: {vendor['cpe_product']}")
else:
logger.error(f"No CPE vendor configured for {vendor_code}!")
return False
try:
all_cves = []
days_back = config.INITIAL_LOOKBACK_DAYS if force else config.UPDATE_LOOKBACK_DAYS
nvd_cves = self.fetch_cve_from_nvd(
vendor['keywords'],
vendor_config=vendor,
days_back=days_back
)
all_cves.extend(nvd_cves)
logger.info(f"Collected {len(nvd_cves)} CVEs from NVD")
if config.CVE_SOURCES.get('github', {}).get('enabled', False):
github_cves = self.fetch_cve_from_github(vendor['keywords'])
all_cves.extend(github_cves)
logger.info(f"Collected {len(github_cves)} CVEs from GitHub")
unique_cves = {cve['cve_id']: cve for cve in all_cves}
logger.info(f"Total unique CVEs after deduplication: {len(unique_cves)}")
with self.get_db_connection() as conn:
cursor = conn.cursor()
for cve_id, cve in unique_cves.items():
cursor.execute("""
INSERT OR REPLACE INTO cve_cache
(cve_id, vendor_code, description, published_date, last_modified,
cvss_score, cvss_vector, severity, refs, cwe_ids,
affected_products, raw_data, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (
cve_id,
vendor_code,
cve.get('description'),
cve.get('published_date'),
cve.get('last_modified'),
cve.get('cvss_score'),
cve.get('cvss_vector'),
cve.get('severity'),
cve.get('references'),
cve.get('cwe_ids'),
None,
cve.get('raw_data')
))
cursor.execute("""
INSERT OR REPLACE INTO cve_metadata
(vendor_code, last_update, total_cve_count, last_cve_id, update_status, error_message)
VALUES (?, CURRENT_TIMESTAMP, ?, ?, 'success', NULL)
""", (
vendor_code,
len(unique_cves),
list(unique_cves.keys())[0] if unique_cves else None
))
logger.info(f"✓ Successfully updated {len(unique_cves)} CVEs for {vendor['name']}")
return True
except Exception as e:
logger.error(f"✗ Error updating vendor cache for {vendor_code}: {e}", exc_info=True)
try:
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT OR REPLACE INTO cve_metadata
(vendor_code, last_update, update_status, error_message)
VALUES (?, CURRENT_TIMESTAMP, 'failed', ?)
""", (vendor_code, str(e)[:500]))
except:
pass
return False
def get_vendor_cves(self, vendor_code: str, limit: int = None, offset: int = 0,
severity: str = None, year: int = None) -> List[Dict]:
with self.get_db_connection() as conn:
cursor = conn.cursor()
query = """
SELECT cve_id, vendor_code, description, published_date, last_modified,
cvss_score, cvss_vector, severity, refs, cwe_ids, updated_at
FROM cve_cache
WHERE vendor_code = ?
"""
params = [vendor_code]
if severity:
query += " AND severity = ?"
params.append(severity.upper())
if year:
query += " AND strftime('%Y', published_date) = ?"
params.append(str(year))
query += " ORDER BY published_date DESC"
if limit:
query += f" LIMIT {limit} OFFSET {offset}"
cursor.execute(query, params)
results = []
for row in cursor.fetchall():
row_dict = dict(row)
if 'refs' in row_dict:
row_dict['references'] = row_dict.pop('refs')
results.append(row_dict)
return results
def get_vendor_stats(self, vendor_code: str) -> Dict:
with self.get_db_connection() as conn:
cursor = conn.cursor()
# Total count
cursor.execute(
"SELECT COUNT(*) as total FROM cve_cache WHERE vendor_code = ?",
(vendor_code,)
)
total = cursor.fetchone()['total']
# Severity distribution
cursor.execute("""
SELECT severity, COUNT(*) as count
FROM cve_cache
WHERE vendor_code = ?
GROUP BY severity
""", (vendor_code,))
severity_counts = {row['severity']: row['count'] for row in cursor.fetchall()}
# Monthly trend (last 12 months)
cursor.execute("""
SELECT strftime('%Y-%m', published_date) as month, COUNT(*) as count
FROM cve_cache
WHERE vendor_code = ? AND published_date >= date('now', '-12 months')
GROUP BY month
ORDER BY month
""", (vendor_code,))
monthly_counts = {row['month']: row['count'] for row in cursor.fetchall()}
# This month
cursor.execute("""
SELECT COUNT(*) as count
FROM cve_cache
WHERE vendor_code = ? AND strftime('%Y-%m', published_date) = strftime('%Y-%m', 'now')
""", (vendor_code,))
this_month = cursor.fetchone()['count']
# This year
cursor.execute("""
SELECT COUNT(*) as count
FROM cve_cache
WHERE vendor_code = ? AND strftime('%Y', published_date) = strftime('%Y', 'now')
""", (vendor_code,))
this_year = cursor.fetchone()['count']
# Recent (last 7 days)
cursor.execute("""
SELECT COUNT(*) as count
FROM cve_cache
WHERE vendor_code = ? AND published_date >= date('now', '-7 days')
""", (vendor_code,))
recent = cursor.fetchone()['count']
return {
'total': total,
'severity': severity_counts,
'monthly': monthly_counts,
'this_month': this_month,
'this_year': this_year,
'recent': recent
}
def search_cves(self, query: str, limit: int = 50) -> List[Dict]:
with self.get_db_connection() as conn:
cursor = conn.cursor()
search_query = f"%{query}%"
cursor.execute("""
SELECT cve_id, vendor_code, description, published_date, cvss_score, severity
FROM cve_cache
WHERE cve_id LIKE ? OR description LIKE ?
ORDER BY published_date DESC
LIMIT ?
""", (search_query, search_query, limit))
return [dict(row) for row in cursor.fetchall()]
def get_all_vendors_summary(self) -> List[Dict]:
summary = []
with self.get_db_connection() as conn:
cursor = conn.cursor()
for vendor in config.VENDORS:
cursor.execute("""
SELECT
COUNT(*) as total,
SUM(CASE WHEN severity = 'CRITICAL' THEN 1 ELSE 0 END) as critical,
SUM(CASE WHEN severity = 'HIGH' THEN 1 ELSE 0 END) as high,
SUM(CASE WHEN published_date >= date('now', '-7 days') THEN 1 ELSE 0 END) as recent
FROM cve_cache
WHERE vendor_code = ?
""", (vendor['code'],))
stats = cursor.fetchone()
cursor.execute(
"SELECT last_update FROM cve_metadata WHERE vendor_code = ?",
(vendor['code'],)
)
metadata = cursor.fetchone()
summary.append({
'code': vendor['code'],
'name': vendor['name'],
'icon': vendor.get('icon', 'fa-shield-alt'),
'total': stats['total'] or 0,
'critical': stats['critical'] or 0,
'high': stats['high'] or 0,
'recent': stats['recent'] or 0,
'last_update': metadata['last_update'] if metadata else None
})
return summary
def get_recent_cves_for_discord(self, hours: int = 1) -> List[Dict]:
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT cve_id, vendor_code, description, published_date, cvss_score, severity
FROM cve_cache
WHERE updated_at >= datetime('now', ? || ' hours')
AND severity IN ('CRITICAL', 'HIGH')
AND cvss_score >= ?
ORDER BY published_date DESC
LIMIT 10
""", (f'-{hours}', config.DISCORD_MIN_CVSS))
return [dict(row) for row in cursor.fetchall()]
def update_all_vendors(force: bool = True):
handler = CVEHandler()
updated = 0
failed = 0
total_vendors = len(config.VENDORS)
logger.info(f"{'='*60}")
logger.info(f"Starting {'FULL' if force else 'incremental'} update for {total_vendors} vendors")
logger.info(f"Lookback period: {config.INITIAL_LOOKBACK_DAYS} days")
logger.info(f"{'='*60}")
start_time = time.time()
for idx, vendor in enumerate(config.VENDORS, 1):
vendor_start = time.time()
logger.info(f"\n[{idx}/{total_vendors}] {'='*50}")
logger.info(f"Updating {vendor['name']} ({vendor['code']})...")
logger.info(f"Keywords: {vendor['keywords'][:5]}...")
try:
if handler.update_vendor_cache(vendor['code'], force=force):
updated += 1
stats = handler.get_vendor_stats(vendor['code'])
logger.info(f"{vendor['name']} updated successfully")
logger.info(f" Total CVEs: {stats['total']}")
logger.info(f" Critical: {stats['severity'].get('CRITICAL', 0)}")
logger.info(f" High: {stats['severity'].get('HIGH', 0)}")
logger.info(f" Medium: {stats['severity'].get('MEDIUM', 0)}")
logger.info(f" Low: {stats['severity'].get('LOW', 0)}")
else:
failed += 1
logger.error(f"{vendor['name']} update failed")
vendor_time = time.time() - vendor_start
logger.info(f" Time taken: {vendor_time:.1f}s")
if idx < total_vendors:
if not config.NVD_API_KEY:
delay = 10
logger.info(f"Waiting {delay}s before next vendor (no API key)...")
else:
delay = 2
logger.debug(f"Waiting {delay}s before next vendor...")
time.sleep(delay)
except KeyboardInterrupt:
logger.warning("Update interrupted by user")
break
except Exception as e:
failed += 1
logger.error(f"✗ Exception updating {vendor['name']}: {e}", exc_info=True)
total_time = time.time() - start_time
logger.info(f"\n{'='*60}")
logger.info(f"Update completed in {total_time/60:.1f} minutes")
logger.info(f"Results: {updated} successful, {failed} failed")
logger.info(f"{'='*60}")
logger.info("\nFinal summary:")
summary = handler.get_all_vendors_summary()
for v in summary:
logger.info(f" {v['name']:25s} - Total: {v['total']:5d} | Critical: {v['critical']:4d} | High: {v['high']:4d}")
return updated, failed
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
handler = CVEHandler()
print(f"Database: {config.DATABASE_PATH}")
print(f"Available vendors: {[v['code'] for v in config.VENDORS]}")

218
discord_bot.py Normal file
View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
import discord
from discord.ext import tasks
import logging
import asyncio
from datetime import datetime
import sys
import config
from cve_handler import CVEHandler
logging.basicConfig(level=config.LOG_LEVEL, format=config.LOG_FORMAT)
logger = logging.getLogger(__name__)
class CVEDiscordBot(discord.Client):
def __init__(self, *args, **kwargs):
intents = discord.Intents.default()
intents.message_content = True
super().__init__(intents=intents, *args, **kwargs)
self.cve_handler = CVEHandler()
self.channel_id = None
self.check_interval = config.DISCORD_CHECK_INTERVAL_MINUTES
if not config.DISCORD_BOT_TOKEN:
logger.error("DISCORD_BOT_TOKEN not configured in .env")
sys.exit(1)
if not config.DISCORD_CHANNEL_ID:
logger.error("DISCORD_CHANNEL_ID not configured in .env")
sys.exit(1)
try:
self.channel_id = int(config.DISCORD_CHANNEL_ID)
except ValueError:
logger.error(f"Invalid DISCORD_CHANNEL_ID: {config.DISCORD_CHANNEL_ID}")
sys.exit(1)
async def on_ready(self):
logger.info(f'Discord bot logged in as {self.user}')
logger.info(f'Bot ID: {self.user.id}')
logger.info(f'Monitoring channel ID: {self.channel_id}')
logger.info(f'Check interval: {self.check_interval} minutes')
logger.info(f'Min CVSS score: {config.DISCORD_MIN_CVSS}')
channel = self.get_channel(self.channel_id)
if not channel:
logger.error(f"Cannot access channel {self.channel_id}")
logger.error("Make sure the bot has been invited to the server and has permissions")
await self.close()
return
logger.info(f"Successfully connected to channel: #{channel.name}")
if not self.check_new_cves.is_running():
self.check_new_cves.start()
logger.info("CVE monitoring task started")
async def on_error(self, event, *args, **kwargs):
logger.error(f"Discord error in event {event}", exc_info=True)
@tasks.loop(minutes=config.DISCORD_CHECK_INTERVAL_MINUTES)
async def check_new_cves(self):
try:
logger.info("Checking for new CVEs...")
channel = self.get_channel(self.channel_id)
if not channel:
logger.error(f"Channel {self.channel_id} not found")
return
hours_back = self.check_interval / 60
new_cves = self.cve_handler.get_recent_cves_for_discord(hours=hours_back)
if not new_cves:
logger.info("No new CVEs found")
return
filtered_cves = [
cve for cve in new_cves
if cve.get('cvss_score', 0) >= config.DISCORD_MIN_CVSS
]
if not filtered_cves:
logger.info(f"Found {len(new_cves)} CVEs but none meet min CVSS threshold of {config.DISCORD_MIN_CVSS}")
return
logger.info(f"Found {len(filtered_cves)} CVEs meeting criteria (out of {len(new_cves)} total)")
for cve in filtered_cves:
try:
embed = self.create_cve_embed(cve)
await channel.send(embed=embed)
logger.info(f"Sent notification for {cve['cve_id']}")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error sending notification for {cve['cve_id']}: {e}")
logger.info(f"Successfully sent {len(filtered_cves)} CVE notifications")
except Exception as e:
logger.error(f"Error in check_new_cves: {e}", exc_info=True)
@check_new_cves.before_loop
async def before_check_new_cves(self):
await self.wait_until_ready()
logger.info("Bot is ready, starting CVE monitoring...")
def create_cve_embed(self, cve: dict) -> discord.Embed:
# Severity colors
severity_colors = {
'CRITICAL': 0xDC3545, # Red
'HIGH': 0xFD7E14, # Orange
'MEDIUM': 0xFFC107, # Yellow
'LOW': 0x6C757D # Gray
}
severity = cve.get('severity', 'UNKNOWN')
color = severity_colors.get(severity, 0x6C757D)
# Get vendor info
vendor_code = cve.get('vendor_code', '')
vendor = next((v for v in config.VENDORS if v['code'] == vendor_code), None)
vendor_name = vendor['name'] if vendor else vendor_code.title()
# Create embed
embed = discord.Embed(
title=f"🚨 {cve['cve_id']}",
description=cve.get('description', 'No description available')[:2000],
color=color,
timestamp=datetime.utcnow()
)
# Add fields
embed.add_field(
name="🏢 Vendor",
value=vendor_name,
inline=True
)
embed.add_field(
name="⚠️ Severity",
value=severity,
inline=True
)
cvss_score = cve.get('cvss_score')
embed.add_field(
name="📊 CVSS Score",
value=f"**{cvss_score:.1f}**" if cvss_score else "N/A",
inline=True
)
published = cve.get('published_date', '')
if published:
try:
pub_date = datetime.fromisoformat(published.replace('Z', '+00:00'))
embed.add_field(
name="📅 Published",
value=pub_date.strftime('%Y-%m-%d %H:%M UTC'),
inline=True
)
except:
embed.add_field(name="📅 Published", value=published[:10], inline=True)
nvd_url = f"https://nvd.nist.gov/vuln/detail/{cve['cve_id']}"
embed.add_field(
name="🔗 Links",
value=f"[View on NVD]({nvd_url})",
inline=False
)
embed.set_footer(
text=f"CVE Monitor • {vendor_name}",
icon_url="https://nvd.nist.gov/favicon.ico"
)
return embed
def start_discord_bot():
if not config.ENABLE_DISCORD_BOT:
logger.info("Discord bot is disabled (ENABLE_DISCORD_BOT=False)")
return
if not config.DISCORD_BOT_TOKEN:
logger.error("DISCORD_BOT_TOKEN not configured")
logger.error("Please set DISCORD_BOT_TOKEN in .env file")
logger.error("Get your token at: https://discord.com/developers/applications")
return
if not config.DISCORD_CHANNEL_ID:
logger.error("DISCORD_CHANNEL_ID not configured")
logger.error("Please set DISCORD_CHANNEL_ID in .env file")
logger.error("Enable Developer Mode in Discord, right-click channel -> Copy ID")
return
logger.info("Starting Discord bot...")
logger.info(f"Configuration:")
logger.info(f" - Check interval: {config.DISCORD_CHECK_INTERVAL_MINUTES} minutes")
logger.info(f" - Min CVSS score: {config.DISCORD_MIN_CVSS}")
logger.info(f" - Notify on CRITICAL: {config.DISCORD_NOTIFY_CRITICAL}")
logger.info(f" - Notify on HIGH: {config.DISCORD_NOTIFY_HIGH}")
bot = CVEDiscordBot()
try:
bot.run(config.DISCORD_BOT_TOKEN)
except discord.LoginFailure:
logger.error("Invalid Discord bot token")
logger.error("Please check DISCORD_BOT_TOKEN in .env file")
except Exception as e:
logger.error(f"Failed to start Discord bot: {e}", exc_info=True)
if __name__ == '__main__':
start_discord_bot()

87
docker-compose.yml Normal file
View File

@@ -0,0 +1,87 @@
services:
cve-monitor:
build: .
container_name: cve-monitor
restart: unless-stopped
ports:
- "${PORT:-5000}:5000"
volumes:
- ./cve_db:/app/cve_db
- ./logs:/app/logs
environment:
- WORKERS=${WORKERS:-4}
- WORKER_TIMEOUT=${WORKER_TIMEOUT:-120}
- PORT=5000
- DATABASE_PATH=/app/cve_db/cve_cache.db
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- NVD_API_KEY=${NVD_API_KEY:-}
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
env_file:
- .env
command: app
networks:
- cve-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
cve-monitor-scheduler:
build: .
container_name: cve-monitor-scheduler
restart: unless-stopped
volumes:
- ./cve_db:/app/cve_db
- ./logs:/app/logs
environment:
- DATABASE_PATH=/app/cve_db/cve_cache.db
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- ENABLE_AUTO_UPDATE=${ENABLE_AUTO_UPDATE:-True}
- UPDATE_INTERVAL_HOURS=${UPDATE_INTERVAL_HOURS:-24}
- INITIAL_LOOKBACK_DAYS=${INITIAL_LOOKBACK_DAYS:-365}
- NVD_API_KEY=${NVD_API_KEY:-}
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
env_file:
- .env
command: scheduler
depends_on:
- cve-monitor
networks:
- cve-network
healthcheck:
test: ["CMD", "test", "-f", "/app/cve_db/cve_cache.db"]
interval: 15s
timeout: 5s
retries: 10
start_period: 60s
cve-monitor-discord:
build: .
container_name: cve-monitor-discord
restart: unless-stopped
volumes:
- ./cve_db:/app/cve_db
- ./logs:/app/logs
environment:
- DATABASE_PATH=/app/cve_db/cve_cache.db
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID}
- DISCORD_CHECK_INTERVAL=${DISCORD_CHECK_INTERVAL:-60}
- DISCORD_MIN_CVSS=${DISCORD_MIN_CVSS:-7.0}
- DISCORD_NOTIFY_CRITICAL=${DISCORD_NOTIFY_CRITICAL:-True}
- DISCORD_NOTIFY_HIGH=${DISCORD_NOTIFY_HIGH:-True}
env_file:
- .env
command: discord
depends_on:
cve-monitor-scheduler:
condition: service_healthy
networks:
- cve-network
networks:
cve-network:
driver: bridge

61
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,61 @@
#!/bin/sh
set -e
mkdir -p /app/cve_db /app/logs
chmod -R 777 /app/cve_db /app/logs
WORKERS=${WORKERS:-4}
WORKER_TIMEOUT=${WORKER_TIMEOUT:-120}
PORT=${PORT:-5000}
echo "======================================"
echo "CVE Monitor Docker Container"
echo "======================================"
case "$1" in
app)
echo "Starting Flask app with Gunicorn..."
echo " Workers: $WORKERS"
echo " Timeout: $WORKER_TIMEOUT"
echo " Port: $PORT"
echo "======================================"
exec gunicorn \
--bind 0.0.0.0:$PORT \
--workers $WORKERS \
--timeout $WORKER_TIMEOUT \
--access-logfile - \
--error-logfile - \
--log-level info \
app:app
;;
scheduler)
echo "Starting CVE Scheduler..."
echo "======================================"
exec python scheduler.py
;;
discord)
echo "Starting Discord Bot..."
echo "======================================"
exec python discord_bot.py
;;
update)
echo "Running manual CVE update..."
echo "======================================"
exec python -c "from cve_handler import update_all_vendors; update_all_vendors(force=True)"
;;
shell)
echo "Starting interactive shell..."
echo "======================================"
exec /bin/sh
;;
*)
echo "Executing custom command: $@"
echo "======================================"
exec "$@"
;;
esac

142
env.example Normal file
View File

@@ -0,0 +1,142 @@
# ============================================================
# CVE MONITOR - CONFIGURATION FILE
# ============================================================
# Copy this file to .env and adjust values for your environment
# ============================================================
# APPLICATION SETTINGS
# ============================================================
APP_NAME=CVE Monitor
APP_VERSION=1.0.0
DEBUG=False
HOST=0.0.0.0
PORT=5000
# ============================================================
# DATABASE CONFIGURATION
# ============================================================
DATABASE_PATH=./cve_db/cve_cache.db
DATABASE_WAL_MODE=True
DATABASE_CACHE_SIZE=10000
# ============================================================
# LOGGING CONFIGURATION
# ============================================================
LOG_LEVEL=INFO
LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s
LOG_FILE=./logs/cve_monitor.log
LOG_MAX_BYTES=10485760
LOG_BACKUP_COUNT=5
# ============================================================
# AUTO-UPDATE CONFIGURATION
# ============================================================
ENABLE_AUTO_UPDATE=True
UPDATE_INTERVAL_HOURS=1
INITIAL_LOOKBACK_DAYS=365
CACHE_HOURS=24
# ============================================================
# EXTERNAL API KEYS (Optional but Recommended)
# ============================================================
# NVD API Key - Get yours at: https://nvd.nist.gov/developers/request-an-api-key
# Without API key: 5 requests per 30 seconds
# With API key: 50 requests per 30 seconds
NVD_API_KEY=
# GitHub Personal Access Token - Get yours at: https://github.com/settings/tokens
# Increases rate limit from 60 to 5000 requests per hour
GITHUB_TOKEN=
# ============================================================
# API ENDPOINTS (Advanced - Don't change unless necessary)
# ============================================================
NVD_API_URL=https://services.nvd.nist.gov/rest/json/cves/2.0
GITHUB_API_URL=https://api.github.com/advisories
NVD_TIMEOUT=30
GITHUB_TIMEOUT=15
# ============================================================
# GUNICORN CONFIGURATION (Production)
# ============================================================
WORKERS=4
WORKER_TIMEOUT=120
# ============================================================
# SECURITY SETTINGS
# ============================================================
ENABLE_SECURITY_HEADERS=True
ENABLE_RATE_LIMITING=True
ENABLE_COMPRESSION=True
ENABLE_ETAG=True
# Content Security Policy
CSP_DEFAULT_SRC='self'
CSP_SCRIPT_SRC='self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com
CSP_STYLE_SRC='self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com
CSP_FONT_SRC='self' cdnjs.cloudflare.com
CSP_IMG_SRC='self' data:
CSP_CONNECT_SRC='self' cdn.jsdelivr.net
# X-Frame-Options: DENY, SAMEORIGIN, or ALLOW-FROM uri
X_FRAME_OPTIONS=DENY
# HSTS max age in seconds (1 year = 31536000)
HSTS_MAX_AGE=31536000
# ============================================================
# FEATURE FLAGS
# ============================================================
ENABLE_CHARTS=True
ENABLE_SEARCH=True
ENABLE_EXPORT=True
ENABLE_DARK_MODE=True
# ============================================================
# UI CONFIGURATION
# ============================================================
ITEMS_PER_PAGE=50
MAX_ITEMS_PER_PAGE=200
# ============================================================
# EXPORT SETTINGS
# ============================================================
EXPORT_FORMATS=json,csv
EXPORT_MAX_ITEMS=1000
# ============================================================
# CDN URLS (for offline use, download and host locally)
# ============================================================
BOOTSTRAP_CSS_CDN=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css
BOOTSTRAP_JS_CDN=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
FONTAWESOME_CDN=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css
CHARTJS_CDN=https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js
# ============================================================
# DISCORD BOT CONFIGURATION
# ============================================================
ENABLE_DISCORD_BOT=False
DISCORD_BOT_TOKEN=
DISCORD_CHANNEL_ID=
DISCORD_CHECK_INTERVAL_MINUTES=60
DISCORD_NOTIFY_CRITICAL=True
DISCORD_NOTIFY_HIGH=True
DISCORD_MIN_CVSS=7.0
DISCORD_MIN_SEVERITY=HIGH
# ============================================================
# MONITORED VENDORS
# ============================================================
# Vendors are configured in config.py VENDORS list, not via environment variables.
# Edit config.py to add/remove/modify vendors.
# ============================================================
# NOTES
# ============================================================
# 1. Boolean values: True/False (case-sensitive)
# 2. Empty values will use defaults from config.py
# 3. Paths can be absolute or relative to project root
# 4. For production, always set DEBUG=False
# 5. Get NVD API key to avoid rate limits
# 6. Use strong CSP in production
# 7. Enable HTTPS in production (handled by reverse proxy)

75
full_scan.py Normal file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import logging
import sys
from datetime import datetime
from cve_handler import update_all_vendors, CVEHandler
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(f'logs/full_scan_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def main():
print("=" * 70)
print("CVE MONITOR - FULL SCAN")
print("=" * 70)
print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print()
try:
updated, failed = update_all_vendors(force=True)
print()
print("=" * 70)
print("SCAN COMPLETED")
print("=" * 70)
print(f"✓ Successfully updated: {updated} vendors")
print(f"✗ Failed: {failed} vendors")
print(f"Finished at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print()
print("=" * 70)
print("DATABASE STATISTICS")
print("=" * 70)
handler = CVEHandler()
summary = handler.get_all_vendors_summary()
total_cves = sum(v['total'] for v in summary)
total_critical = sum(v['critical'] for v in summary)
total_high = sum(v['high'] for v in summary)
print(f"Total CVEs in database: {total_cves}")
print(f" Critical: {total_critical}")
print(f" High: {total_high}")
print()
print("Per vendor breakdown:")
print("-" * 70)
print(f"{'Vendor':<25} {'Total':>8} {'Critical':>10} {'High':>10} {'Recent':>10}")
print("-" * 70)
for v in sorted(summary, key=lambda x: x['total'], reverse=True):
print(f"{v['name']:<25} {v['total']:>8} {v['critical']:>10} {v['high']:>10} {v['recent']:>10}")
print("=" * 70)
return 0 if failed == 0 else 1
except KeyboardInterrupt:
print("\n\n Scan interrupted by user")
return 2
except Exception as e:
logger.error(f"Fatal error during scan: {e}", exc_info=True)
return 3
if __name__ == '__main__':
sys.exit(main())

8
requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
Flask
Werkzeug
requests
urllib3
python-dateutil
gunicorn
discord.py
python-dotenv

40
scheduler.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import time
import logging
from datetime import datetime
from cve_handler import update_all_vendors
import config
logging.basicConfig(
level=config.LOG_LEVEL,
format=config.LOG_FORMAT
)
logger = logging.getLogger(__name__)
def run_scheduler():
logger.info("CVE Update Scheduler started")
logger.info(f"Update interval: {config.UPDATE_INTERVAL_HOURS} hours")
logger.info(f"Auto-update enabled: {config.ENABLE_AUTO_UPDATE}")
time.sleep(120)
while True:
if config.ENABLE_AUTO_UPDATE:
try:
logger.info(f"Starting scheduled update at {datetime.now()}")
updated, failed = update_all_vendors()
logger.info(f"Update completed: {updated} successful, {failed} failed")
except Exception as e:
logger.error(f"Error in scheduled update: {e}", exc_info=True)
else:
logger.debug("Auto-update is disabled, skipping")
sleep_seconds = config.UPDATE_INTERVAL_HOURS * 3600
next_update = datetime.now().timestamp() + sleep_seconds
logger.info(f"Next update scheduled at {datetime.fromtimestamp(next_update)}")
time.sleep(sleep_seconds)
if __name__ == '__main__':
run_scheduler()

264
static/css/style.css Normal file
View File

@@ -0,0 +1,264 @@
/* CVE Monitor - Custom Styles */
:root {
--sidebar-width: 250px;
--navbar-height: 56px;
}
body {
font-size: 0.95rem;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
position: fixed;
top: var(--navbar-height);
bottom: 0;
left: 0;
z-index: 100;
padding: 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
overflow-y: auto;
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
padding: 0.75rem 1rem;
border-left: 3px solid transparent;
transition: all 0.2s;
}
.sidebar .nav-link:hover {
color: #0d6efd;
background-color: rgba(0, 0, 0, 0.05);
}
.sidebar .nav-link.active {
color: #0d6efd;
border-left-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.1);
}
.sidebar .nav-link i {
margin-right: 0.5rem;
width: 20px;
text-align: center;
}
.sidebar-heading {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
}
/* Dark mode */
[data-bs-theme="dark"] .sidebar {
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.1);
}
[data-bs-theme="dark"] .sidebar .nav-link {
color: #dee2e6;
}
[data-bs-theme="dark"] .sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
[data-bs-theme="dark"] .sidebar .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
}
/* Main content */
main {
margin-left: var(--sidebar-width);
padding-top: 1rem;
}
@media (max-width: 767.98px) {
.sidebar {
position: static;
height: auto;
}
main {
margin-left: 0;
}
}
/* Cards */
.card {
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1rem;
}
.card-body {
padding: 1.25rem;
}
/* Stats cards */
.card .fs-1 {
opacity: 0.2;
}
.card:hover .fs-1 {
opacity: 0.3;
transition: opacity 0.3s;
}
/* Table */
.table {
font-size: 0.9rem;
}
.table thead th {
border-bottom: 2px solid #dee2e6;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
color: #6c757d;
}
.table tbody tr {
cursor: pointer;
transition: background-color 0.2s;
}
.table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
[data-bs-theme="dark"] .table tbody tr:hover {
background-color: rgba(255, 255, 255, 0.05);
}
/* Severity badges */
.badge-critical {
background-color: #dc3545;
color: white;
}
.badge-high {
background-color: #fd7e14;
color: white;
}
.badge-medium {
background-color: #0dcaf0;
color: black;
}
.badge-low {
background-color: #6c757d;
color: white;
}
/* CVE ID column */
.cve-id {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #0d6efd;
}
/* Loading spinner overlay */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
[data-bs-theme="dark"] .loading-overlay {
background: rgba(0, 0, 0, 0.8);
}
/* Pagination */
.pagination {
margin-top: 1rem;
}
/* Search results */
#searchResults .list-group-item {
cursor: pointer;
transition: background-color 0.2s;
}
#searchResults .list-group-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Charts */
canvas {
max-height: 300px;
}
/* Responsive */
@media (max-width: 575.98px) {
.btn-toolbar {
flex-direction: column;
align-items: flex-start !important;
}
.btn-group {
margin-bottom: 0.5rem;
}
}
/* Scrollbar customization */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
[data-bs-theme="dark"] ::-webkit-scrollbar-track {
background: #2d2d2d;
}
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb {
background: #666;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
/* Vendor badge counter */
.vendor-badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
border-radius: 10px;
}

425
static/js/app.js Normal file
View File

@@ -0,0 +1,425 @@
const state = {
currentVendor: null,
currentPage: 1,
itemsPerPage: 50,
filters: { severity: '', year: '' },
charts: { trend: null, severity: null }
};
function updateThemeIcon(theme) {
const icon = document.querySelector('#darkModeToggle i');
if (icon) icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
function toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
if (state.currentVendor) {
fetch(`/api/stats/${state.currentVendor}`)
.then(r => r.json())
.then(data => updateCharts(data.stats))
.catch(console.error);
}
}
function getSeverityIcon(severity) {
const icons = {
'CRITICAL': '<i class="fas fa-skull-crossbones"></i>',
'HIGH': '<i class="fas fa-exclamation-triangle"></i>',
'MEDIUM': '<i class="fas fa-exclamation-circle"></i>',
'LOW': '<i class="fas fa-info-circle"></i>'
};
return icons[severity] || '<i class="fas fa-question"></i>';
}
function formatDate(dateString) {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
function formatMonth(monthString) {
const [year, month] = monthString.split('-');
return new Date(year, month - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function updateLastUpdate() {
const elem = document.getElementById('lastUpdate');
if (elem) elem.innerHTML = `<i class="fas fa-clock me-1"></i>Last update: ${new Date().toLocaleTimeString()}`;
}
function showLoading() {
const tbody = document.getElementById('cveTableBody');
if (tbody) tbody.innerHTML = `<tr><td colspan="5" class="text-center py-4"><div class="spinner-border text-primary"></div><p class="mt-2 text-muted">Loading...</p></td></tr>`;
}
function showError(message) {
console.error(message);
alert(message);
}
document.addEventListener('DOMContentLoaded', () => {
initializeApp();
setupEventListeners();
loadVendors();
});
function initializeApp() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-bs-theme', savedTheme);
updateThemeIcon(savedTheme);
}
function setupEventListeners() {
const handlers = {
'darkModeToggle': toggleDarkMode,
'refreshBtn': () => state.currentVendor && loadVendorData(state.currentVendor, true),
'clearFilters': clearFiltersHandler,
'exportJSON': () => exportData('json'),
'exportCSV': () => exportData('csv'),
'searchBtn': performSearch
};
Object.entries(handlers).forEach(([id, handler]) => {
const elem = document.getElementById(id);
if (elem) elem.addEventListener('click', handler);
});
const filterForm = document.getElementById('filterForm');
if (filterForm) filterForm.addEventListener('submit', (e) => { e.preventDefault(); applyFilters(); });
const searchInput = document.getElementById('searchInput');
if (searchInput) searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); });
const prevPage = document.getElementById('prevPage');
const nextPage = document.getElementById('nextPage');
if (prevPage) prevPage.querySelector('a')?.addEventListener('click', (e) => { e.preventDefault(); changePage(-1); });
if (nextPage) nextPage.querySelector('a')?.addEventListener('click', (e) => { e.preventDefault(); changePage(1); });
}
async function loadVendors() {
try {
const response = await fetch('/api/vendors');
const data = await response.json();
if (data.vendors) {
renderVendorList(data.vendors);
renderVendorDropdown(data.vendors);
if (data.vendors.length > 0) loadVendorData(data.vendors[0].code);
}
} catch (error) {
console.error('Error loading vendors:', error);
showError('Failed to load vendors list');
}
}
function renderVendorList(vendors) {
const vendorList = document.getElementById('vendorList');
if (!vendorList) return;
vendorList.innerHTML = vendors.map(vendor => `
<li class="nav-item">
<a class="nav-link" href="#" data-vendor="${vendor.code}">
${vendor.name}
<span class="badge bg-primary float-end">${vendor.total || 0}</span>
</a>
</li>
`).join('');
vendorList.querySelectorAll('a[data-vendor]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
loadVendorData(link.getAttribute('data-vendor'));
vendorList.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
});
});
}
function renderVendorDropdown(vendors) {
const dropdown = document.getElementById('vendorDropdown');
if (!dropdown) return;
dropdown.innerHTML = vendors.map(vendor => `
<li><a class="dropdown-item" href="#" data-vendor="${vendor.code}">${vendor.name}</a></li>
`).join('');
dropdown.querySelectorAll('a[data-vendor]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
loadVendorData(link.getAttribute('data-vendor'));
});
});
}
async function loadVendorData(vendorCode, forceRefresh = false) {
state.currentVendor = vendorCode;
state.currentPage = 1;
showLoading();
try {
const [cvesResponse, statsResponse] = await Promise.all([
fetch(`/api/cve/${vendorCode}?limit=${state.itemsPerPage}&offset=0`),
fetch(`/api/stats/${vendorCode}`)
]);
const cvesData = await cvesResponse.json();
const statsData = await statsResponse.json();
updateTitle(vendorCode);
updateStats(statsData.stats);
renderCVETable(cvesData.cves);
updateCharts(statsData.stats);
updateLastUpdate();
} catch (error) {
console.error('Error loading vendor data:', error);
showError('Failed to load CVE data');
}
}
function updateTitle(vendorCode) {
const vendors = {
'microsoft': 'Microsoft', 'apple': 'Apple', 'fortinet': 'Fortinet',
'cisco': 'Cisco', 'adobe': 'Adobe', 'oracle': 'Oracle',
'google': 'Google', 'linux': 'Linux Kernel', 'vmware': 'VMware',
'paloalto': 'Palo Alto Networks', 'docker': 'Docker', 'kubernetes': 'Kubernetes'
};
const mainTitle = document.getElementById('mainTitle');
if (mainTitle) mainTitle.innerHTML = `<i class="fas fa-shield-alt text-primary me-2"></i>${vendors[vendorCode] || vendorCode} CVEs`;
}
function updateStats(stats) {
const elements = {
statTotal: stats.total || 0,
statCritical: stats.severity?.CRITICAL || 0,
statHigh: stats.severity?.HIGH || 0,
statMonth: stats.this_month || 0
};
Object.entries(elements).forEach(([id, value]) => {
const elem = document.getElementById(id);
if (elem) elem.textContent = value;
});
}
function renderCVETable(cves) {
const tbody = document.getElementById('cveTableBody');
if (!tbody) return;
if (!cves || cves.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="text-center py-4 text-muted"><i class="fas fa-inbox fa-3x mb-3"></i><p>No CVEs found</p></td></tr>`;
return;
}
tbody.innerHTML = cves.map(cve => `
<tr onclick="showCVEDetails('${cve.cve_id}')" style="cursor: pointer;">
<td><span class="cve-id">${cve.cve_id}</span></td>
<td><span class="badge badge-${(cve.severity || 'unknown').toLowerCase()}">${getSeverityIcon(cve.severity)} ${cve.severity || 'UNKNOWN'}</span></td>
<td><strong>${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'}</strong></td>
<td><div class="text-truncate" style="max-width: 400px;">${escapeHtml(cve.description || 'No description available')}</div></td>
<td><small class="text-muted">${formatDate(cve.published_date)}</small></td>
</tr>
`).join('');
if (cves.length >= state.itemsPerPage) {
const paginationNav = document.getElementById('paginationNav');
if (paginationNav) paginationNav.classList.remove('d-none');
}
}
async function showCVEDetails(cveId) {
const modalElement = document.getElementById('cveModal');
if (!modalElement) return;
const modal = new bootstrap.Modal(modalElement);
const modalTitle = document.getElementById('cveModalTitle');
const modalBody = document.getElementById('cveModalBody');
if (modalTitle) modalTitle.textContent = cveId;
if (modalBody) modalBody.innerHTML = '<div class="text-center"><div class="spinner-border"></div></div>';
modal.show();
try {
const response = await fetch(`/api/cve/${state.currentVendor}`);
const data = await response.json();
const cve = data.cves.find(c => c.cve_id === cveId);
if (cve && modalBody) {
const references = cve.references ? JSON.parse(cve.references) : [];
const cweIds = cve.cwe_ids ? JSON.parse(cve.cwe_ids) : [];
modalBody.innerHTML = `
<div class="row">
<div class="col-md-6 mb-3">
<h6>Severity</h6>
<span class="badge badge-${(cve.severity || 'unknown').toLowerCase()} fs-6">${getSeverityIcon(cve.severity)} ${cve.severity || 'UNKNOWN'}</span>
</div>
<div class="col-md-6 mb-3">
<h6>CVSS Score</h6>
<p class="fs-4 mb-0"><strong>${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'}</strong></p>
${cve.cvss_vector ? `<small class="text-muted">${cve.cvss_vector}</small>` : ''}
</div>
</div>
<div class="mb-3"><h6>Description</h6><p>${escapeHtml(cve.description || 'N/A')}</p></div>
<div class="row">
<div class="col-md-6 mb-3"><h6>Published</h6><p>${formatDate(cve.published_date)}</p></div>
<div class="col-md-6 mb-3"><h6>Modified</h6><p>${formatDate(cve.last_modified)}</p></div>
</div>
${cweIds.length > 0 ? `<div class="mb-3"><h6>CWE IDs</h6><p>${cweIds.map(cwe => `<span class="badge bg-secondary me-1">${cwe}</span>`).join('')}</p></div>` : ''}
${references.length > 0 ? `<div class="mb-3"><h6>References</h6><ul class="list-unstyled">${references.slice(0, 5).map(ref => `<li><a href="${ref}" target="_blank"><i class="fas fa-external-link-alt me-1"></i>${ref}</a></li>`).join('')}</ul></div>` : ''}
`;
}
} catch (error) {
console.error('Error loading CVE details:', error);
if (modalBody) modalBody.innerHTML = '<p class="text-danger">Error loading CVE details</p>';
}
}
function updateCharts(stats) {
updateTrendChart(stats.monthly || {});
updateSeverityChart(stats.severity || {});
}
function updateTrendChart(monthlyData) {
const ctx = document.getElementById('trendChart');
if (!ctx) return;
if (state.charts.trend) state.charts.trend.destroy();
const months = Object.keys(monthlyData).sort();
state.charts.trend = new Chart(ctx, {
type: 'bar',
data: {
labels: months.map(m => formatMonth(m)),
datasets: [{
label: 'CVE Count',
data: months.map(m => monthlyData[m]),
backgroundColor: 'rgba(13, 110, 253, 0.5)',
borderColor: 'rgba(13, 110, 253, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } }
}
});
}
function updateSeverityChart(severityData) {
const ctx = document.getElementById('severityChart');
if (!ctx) return;
if (state.charts.severity) state.charts.severity.destroy();
state.charts.severity = new Chart(ctx, {
type: 'pie',
data: {
labels: ['Critical', 'High', 'Medium', 'Low'],
datasets: [{
data: [severityData.CRITICAL || 0, severityData.HIGH || 0, severityData.MEDIUM || 0, severityData.LOW || 0],
backgroundColor: ['rgba(220, 53, 69, 0.8)', 'rgba(253, 126, 20, 0.8)', 'rgba(13, 202, 240, 0.8)', 'rgba(108, 117, 125, 0.8)']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom' } }
}
});
}
async function applyFilters() {
state.filters.severity = document.getElementById('filterSeverity')?.value || '';
state.filters.year = document.getElementById('filterYear')?.value || '';
state.currentPage = 1;
showLoading();
try {
let url = `/api/cve/${state.currentVendor}/filter?limit=${state.itemsPerPage}&offset=0`;
if (state.filters.severity) url += `&severity=${state.filters.severity}`;
if (state.filters.year) url += `&year=${state.filters.year}`;
const response = await fetch(url);
const data = await response.json();
renderCVETable(data.cves);
} catch (error) {
console.error('Error applying filters:', error);
showError('Failed to apply filters');
}
}
function clearFiltersHandler() {
const severityFilter = document.getElementById('filterSeverity');
const yearFilter = document.getElementById('filterYear');
if (severityFilter) severityFilter.value = '';
if (yearFilter) yearFilter.value = '';
state.filters = { severity: '', year: '' };
if (state.currentVendor) loadVendorData(state.currentVendor);
}
function changePage(delta) {
state.currentPage += delta;
if (state.currentPage < 1) state.currentPage = 1;
const offset = (state.currentPage - 1) * state.itemsPerPage;
fetch(`/api/cve/${state.currentVendor}?limit=${state.itemsPerPage}&offset=${offset}`)
.then(r => r.json())
.then(data => {
renderCVETable(data.cves);
const currentPage = document.getElementById('currentPage');
if (currentPage) currentPage.textContent = state.currentPage;
})
.catch(console.error);
}
async function performSearch() {
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
const query = searchInput.value.trim();
const resultsDiv = document.getElementById('searchResults');
if (!resultsDiv) return;
if (query.length < 3) {
resultsDiv.innerHTML = '<p class="text-muted">Please enter at least 3 characters</p>';
return;
}
resultsDiv.innerHTML = '<div class="text-center"><div class="spinner-border"></div></div>';
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
resultsDiv.innerHTML = `<div class="list-group">${data.results.map(cve => `
<a href="#" class="list-group-item list-group-item-action" onclick="showCVEDetails('${cve.cve_id}'); return false;">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1 cve-id">${cve.cve_id}</h6>
<span class="badge badge-${(cve.severity || 'unknown').toLowerCase()}">${cve.severity}</span>
</div>
<p class="mb-1 small">${escapeHtml(cve.description.substring(0, 150))}...</p>
<small class="text-muted">${formatDate(cve.published_date)}</small>
</a>`).join('')}</div>`;
} else {
resultsDiv.innerHTML = '<p class="text-muted">No results found</p>';
}
} catch (error) {
console.error('Error searching:', error);
resultsDiv.innerHTML = '<p class="text-danger">Search failed</p>';
}
}
function exportData(format) {
if (!state.currentVendor) return;
window.open(`/api/export/${state.currentVendor}/${format}`, '_blank');
}

27
templates/404.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Page Not Found | CVE Monitor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="col-md-6 text-center">
<i class="fas fa-exclamation-triangle text-warning" style="font-size: 5rem;"></i>
<h1 class="display-1 fw-bold mt-4">404</h1>
<h2 class="mb-4">Page Not Found</h2>
<p class="lead text-muted mb-4">
The page you are looking for doesn't exist or has been moved.
</p>
<a href="/" class="btn btn-primary btn-lg">
<i class="fas fa-home me-2"></i>Go Home
</a>
</div>
</div>
</div>
</body>
</html>

27
templates/500.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 - Server Error | CVE Monitor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="col-md-6 text-center">
<i class="fas fa-server text-danger" style="font-size: 5rem;"></i>
<h1 class="display-1 fw-bold mt-4">500</h1>
<h2 class="mb-4">Internal Server Error</h2>
<p class="lead text-muted mb-4">
Something went wrong on our end. Please try again later.
</p>
<a href="/" class="btn btn-primary btn-lg">
<i class="fas fa-home me-2"></i>Go Home
</a>
</div>
</div>
</div>
</body>
</html>

89
templates/base.html Normal file
View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="pl" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
<link href="{{ config.BOOTSTRAP_CSS_CDN }}" rel="stylesheet">
<link rel="stylesheet" href="{{ config.FONTAWESOME_CDN }}">
{% if config.ENABLE_CHARTS %}
<script src="{{ config.CHARTJS_CDN }}"></script>
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="fas fa-shield-alt me-2"></i>
<strong>{{ config.APP_NAME }}</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-building me-1"></i> Vendors
</a>
<ul class="dropdown-menu dropdown-menu-end" id="vendorDropdown">
<li><div class="text-center py-2"><div class="spinner-border spinner-border-sm"></div></div></li>
</ul>
</li>
{% if config.ENABLE_DARK_MODE %}
<li class="nav-item">
<button class="btn btn-sm btn-outline-light ms-2" id="darkModeToggle" title="Toggle theme">
<i class="fas fa-moon"></i>
</button>
</li>
{% endif %}
<li class="nav-item">
<span class="navbar-text ms-3 small text-muted" id="lastUpdate">
<i class="fas fa-clock me-1"></i>Loading...
</span>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-3 col-lg-2 d-md-block sidebar">
<div class="position-sticky pt-3">
<h6 class="sidebar-heading px-3 mt-3 mb-2 text-muted text-uppercase">
<span>Vendors</span>
</h6>
<ul class="nav flex-column" id="vendorList">
<li class="nav-item">
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</li>
</ul>
</div>
</div>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
{% block content %}{% endblock %}
</main>
</div>
</div>
{% block modals %}{% endblock %}
<script src="{{ config.BOOTSTRAP_JS_CDN }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% block extra_scripts %}{% endblock %}
</body>
</html>

240
templates/index.html Normal file
View File

@@ -0,0 +1,240 @@
{% extends "base.html" %}
{% block title %}{{ config.APP_NAME }} - Dashboard{% endblock %}
{% block content %}
<!-- Header -->
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2" id="mainTitle">
<i class="fas fa-shield-alt text-primary me-2"></i>
CVE Dashboard
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="refreshBtn">
<i class="fas fa-sync-alt"></i> Refresh
</button>
{% if config.ENABLE_EXPORT %}
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-download"></i> Export
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" id="exportJSON">Export JSON</a></li>
<li><a class="dropdown-item" href="#" id="exportCSV">Export CSV</a></li>
</ul>
{% endif %}
</div>
{% if config.ENABLE_SEARCH %}
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#searchModal">
<i class="fas fa-search"></i> Search
</button>
{% endif %}
</div>
</div>
<!-- Stats Cards -->
<div class="row mb-4" id="statsCards">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-primary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-muted">Total CVEs</h6>
<h3 class="card-title mb-0" id="statTotal">-</h3>
</div>
<div class="fs-1 text-primary"><i class="fas fa-bug"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-danger">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-muted">Critical</h6>
<h3 class="card-title mb-0 text-danger" id="statCritical">-</h3>
</div>
<div class="fs-1 text-danger"><i class="fas fa-skull-crossbones"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-muted">High Risk</h6>
<h3 class="card-title mb-0 text-warning" id="statHigh">-</h3>
</div>
<div class="fs-1 text-warning"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-info">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-muted">This Month</h6>
<h3 class="card-title mb-0 text-info" id="statMonth">-</h3>
</div>
<div class="fs-1 text-info"><i class="fas fa-calendar-alt"></i></div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mb-3">
<div class="card-body">
<form class="row g-3" id="filterForm">
<div class="col-md-4">
<label class="form-label">Severity</label>
<select class="form-select" id="filterSeverity">
<option value="">All Severities</option>
<option value="CRITICAL">Critical</option>
<option value="HIGH">High</option>
<option value="MEDIUM">Medium</option>
<option value="LOW">Low</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Year</label>
<select class="form-select" id="filterYear">
<option value="">All Years</option>
<option value="2026">2026</option>
<option value="2025">2025</option>
<option value="2024">2024</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary me-2">
<i class="fas fa-filter"></i> Apply
</button>
<button type="button" class="btn btn-secondary" id="clearFilters">
<i class="fas fa-times"></i> Clear
</button>
</div>
</form>
</div>
</div>
<!-- CVE Table -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Vulnerabilities</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="cveTable">
<thead>
<tr>
<th>CVE ID</th>
<th>Severity</th>
<th>CVSS</th>
<th>Description</th>
<th>Published</th>
</tr>
</thead>
<tbody id="cveTableBody">
<tr>
<td colspan="5" class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2 text-muted">Loading CVE data...</p>
</td>
</tr>
</tbody>
</table>
</div>
<nav id="paginationNav" class="d-none">
<ul class="pagination justify-content-center mb-0">
<li class="page-item disabled" id="prevPage">
<a class="page-link" href="#"><i class="fas fa-chevron-left"></i></a>
</li>
<li class="page-item active">
<span class="page-link" id="currentPage">1</span>
</li>
<li class="page-item" id="nextPage">
<a class="page-link" href="#"><i class="fas fa-chevron-right"></i></a>
</li>
</ul>
</nav>
</div>
</div>
{% if config.ENABLE_CHARTS %}
<!-- Charts -->
<div class="row mb-4">
<div class="col-lg-8 mb-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>CVE Trend (Last 12 Months)</h5>
</div>
<div class="card-body">
<canvas id="trendChart" height="80"></canvas>
</div>
</div>
</div>
<div class="col-lg-4 mb-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-chart-pie me-2"></i>Severity Distribution</h5>
</div>
<div class="card-body">
<canvas id="severityChart"></canvas>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block modals %}
<!-- CVE Detail Modal -->
<div class="modal fade" id="cveModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cveModalTitle">CVE Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="cveModalBody"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% if config.ENABLE_SEARCH %}
<!-- Search Modal -->
<div class="modal fade" id="searchModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Search CVEs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<input type="text" class="form-control" id="searchInput" placeholder="Search by CVE ID or keyword...">
<button class="btn btn-primary" id="searchBtn">
<i class="fas fa-search"></i> Search
</button>
</div>
<div id="searchResults"></div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}