mirror of
https://github.com/PegaProx/project-pegaprox.git
synced 2026-08-12 15:27:47 +08:00
Big release. New top-level features: - Cost Dashboard / Chargeback — per-VM/tenant rollup, price book, recommendations, PDF + CSV export - Power & Carbon Tracking — Redfish/IPMI/RAPL aggregation, kWh + CO₂e dashboard, PDF + CSV - Network Topology Visualization — interactive nodes/bridges/bonds/VLANs/VMs map - Snapshot Schedules — per-VM or per-tag cron with retention (count + age), 60s tick scheduler - Config Drift Detection — 6h fingerprint scanner with sorted-CSV normalisation (no false positives on tags/content/nodes) - SIEM Forwarder — Syslog (UDP/TCP, RFC 5424), Splunk HEC, Elasticsearch, Loki, generic webhook; per-target TLS verify, retry queue - Cloud-Init Template Library — curated upstream cloud images + custom URL/upload, hardened deploy path - PWA + Web Push — installable app, offline shell cache, VAPID push (no third-party gateway) - Insights tab — capacity ETA, fragmentation, idle/oversized VMs, power outliers + PDF export - Audit Search v2 — faceted full-text + CSV export, fail-closed HMAC chain incl. cluster + severity - DR Drill Wizard — read-only 11-check structured dry-run for Site Recovery plans, JSON + PDF report 10 new blueprints registered; 4 background workers (drift scanner, SIEM forwarder, snapshot scheduler, push handler) — all idempotent + restart-safe. Comprehensive security audit (3 rounds): - C-1: command injection in template library — shlex.quote() + URL/regex whitelist - H-1: CSRF skip on JSON POSTs — Origin/Referer enforced on every state-changing /api/* - H-2: 21 transitive CVEs — cryptography 47, requests 2.33.1, pyOpenSSL 26.1, PyJWT 2.12.1, pyasn1 0.6.3, pillow 12.2.0; pip-audit clean - M-1: VAPID private key encrypted at rest (AES-GCM, transparent migration) - M-2: audit HMAC includes cluster + severity, fail-closed (legacy 5-field fallback for pre-0.9.9 entries) - M-3: 17 str(e) leaks replaced with logging.exception() + generic message - M-4: SIEM TLS verify per-target, default true (removed hardcoded verify=False) - M-5: opaque session revocation token, constant-time compare - M-10: V2P password scrubbed on phase=completed/failed - M-11: webhook URL credential redactor before logging - M-12: push endpoint host whitelist (RFC1918/loopback/metadata refused) - B-1: api/push.py used flask.session instead of request.session (every push endpoint 401'd) — replaced with _current_user() helper Air-gap mode hardening: - /-route now injects localStorage flag prelude server-side when air_gap_mode=true so the very first page load on a fresh browser doesn't hit cdn.jsdelivr while waiting for /auth/check - html2canvas onerror handlers (4 spots) refuse CDN fallback when air-gap is on - html2canvas shipped locally (static/js/html2canvas.min.js) Bumps: - pegaprox/constants.py + web/src/constants.js → Beta 0.9.9, build 2026.05.03 - version.json → 0.9.9, update_files now 200 entries - README.md extended with new feature sections; "What's New" block dropped in favour of GitHub releases as source of truth - i18n: every new feature shipped in DE/EN/FR/ES/PT/KO/IT
112 lines
4.0 KiB
JavaScript
112 lines
4.0 KiB
JavaScript
// PegaProx Service Worker
|
|
// MK May 2026 — wake-up push pattern + light static caching.
|
|
// Live data is NEVER cached — only static shell + images.
|
|
// On push event we fetch /api/push/inbox to show the freshest notification.
|
|
|
|
const CACHE_NAME = 'pegaprox-shell-v3';
|
|
const SHELL_ASSETS = [
|
|
'/',
|
|
'/manifest.webmanifest',
|
|
'/images/pegaprox.png',
|
|
'/favicon.ico',
|
|
];
|
|
|
|
self.addEventListener('install', (e) => {
|
|
// pre-cache shell so the app boots offline. Failures are non-fatal.
|
|
e.waitUntil(
|
|
caches.open(CACHE_NAME).then(c => Promise.allSettled(SHELL_ASSETS.map(u => c.add(u))))
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
// drop old caches
|
|
e.waitUntil(
|
|
caches.keys().then(keys => Promise.all(
|
|
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
|
|
)).then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// Network-only for /api/* (auth + live data must hit server).
|
|
// Network-first with cache fallback for static (so offline still loads shell).
|
|
self.addEventListener('fetch', (e) => {
|
|
const url = new URL(e.request.url);
|
|
|
|
if (url.origin !== self.location.origin) return; // pass through external
|
|
if (e.request.method !== 'GET') return; // pass through non-GET
|
|
if (url.pathname.startsWith('/api/')) return; // pass through API
|
|
if (url.pathname.startsWith('/ws')) return; // pass through ws
|
|
if (url.pathname.startsWith('/.well-known/')) return; // pass through ACME
|
|
|
|
// For shell + static: try network, then cache, then show whatever we have.
|
|
e.respondWith(
|
|
fetch(e.request).then(resp => {
|
|
// only cache successful basic GETs
|
|
if (resp.ok && resp.type === 'basic') {
|
|
const clone = resp.clone();
|
|
caches.open(CACHE_NAME).then(c => c.put(e.request, clone)).catch(() => {});
|
|
}
|
|
return resp;
|
|
}).catch(() => caches.match(e.request).then(r => r || caches.match('/')))
|
|
);
|
|
});
|
|
|
|
// Wake-up push: fetch the latest inbox entry and showNotification.
|
|
self.addEventListener('push', (e) => {
|
|
e.waitUntil(
|
|
fetch('/api/push/inbox?unread=1', { credentials: 'include' })
|
|
.then(r => r.ok ? r.json() : { items: [] })
|
|
.then(({ items }) => {
|
|
if (!items || !items.length) {
|
|
// wake-up arrived but nothing in inbox (could be a test or already-cleared)
|
|
return self.registration.showNotification('PegaProx', {
|
|
body: 'You have a new alert',
|
|
icon: '/images/pegaprox.png',
|
|
badge: '/images/pegaprox.png',
|
|
tag: 'pegaprox-generic',
|
|
});
|
|
}
|
|
// show only the freshest item (avoid spamming if many backed up)
|
|
const item = items[0];
|
|
const sev = (item.severity || 'info').toLowerCase();
|
|
const opts = {
|
|
body: item.body || '',
|
|
icon: '/images/pegaprox.png',
|
|
badge: '/images/pegaprox.png',
|
|
tag: item.tag || `pegaprox-${item.id}`,
|
|
renotify: true,
|
|
requireInteraction: sev === 'critical',
|
|
data: { url: item.url || '/', id: item.id },
|
|
};
|
|
return self.registration.showNotification(item.title || 'PegaProx', opts);
|
|
})
|
|
.catch(err => {
|
|
// network down or session expired — best-effort generic notification
|
|
return self.registration.showNotification('PegaProx', {
|
|
body: 'New activity (open the app to view)',
|
|
icon: '/images/pegaprox.png',
|
|
tag: 'pegaprox-fallback',
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
self.addEventListener('notificationclick', (e) => {
|
|
const target = (e.notification.data && e.notification.data.url) || '/';
|
|
e.notification.close();
|
|
e.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(list => {
|
|
// focus an existing tab if one is open at our origin
|
|
for (const c of list) {
|
|
if (c.url.startsWith(self.location.origin)) {
|
|
c.focus();
|
|
if ('navigate' in c) c.navigate(target).catch(() => {});
|
|
return;
|
|
}
|
|
}
|
|
return clients.openWindow(target);
|
|
})
|
|
);
|
|
});
|